函数的可重入性
1.什么是可重入性
重入一般可以理解為一個(gè)函數(shù)在同時(shí)多次調(diào)用,例如操作系統(tǒng)在進(jìn)程調(diào)度過(guò)程中,或者單片機(jī)、處理器等的中斷的時(shí)候會(huì)發(fā)生重入的現(xiàn)象。
可重入的函數(shù)必須滿足以下三個(gè)條件:
(1)可以在執(zhí)行的過(guò)程中可以被打斷;
(2)被打斷之后,在該函數(shù)一次調(diào)用執(zhí)行完之前,可以再次被調(diào)用(或進(jìn)入,reentered)。
(3)再次調(diào)用執(zhí)行完之后,被打斷的上次調(diào)用可以繼續(xù)恢復(fù)執(zhí)行,并正確執(zhí)行。
可重入函數(shù)可以在任意時(shí)刻被中斷,稍后再繼續(xù)運(yùn)行,不會(huì)丟失數(shù)據(jù)。不可重入(non-reentrant)函數(shù)不能由超過(guò)一個(gè)任務(wù)所共享,除非能確保函數(shù)的互斥(或者使用信號(hào)量,或者在代碼的關(guān)鍵部分禁用中斷)。
通常,以下幾種情況會(huì)受到可重入性的制約:
(1)信號(hào)處理程序A內(nèi)外都調(diào)用了同一個(gè)不可重入函數(shù)B;B在執(zhí)行期間被信號(hào)打斷,進(jìn)入A (A中調(diào)用了B),完事之后返回B被中斷點(diǎn)繼續(xù)執(zhí)行,這時(shí)B函數(shù)的環(huán)境可能改變,其結(jié)果就不可預(yù)料了。
眾所周知,在進(jìn)程中斷期間,系統(tǒng)會(huì)保存和恢復(fù)進(jìn)程的上下文,然而恢復(fù)的上下文僅限于返回地址,cpu寄存器等之類(lèi)的少量上下文,而函數(shù)內(nèi)部使用的諸如全局或靜態(tài)變量,buffer等并不在保護(hù)之列,所以如果這些值在函數(shù)被中斷期間發(fā)生了改變,那么當(dāng)函數(shù)回到斷點(diǎn)繼續(xù)執(zhí)行時(shí),其結(jié)果就不可預(yù)料了。打個(gè)比方,比如malloc,將如一個(gè)進(jìn)程此時(shí)正在執(zhí)行malloc分配堆空間,此時(shí)程序捕捉到信號(hào)發(fā)生中斷,執(zhí)行信號(hào)處理程序中恰好也有一個(gè)malloc,這樣就會(huì)對(duì)進(jìn)程的環(huán)境造成破壞,因?yàn)閙alloc通常為它所分配的存儲(chǔ)區(qū)維護(hù)一個(gè)鏈接表,插入執(zhí)行信號(hào)處理函數(shù)時(shí),進(jìn)程可能正在對(duì)這張表進(jìn)行操作,而信號(hào)處理函數(shù)的調(diào)用剛好覆蓋了進(jìn)程的操作,造成錯(cuò)誤。
(2)多線程共享進(jìn)程內(nèi)部的資源,如果兩個(gè)線程A,B調(diào)用同一個(gè)不可重入函數(shù)F,A線程進(jìn)入F后,線程調(diào)度,切換到B,B也執(zhí)行了F,那么當(dāng)再次切換到線程A時(shí),其調(diào)用F的結(jié)果也是不可預(yù)料的。
常見(jiàn)的不可重入函數(shù)有:
printf --------引用全局變量stdout
malloc --------全局內(nèi)存分配表
free??? --------全局內(nèi)存分配表
2.可重入與線程安全
可重入的定義源于單線程環(huán)境。在單線程環(huán)境下,一段代碼在執(zhí)行中可能被硬件中斷,并轉(zhuǎn)而調(diào)用中斷服務(wù)程序(ISR)。在本次調(diào)用中斷處理函數(shù)之前,有可能中斷處理函數(shù)已經(jīng)在執(zhí)行。因此,任何中斷處理函數(shù)都應(yīng)該是可重入的。
線程安全的概念則源自于多線程環(huán)境。可見(jiàn),他們的起源是不一樣的。那么,他們沒(méi)有什么必然關(guān)系呢。可總結(jié)如下:
(1)一個(gè)線程安全的函數(shù)不一定是可重入的;
(2)一個(gè)可重入的函數(shù)缺也不一定是線程安全的!
3.不可重入的危害
在單線程進(jìn)程中,只存在一個(gè)控制流。因此,這些進(jìn)程所執(zhí)行的代碼無(wú)需重入或是線程安全的。在多線程程序中,相同的功能和資源可以通過(guò)多個(gè)控制流并發(fā)訪問(wèn)。
要保護(hù)資源的完整性,編寫(xiě)的多線程程序代碼必須能重入并是線程安全的。不可重入對(duì)多線程環(huán)境的危害是很大的,甚至?xí)斐上到y(tǒng)崩潰。
4.不可重入的例子
4.1 不可重入且線程不安全
下面這個(gè)swap函數(shù)是不可重入的:
[cpp]?view plain?copy ? ?
可以把t改成線程局部變量,使得該函數(shù)變成線程安全。然而,這樣修改的話,swap函數(shù)依然是不可重入的。例如一個(gè)線程已經(jīng)在執(zhí)行swap函數(shù),這個(gè)時(shí)候在同樣的語(yǔ)境下收到硬件中斷,isr()函數(shù)會(huì)被調(diào)用,進(jìn)而調(diào)用swap,swap的不可重入問(wèn)題就暴露出來(lái)了。
4.2 可重入但是線程安全
我們做一定修改,在swap函數(shù)里,在交換前,對(duì)此時(shí)刻的t全局變量做一個(gè)本地的緩存,在交換結(jié)束的時(shí)候,始終使用該緩存。這樣的話,swap函數(shù)在退出的時(shí)候,全局變量的之跟進(jìn)入的時(shí)候是一樣的。這樣,就可以保證該函數(shù)是線程可重入的。代碼如下:
[cpp]?view plain?copy ? ?
5.預(yù)防不可重入的幾個(gè)原則
原則總結(jié)如下:
(1)不要使用static變量和全局變量,堅(jiān)持只用局部變量;
(2)若必須訪問(wèn)全局變量,利用互斥信號(hào)量來(lái)保護(hù)全局變量;
(3)獲取得知哪些系統(tǒng)調(diào)用是可重入的,在多任務(wù)處理程序中都使用安全的系統(tǒng)調(diào)用;
(4)不調(diào)用其它任何不可重入的函數(shù);
(5)謹(jǐn)慎使用堆棧malloc/new。
6.優(yōu)秀實(shí)踐:如何優(yōu)化已有代碼,使函數(shù)成為可重入?
在多數(shù)情況下,必須用帶有已修改的將要重入的函數(shù)來(lái)替代非重入函數(shù)。非重入函數(shù)不能由多個(gè)線程使用。此外,可能也無(wú)法使非重入函數(shù)變?yōu)榫€程安全。
6.1 返回指向靜態(tài)數(shù)據(jù)的指針的函數(shù)是不可重入的,如何使其變得可重入?
許多非重入函數(shù)會(huì)返回一個(gè)指向靜態(tài)數(shù)據(jù)的指針。可以用以下方法來(lái)避免這種情況:
- 返回動(dòng)態(tài)分配的數(shù)據(jù)。在這種情況下,調(diào)用程序?qū)⒇?fù)責(zé)釋放存儲(chǔ)量。好處在于無(wú)需對(duì)接口進(jìn)行修改。但是,向后兼容性就無(wú)法保證了;現(xiàn)有的使用已修改函數(shù)的單線程程序在不更改的情況下不會(huì)釋放存儲(chǔ)量,這將導(dǎo)致內(nèi)存泄漏。
- 使用調(diào)用程序提供的存儲(chǔ)量。雖然必須修改接口,但是推薦使用這種方法。
使用調(diào)用程序提供的存儲(chǔ)量使非重入標(biāo)準(zhǔn) C 庫(kù)子例程重入。
6.2 在連續(xù)調(diào)用中保存數(shù)據(jù)的函數(shù)是不可重入的,如何修改?
在連續(xù)調(diào)用中將不保存任何數(shù)據(jù),因?yàn)椴煌木€程可能連續(xù)地調(diào)用該函數(shù)。如果函數(shù)必須在連續(xù)調(diào)用中保存某些數(shù)據(jù),比如工作緩存或指針,那么調(diào)用程序應(yīng)提供該數(shù)據(jù)。
請(qǐng)考慮以下例子。函數(shù)返回了字符串中連續(xù)的小寫(xiě)字符。該字符串只在第一次調(diào)用時(shí)提供,就像?strtok?子例程。函數(shù)在到達(dá)字符串的結(jié)尾處時(shí)返回 0。該函數(shù)可通過(guò)以下代碼段來(lái)實(shí)現(xiàn):/* non-reentrant function */ char lowercase_c(char *string) {static char *buffer;static int index;char c = 0;/* stores the string on first call */if (string != NULL) {buffer = string;index = 0;}/* searches a lowercase character */for (; c = buffer[index]; index++) {if (islower(c)) {index++;break;}}return c; }
總結(jié)
- 上一篇: C语言清空缓冲区
- 下一篇: C和C++中struct和typedef