可重入函數reentrant function

在哪些情況下是需要考慮函數的可重入性的

1. 進程捕捉信號並對其進行處理時

進程捕獲到信號並對其進行處理時,進程正在執行的正常指令序列就被信號處理程序臨時中斷,它首先執行該信號處理程序中的指令。如果從信號處理程序返回(即在信號處理函數中沒有調用exitlongjump),則繼續執行在捕獲到信號時進程正在執行的正常指令序列(這類似於發生硬件中斷時所做的)。
但是在信號處理程序中,不能夠判斷捕捉到信號時進程執行到何處,則存在非常多的可能情況,以下列舉了兩種可能的情形。
情形一:

如果進程正在執行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中採取的可重入的技術有:
* 動態堆棧變量(各子函數有自己獨立的堆棧空間)
* 受保護的全局變量和靜態變量
* 任務變量

實例二:保護靜態變量

某些函數(如gethostbynamegetpwnam)將計算結果放在靜態結構中,並返回一個指向這個結構的指針。如果我們從併發線程中調用這些函數,那麼將可能發生災難,因爲正在被一個線程使用的結果會被另一個線程悄悄地覆蓋了。

有兩種方法來處理這類線程不安全函數。一種是選擇重寫函數,使得調用者傳遞存放結果的地址。這就消除了所有共享數據,但是它要求程序員還要改寫調用者的代碼。

如果線程不安全函數是難以修改或不可修改的(例如,它是從一個庫中鏈接過來的),那麼另外一種選擇就是使用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;//使用了靜態局部變量
  }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章