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