在嵌入式裸機時代,也就是無OS時代,我們在裸機環境下編寫C語言程序非常簡單,實現一個函數,然后將函數接口API提供給其它模塊調用就可以了。比如下面的函數,我們實現一個sum函數,用來求兩個數的和:
但是在一個運行OS的多任務環境中,我們在編寫sum函數時就要注意一些細節了:我們編寫的sum函數可能會被多個任務調用,而且可能在sum函數執行的過程被打斷,接著在另一個任務中再次調用sum函數。而在上面的sum函數實現中,我們定義了一個靜態變量sum用來保存兩個數相加的臨時結果,靜態變量是保存到數據段中的,大家可以想一想,在一個任務A中正在執行sum(1,2)函數中的第4行,此時任務被打斷掛起,接著運行任務B,在任務B中接著執行sum(10,20)函數,執行結束后接著運行任務A,A獲得CPU控制權后繼續運行sum(1,2)的第5行,此時sum(1,2)的返回結果就變成了30,而不是正確結果3。
在一個多任務環境中,如果一個函數可以重復并發調用,而且多次調用并不會影響函數的運行結果,那么這個函數是可重入的,我們稱這個函數為:可重入函數。在上面的sum函數實現中,當其被多次并發調用時,函數的運行結果并不確定,我們稱其為不可重入函數。
我們如何去判定一個函數是可重入的,還是不可重入的呢?很簡單,當一個函數滿足下面任一條件,那么這個函數就是不可重入函數。
- 函數內部使用了全局變量
- 函數內部使用了靜態局部變量
- 函數返回值為全局變量或靜態變量
- 函數內部使用了malloc/free函數
- 函數北部使用了標準I/O函數
- 函數內部調用其它不可重入函數
不可重入函數在一個多任務環境中不能被多次并發調用,如果一個函數可能被多次調用,那么我們設計這個函數時盡量要將其設計為可重入函數。
- 不使用/返回靜態變量、全局變量
- 不使用標準I/O函數
- 不使用malloc/free函數
- 不調用不可重入函數
在函數設計時,只要注意上面的原則,那么我們就可以將一個函數設計為可重入函數,可重入函數在多任務環境下可以被多次并發調用,是線程安全的,程序員可以放心大膽地調用。
理想很豐滿,現實很骨干。我們在編程中如果說不用malloc/free、全局變量,那是不現實的。只要我們使用了這些全局變量,靜態變量,那么函數就變成不可重入了,在多任務環境下使用這個函數就變得線程不安全了,那怎么辦呢?
方法還是有的,一個函數之所以變得不可重入,就是因為函數內有一些資源是全局共享的,在多任務環境下多次并發調用該函數時可能會破壞掉這些共享的全局資源。我們如果把這些資源在訪問的時候保護起來,不讓其它任務訪問(即互斥訪問),即同一時刻只允許一個進程訪問就安全了。這些被保護的資源我們稱為臨界資源,訪問這些臨界資源的代碼段,我們稱之為臨界區。臨界區的訪問方式為互斥訪問,即同一時刻只允許一個進程訪問。
臨界區的實現方式有很多種,不同的操作系統可能會提供不同的實現方式。我們可以通過下面的操作原語來實現一個臨界區:
不同的操作系統,具體的實現手段可能不一樣,常見的方法有:關中斷;實現互斥訪問,比如通過信號量、互斥量、自旋鎖等實現,甚至原子操作等。比如在uc/os操作系統中,我們使用關中斷的方式來實現臨界區,確保函數的線程安全。
而在linux/windows操作系統中,我們通常使用鎖機制來實現臨界區:
在一個不可重入函數中,通過臨界區來實現共享全局資源的互斥訪問,那么在多任務環境下調用這個函數也就變得安全了,也就是說這個不可重入函數是線程安全的。
通過上面的分析,我們可以得出下面的結論:一個函數如果是可重入函數,那么這個函數是線程安全的,其它進程線程都可以對這個函數并發訪問,并不會影響函數的運行結果。如果一個函數是不可重入函數,我們通過臨界區設計對共享全局資源進行互斥訪問,也可以讓這個函數變得線程安全,其它進程線程也可以放心調用。由此,我們得出線程安全與可重入之間的關系如下:
也就是說,一個可重入函數肯定是線程安全的,而線程安全函數并不一定是可重入函數,不可重入函數也有可能是線程安全的,比如我們常見的malloc函數,就是不可重入函數,但是是線程安全的,為什么呢?
通過《C語言嵌入式Linux高級編程》課程學習,我們已經知道,對于我們使用malloc/free申請釋放的內存,glibc在用戶空間實現了一個內存管理器,將各個大小的內存塊鏈成多個全局鏈表進行管理。
當我們使用malloc/free申請釋放內存時,如果申請/釋放的內存塊大小符合規定,一般都是直接對這些全局鏈表進行操作、避免多次系統調用進入內核態,減少系統開銷。因為malloc/free函數對全局鏈表進行了操作,所以malloc/free是不可重入函數。在訪問這些全局鏈表時,我們需要通過鎖機制加以保護,每次malloc/free操作全局鏈表時,其它地方就被互斥訪問了,只有當malloc/free操作全局鏈表完成退出,其它地方的malloc/free才能對這個全局鏈表進行訪問。
通過上面的分析,我們可以看到:malloc/free雖然是不可重入函數,但是通過加鎖對共享全局資源的互斥訪問,也就變得線程安全了,在多任務環境下,每個進程都可以放心大膽地調用它:因為malloc雖然是不可重入函數,但它是線程安全的。
-
嵌入式
+關注
關注
5068文章
19017瀏覽量
303262 -
C語言
+關注
關注
180文章
7598瀏覽量
136191 -
函數
+關注
關注
3文章
4306瀏覽量
62430
發布評論請先 登錄
相關推薦
評論