前言
在C程序開發中我們會用到很多標準庫的函數,如內存分配的那幾個函數(malloc/calloc/realloc/free),標準輸入輸出的函數(printf/getchar/sprintf…)等。這些標準函數給我們提供了跨平臺的能力,我們知道任何平臺上都會有這些庫,這些函數,以及它們的標準行爲(實際上不同平臺的實現可能會有一些差異,但不在這篇博文的討論範圍內),因此各種通用的模塊,大型同性交友網站上那些庫,一般都會肆無忌憚地直接使用它們。
但是使用的很順手的同時,一個合格的程序員也應該注意到它們的風險。因爲它們不見得是可重入的。
可重入
可重入函數主要用於多任務環境中,一個可重入的函數簡單來說就是可以被中斷的函數,也就是說,可以在這個函數執行的任何時刻中斷它,轉入OS調度下去執行另外一段代碼,而返回控制時不會出現什麼錯誤;而不可重入的函數由於使用了一些系統資源,比如全局變量區,中斷向量表等,所以它如果被中斷的話,可能會出現問題,這類函數是不能運行在多任務環境下的。—— 百度百科
這些標準庫函數可不可重入取決於其實現。在Windows/Linux環境下我們用它們好像從來也沒有出現過什麼問題,這主要是因爲它們的實現中幫助我們做了線程安全方面的事(這句未經嚴格求證),因此即使在多線程環境下也不會出問題。
但是對於我們這些苦逼的嵌入式程序員來說,很有可能你用的這些標準庫函數實際上就是不可重入的。這會出現什麼問題?也許你運行個十天半個月的,啥事情都沒發生,然後突然某一天系統宕機了,結果你還復現不出來;也有可能正好你的業務邏輯完美避開了線程安全的雷區,於是這輩子相安無事(太理想化了)。
嵌入式的標準庫會不可重入主要是因爲本身這些實現就不是爲多任務環境寫的,它並意識不到自己是在一個OS上,自然也不會考慮什麼線程安全不安全的問題。
在CodeWarrior IDE上,直接去看malloc的實現(在IDE的文件夾中找到alloc.c)會發現其用了全局的指針來實現內存管理,而printf的實現也用了一個全局的函數指針變量,所以如果你用了OS的話直接使用這些函數時就可能會出問題。
互斥鎖保護臨界區
每個進程中訪問臨界資源的那段代碼稱爲臨界區(Critical Section)(臨界資源是一次僅允許一個進程使用的共享資源)。每次只准許一個進程進入臨界區,進入後不允許其他進程進入。不論是硬件臨界資源,還是軟件臨界資源,多個進程必須互斥地對它進行訪問。
多個進程中涉及到同一個臨界資源的臨界區稱爲相關臨界區。.——百度百科
那這種情況怎麼辦呢?
一個很常用的技術就是通過在訪問臨界區的時候上鎖,在這裏,由於我們不會直接去改標準函數的實現,所以整個標準函數庫就被我們當做臨界區,同時只能有一個線程訪問其中的函數。
這裏還得意識到,由於malloc、calloc、realloc和free函數都用到同樣的臨界資源,因此它們是相關的,所以同時只能有四個函數中的一個被訪問,因此它們要上的是同一個鎖。
那這樣就可以寫出線程安全的堆實現(就不貼出所有的相關模塊了,沒有的實現的函數就當僞代碼看吧,應該可讀性還是不錯的,應該可以直接看懂)。
/*
*******************************************************************************************
*
* Heap Of The Abstract OS Layer
* 抽象操作系統層的堆
*
* File : MyOSHeap.h
* By : Lin Shijun(http://blog.csdn.net/lin_strong)
* Date: 2019/09/08
* version: V1.0
* NOTE(s):a. This file is to adapt default heap interfaces(malloc/calloc/realloc/free) to
* thread safe interfaces, which use MyOSMutex as lock.
* b. force include this file to redirect all the memory interfaces
* c. the malloc shouldn't be a macro in stdlib.h
*********************************************************************************************
*/
#ifndef _MYOSHEAP_H
#define _MYOSHEAP_H
#include <stdlib.h>
#undef malloc
#define malloc MyOSHeap_malloc
#undef calloc
#define calloc MyOSHeap_calloc
#undef realloc
#define realloc MyOSHeap_realloc
#undef free
#define free MyOSHeap_free
// see the corresponding interface in stdlib.h but thread-safe
void * MyOSHeap_malloc(size_t _Size);
void * MyOSHeap_realloc(void * _Memory, size_t _NewSize);
void * MyOSHeap_calloc(size_t _Count, size_t _Size);
void MyOSHeap_free(void * _Memory);
// for free the memory used by this module, not thread-safe.
void MyOSHeap_Destroy(void);
#endif
/*
*******************************************************************************************
*
* Heap Of The Abstract OS Layer
* 抽象操作系統層的堆
*
* File : MyOSHeap.c
* By : Lin Shijun(http://blog.csdn.net/lin_strong)
* Date: 2019/09/08
* version: V1.0
* NOTE(s):
*********************************************************************************************
*/
/*
******************************************************************************************
* INCLUDES
******************************************************************************************
*/
#include "MyOSHeap.h"
#include "MyOSMutex.h"
#include "MyOS.h"
// resume malloc define
#undef malloc
#undef calloc
#undef realloc
#undef free
/*
******************************************************************************************
* LOCAL FUNCTION
******************************************************************************************
*/
static BOOL _lock(void);
static void _unlock(void);
/*
******************************************************************************************
* INTERFACE IMPLEMENTATIONS
******************************************************************************************
*/
void * MyOSHeap_malloc(size_t _Size){
void * ret = NULL;
BOOL locked;
locked = _lock();
ret = malloc(_Size);
if(locked)
_unlock();
return ret;
}
void * MyOSHeap_realloc(void * _Memory, size_t _NewSize){
void * ret = NULL;
BOOL locked;
locked = _lock();
ret = realloc(_Memory, _NewSize);
if(locked)
_unlock();
return ret;
}
void * MyOSHeap_calloc(size_t _Count, size_t _Size){
void * ret = NULL;
BOOL locked;
locked = _lock();
ret = calloc(_Count, _Size);
if(locked)
_unlock();
return ret;
}
void MyOSHeap_free(void * _Memory){
BOOL locked;
locked = _lock();
free(_Memory);
if(locked)
_unlock();
}
static MyOSMutex _mutex = NULL;
void MyOSHeap_Destroy(void){
if(_mutex != NULL)
MyOSMutex_Destroy(_mutex);
_mutex = NULL;
}
/*
******************************************************************************************
* LOCAL FUNCTION IMPLEMENTATION
******************************************************************************************
*/
// Lazy Man Model
static MyOSMutex _lock_getInstance(void){
MyOS_SR sr;
if(_mutex == NULL){
sr = MyOS_disableInterrupts();
if(_mutex == NULL){
_mutex = MyOSMutex_Create();
}
MyOS_enableInterrupts(sr);
}
return _mutex;
}
static BOOL _lock(void){
return MYOSMUTEXERR_NO == MyOSMutex_Pend(_lock_getInstance(), 0);
}
static void _unlock(void){
MyOSMutex_Post(_mutex);
}
(那些宏定義什麼的在下一節解釋)
這裏這個實現是基於互斥鎖的方式保護不可重入的函數實現的線程安全。printf等同理。
當然,有些大牛可能會說malloc的實現有內存碎片問題什麼的,我要用更好的內存分配器。
嗯,贊同,確實是這樣的,但我們這裏討論的是怎麼把不可重入函數改成線程安全的,那是另一回事。我下一步也打算研究研究tcmalloc,或者基於uCOS的內存管理自己搞個內存分配器。
預處理器/宏 替代技術
跳過之前的討論,現在我們準備了一個線程安全版本的標準庫函數,要替換掉原來對標準庫函數的調用,那我們該怎麼辦呢?
勤快的人可能已經開始打開一個個文件把所有的malloc調用都改爲MyOSHeap_malloc了。但這樣一來很麻煩,二來一要改調用就直接改源碼其實很不利於模塊的通用性。這麼長的名字看着肯定也沒有原來一個簡單的malloc舒服。
所以最好的方案應該就是不動原代碼,源碼中該用malloc還用malloc,但是你得讓它實際調用到線程安全的那個版本。
這裏介紹的這個技術簡單來說就是利用了宏的文本替換功能,預處理器會在預處理階段把所有宏定義過的文本進行替換,這樣,如果在一段代碼前它看到了
#define malloc abcdefg
那後面比如出現了這樣的代碼
p = (char *)malloc(1024);
那實際上在預處理後,也就是在進入編譯器之前,它就會變成。
p = (char *)abcdefg(1024);
因此只要在所有代碼前都加上那個#define,那所有的malloc就都被對應替換掉了。
這也是.h文件中這段
#include <stdlib.h>
#undef malloc
#define malloc MyOSHeap_malloc
#undef calloc
#define calloc MyOSHeap_calloc
#undef realloc
#define realloc MyOSHeap_realloc
#undef free
#define free MyOSHeap_free
所做的事情,實現了一個重定向。
這樣,只要在所有代碼前都#include這個頭文件,就完成了所有替代。
當然,怎麼可能傻到打開所有文件在每一個文件前面都加一行#include。。
編譯器選項中都會有一個強制include文件的選項,我們只要在編譯器選項中選擇強制include這個頭文件,就相當於在每個文件前加了這一行。
具體來說,CodeWarrior中在Edit-Standard Settings-compiler for HC12-Options-Input-Additional include file裏。
而VS2012中,打開工程屬性頁 - 配置屬性 - C/C++ - 高級 - 強制包含文件
然後就在不動源碼的情況下很愉快的把所有的標準庫函數都替代掉了O(∩_∩)O~
其他技術
除了預處理器替代,其實還有些其他技術。
比如鏈接時替代,基本就是如果原先的函數是打包到靜態庫lib文件,然後把庫導入到工程中的話,你可以在工程中直接創建一模一樣名字的函數,這樣,在靜態鏈接時,鏈接器會優先鏈接工程中的那個函數,這樣就把原來的那個函數替代掉了。
鏈接時替代好像也可以通過編譯器選項來實現,但是我沒實踐過,就不說了。
另外還有些替代技術,展開來說又得好大的篇幅,就跳過吧。
結語
結語寫點啥呢。
其實這些個替代技術是從《Test-Driven Development for Embedded C》上學來的,推薦大家去看看這本書,可以學到很多讓代碼寫的更優雅的技術。
熟練運用好這些替代技術還可以玩些做很多更有意思的事情。比如,可以對內存分配進行監控,檢查內存泄露點。CppUTest和Unity的內存泄露檢測就是這麼搞的。
現在我正在寫嵌入式TDD實戰系列,手把手教嵌入式TDD的整個過程,敬請期待。