在哪些情況下是需要考慮函數的可重入性的
1. 進程捕捉信號並對其進行處理時
進程捕獲到信號並對其進行處理時,進程正在執行的正常指令序列就被信號處理程序臨時中斷,它首先執行該信號處理程序中的指令。如果從信號處理程序返回(即在信號處理函數中沒有調用exit或longjump),則繼續執行在捕獲到信號時進程正在執行的正常指令序列(這類似於發生硬件中斷時所做的)。
但是在信號處理程序中,不能夠判斷捕捉到信號時進程執行到何處,則存在非常多的可能情況,以下列舉了兩種可能的情形。
情形一:
如果進程正在執行malloc,在其堆中分配另外的存儲空間,而此時由於捕捉到信號而轉去執行該信號的處理程序,在信號處理程序中又調用了malloc,這是會發生什麼呢??
在這種情況下,因爲malloc函數通常會爲它所分配的存儲區維護一個鏈表,而插入轉去執行信號處理程序時,進程可能正在更改此鏈表,這就可能對進程造成破壞。
情形二:
進程正在執行getpwnam這種將其結果存放在靜態存儲單元中的函數,其間有轉去執行信號處理程序,在信號處理程序中又調用了這個函數,這時又會發生什麼呢?
顯然,在這種情況下,返回給正常調用中的信息會被返回給信號處理程序的信息覆蓋。
因此,在信號處理程序中,要調用異步信號安全(async-signal safe)的,可重入(reentrant )的函數。Single Unix Specificaction說明了這些函數。
2. 多線程
關於多線程,可以參考下面這篇文章:
簡單來說,OS的調度是隨機的,因此在線程執行過程中的任意時刻都有可能被阻塞,然後CPU轉去執行另外一個函數。因此這就會出現和上述相同的問題。
可重入性的本質
重入即表示重複進入,首先它意味着這個函數可以被中斷(正如上面的轉去執行信號處理程序,OS調度,CPU轉去執行另一個線程的函數),其次意味着它除了使用自己棧上的變量以外不依賴於任何環境(包括static),這樣的函數就是purecode(純代碼)可重入,可以允許有該函數的多個副本在運行,由於它們使用的是分離的棧,所以不會互相干擾。
不可重入函數
基於上面關於可重入性的討論,我們可以總結出如下規則,滿足如下條件之一的函數,不是可重入函數。
1. 它們調用了malloc或free
2. 它們使用標準I/O函數。標準I/O庫的很多實現都以不可重入的方式使用了全局數據結構.
3. 函數體內訪問全局變量
4. 已知它們使用靜態數據結構
函數的返回值是指向靜態變量指針
在我的這篇博文中討論了這個問題:
函數返回指針類型與函數的可重入性函數將其執行結果存儲在靜態存儲單元中
在第一節中描述的getpwnam就屬於此類。
線程安全與可重入函數
在第一節中提到過,在多線程環境中是需要考慮函數的可重入性的。現在對這個問題進行詳細地描述。這部分內容主要參考瞭如下博文:
線程安全(thread-safe)
一個函數被稱爲線程安全的(thread-safe),當且僅當被多個併發進程反覆調用時,它會一直產生正確的結果。如果一個函數不是線程安全的,我們就說它是線程不安全的(thread-unsafe)。
根據上面的論述,我們可以確定可重入函數和線程安全函數之間的關係:
可重入函數是線程安全函數的一種,其特點在於它夠被多個線程調用時,不會引用任何共享數據。
顯然,我們在上面描述的不可重入函數都是線程不安全函數,如果在多線程環境中,不採用如何如何任何措施就使用不可重入函數,將會給程序帶來不良的後果,有時甚至會導致程序崩潰,下面就描述可供使用的措施。
方法一:使用同步措施來保護共享變量
將這類線程不安全函數變爲線程安全的,相對比較容易:利用像P和V操作這樣的同步操作來保護共享變量。這個方法的優點是在調用程序中不需要做任何修改,缺點是同步操作將減慢程序的執行時間。
示例一:保護全局變量
假設Exam是int型全局變量,函數Squre_Exam返回Exam平方值。那麼如下函數不具有可重入性。
unsigned int example( int para )
{
unsigned int temp;
Exam = para; // (**)
temp = Square_Exam( );
return temp;
}
此函數若被多個進程調用的話,其結果可能是未知的,因爲當(**)語句剛執行完後,另外一個使用本函數的線程可能正好被激活,那麼當新激活的進程執行到此函數時,將使Exam賦與另一個不同的para值,所以當控制重新回到“temp = Square_Exam( )”後,計算出的temp很可能不是預想中的結果。此函數應如下改進。
unsigned int example( int para ) {
unsigned int temp;
[申請信號量操作] //(1)
Exam = para;
temp = Square_Exam( );
[釋放信號量操作]
return temp;
}
若申請不到“信號量”,說明另外的進程正處於給Exam賦值並計算其平方過程中(即正在使用此信號),本進程必須等待其釋放信號後,纔可繼續執行。若申請到信號,則可繼續執行,但其它進程必須等待本進程釋放信號量後,才能再使用本信號。
保證函數的可重入性的方法:
在寫函數時候儘量使用局部變量(例如寄存器、堆棧中的變量),對於要使用的全局變量要加以保護(如採取關中斷、信號量等方法),這樣構成的函數就一定是一個可重入的函數。
VxWorks中採取的可重入的技術有:
* 動態堆棧變量(各子函數有自己獨立的堆棧空間)
* 受保護的全局變量和靜態變量
* 任務變量
實例二:保護靜態變量
某些函數(如gethostbyname,getpwnam)將計算結果放在靜態結構中,並返回一個指向這個結構的指針。如果我們從併發線程中調用這些函數,那麼將可能發生災難,因爲正在被一個線程使用的結果會被另一個線程悄悄地覆蓋了。
有兩種方法來處理這類線程不安全函數。一種是選擇重寫函數,使得調用者傳遞存放結果的地址。這就消除了所有共享數據,但是它要求程序員還要改寫調用者的代碼。
如果線程不安全函數是難以修改或不可修改的(例如,它是從一個庫中鏈接過來的),那麼另外一種選擇就是使用lock-and-copy(加鎖-拷貝)技術。這個概念將線程不安全函數與互斥鎖聯繫起來。在每個調用線程不安全函數的位置,對函數的返回結果加互斥鎖,然後調用線程不安全函數,動態地爲結果分配內存空間,拷貝函數返回的結果到這個內存空間,然後對互斥鎖解鎖。一個吸引人的變化是定義了一個線程安全的封裝(wrapper)函數,它執行lock-and-copy,然後調用這個封轉函數來取代所有線程不安全的函數。例如下面的gethostbyname的線程安全函數。
struct hostent* gethostbyname_ts(char* host)
{
struct hostent* shared, * unsharedp;
unsharedp = Malloc(sizeof(struct hostent));
P(&mutex)
shared = gethostbyname(hostname);
//*unsharedp = * shared;//不能夠使用淺拷貝,必須使用深拷貝
memcpy(unsharedp, shared, sizeof(struct hostent));
V(&mutex);
return unsharedp;
}
memcpy函數的文檔:
memcpy
方法二:消除共享變量
不可重入版本:
unsigned int next = 1;
int rand(void)
{
next = next * 1103515245 + 12345;
return (unsigned int) (next / 65536) % 32768;
}
消除共享變量後的可重入版本:
int rand_r(unsigned int* nextp)
{
*nextp = *nextp * 1103515245 + 12345;
return (unsigned int) (*nextp / 65536) % 32768;
}
如下列舉了一些可重入函數與不可重入函數:
可重入函數
void strcpy(char *lpszDest, char *lpszSrc)
{
while(*lpszDest++=*lpszSrc++);
*dest=0;
}
不可重入函數
charcTemp;//全局變量
void SwapChar1(char *lpcX, char *lpcY)
{
cTemp=*lpcX;
*lpcX=*lpcY;
lpcY=cTemp;//訪問了全局變量
}
不可重入函數2
void SwapChar2(char *lpcX,char *lpcY)
{
static char cTemp;//靜態局部變量
cTemp=*lpcX;
*lpcX=*lpcY;
lpcY=cTemp;//使用了靜態局部變量
}