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