多線程程序中fork導致的一些問題


  最近項目中,在使用多線程和多進程時,遇到了些問題。

  問題描述:在多線程程序中fork出一個新進程,發現新的進程無法正常工作。

  解決辦法:將開線程的代碼放在fork以后。也就是放在新的子進程中進行創建。

  產生原因:在使用fork時會將原來進程中的所有內存數據復制一份保存在子進程中。但是在拷貝的時候,但是線程是無法被拷貝的。如果在原來線程中加了鎖,在使用的時候會造成死鎖。以下是具體的例子(轉發):

  
  在多線程程序里,在”自身以外的線程存在的狀態”下一使用fork的話,就可能引起各種各樣的問題.比較典型的例子就是,fork出來的子進程可能會死鎖.請不要,在不能把握問題的原委的情況下就在多線程程序里fork子進程.

  能引起什么問題呢?

  那看看實例吧.一執行下面的代碼,在子進程的執行開始處調用doit()時,發生死鎖的機率會很高.

void* doit(void*) {
    static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    pthread_mutex_lock(&mutex);
    struct timespec ts = {10, 0}; nanosleep(&ts, 0); // 10秒寢る
                                                     // 睡10秒
    pthread_mutex_unlock(&mutex);
    return 0;
}

int main(void) {
        pthread_t t;

        pthread_create(&t, 0, doit, 0);                                 // 做成並啟動子線程
        if (fork() == 0) {
              //子進程
             //在子進程被創建的瞬間,父的子進程在執行nanosleep的場合比較多
              doit(0);

              return 0;
        }
        pthread_join(t, 0); //
         // 等待子線程結束
}


以下是說明死鎖的理由:
一般的,fork做如下事情
   1. 父進程的內存數據會原封不動的拷貝到子進程中
   2. 子進程在單線程狀態下被生成


在內存區域里,靜態變量mutex的內存會被拷貝到子進程里.而且,父進程里即使存在多個線程,但它們也不會被繼承到子進程里. fork的這兩個特征就是造成死鎖的原因.
譯者注: 死鎖原因的詳細解釋 ---
   1. 線程里的doit()先執行.
   2. doit執行的時候會給互斥體變量mutex加鎖.
   3. mutex變量的內容會原樣拷貝到fork出來的子進程中(在此之前,mutex變量的內容已經被線程改寫成鎖定狀態).
   4.子進程再次調用doit的時候,在鎖定互斥體mutex的時候會發現它已經被加鎖,所以就一直等待,直到擁有該互斥體的進程釋放它(實際上沒有人擁有這個mutex鎖).
   5.線程的doit執行完成之前會把自己的mutex釋放,但這是的mutex和子進程里的mutex已經是兩份內存.所以即使釋放了mutex鎖也不會對子進程里的mutex造成什么影響.

例如,請試着考慮下面那樣的執行流程,就明白為什么在上面多線程程序里不經意地使用fork就造成死鎖了*3.
1.    在fork前的父進程中,啟動了線程1和2
2.    線程1調用doit函數
3.    doit函數鎖定自己的mutex
4.    線程1執行nanosleep函數睡10秒
5.    在這兒程序處理切換到線程2
6.    線程2調用fork函數
7.    生成子進程
8.    這時,子進程的doit函數用的mutex處於”鎖定狀態”,而且,解除鎖定的線程在子進程里不存在
9.    子進程的處理開始
10.   子進程調用doit函數
11.   子進程再次鎖定已經是被鎖定狀態的mutex,然后就造成死鎖

像這里的doit函數那樣的,在多線程里因為fork而引起問題的函數,我們把它叫 做”fork-unsafe函數”.反之,不能引起問題的函數叫做”fork-safe函數”.雖然在一些商用的UNIX里,源於OS提供的函數(系統調 用),在文檔里有fork-safety的記載,但是在 Linux(glibc)里當然!不會被記載.即使在POSIX里也沒有特別的規定,所以那些函數是fork-safe的,幾乎不能判別.不明白的話,作 為unsafe考慮的話會比較好一點吧.(2004/9/12追記)Wolfram Gloger說過,調用異步信號安全函數是規格標准,所以試着調查了一下,在pthread_atforkの這個地方里有” In the meantime*5, only a short list of async-signal-safe library routines are promised to be available.”這樣的話.好像就是這樣.

隨便說一下,malloc函數就是一個維持自身固有mutex的典型例子,通常情況下它是fork-unsafe的.依賴於malloc函數的函數有很多,例如printf函數等,也是變成fork-unsafe的.

直到目前為止,已經寫上了thread+fork是危險的,但是有一個特例需要告訴大家.”fork后馬上調用exec的場合,是作為一個特列不會產生問題的”. 什么原因呢..?exec函數*6一被調用,進程的”內存數據”就被臨時重置成非常漂亮的狀態.因此,即使在多線程狀態的進程里,fork后不馬上調用一切危險的函數,只是調用exec函數的話,子進程將不會產生任何的誤動作.但是,請注意這里使用的”馬上”這個詞.即使exec前僅僅只是調用一回printf(“I’m child process”),也會有死鎖的危險.
譯者注:exec函數里指明的命令一被執行,該命令的內存映像就會覆蓋父進程的內存空間.所以,父進程里的任何數據將不復存在.

本blog的理解:查看前面進程創建中,子進程在創建后,是寫時復制的,也就是子進程剛創建時,與父進程一樣的副本,當exce后,那么老的地址空間被丟棄,而被新的exec的命令的內存的印像覆蓋了進程的內存空間,所以鎖的狀態無關緊要了。
如何規避災難呢?
為了在多線程的程序中安全的使用fork,而規避死鎖問題的方法有嗎?試着考慮幾個.

規避方法1:做fork的時候,在它之前讓其他的線程完全終止.
在fork之前,讓其他的線程完全終止的話,則不會引起問題.但這僅僅是可能的情況.還有,因為一些原因而其他線程不能結束就執行了fork的時候,就會是產生出一些解析困難的不具合的問題.

規避方法2:fork后在子進程中馬上調用exec函數

(2004/9/11 追記一些忘了寫的東西)
不用使用規避方法1的時候,在fork后不調用任何函數(printf等)就馬上調用execl等,exec系列的函數.如果在程序里不使用”沒有exec就fork”的話,這應該就是實際的規避方法吧.
譯者注:筆者的意思可能是把原本子進程應該做的事情寫成一個單獨的程序,編譯成可執行程序后由exec函數來調用.

規避方法3:”其他線程”中,不做fork-unsafe的處理
除了調用fork的線程,其他的所有線程不要做 fork-unsafe的處理.為了提高數值計算的速度而使用線程的場合*7,這可能是fork- safe的處理,但是在一般的應用程序里則不是這樣的.即使僅僅是把握了那些函數是fork-safe的,做起來還不是很容易的.fork-safe函 數,必須是異步信號安全函數,而他們都是能數的過來的.因此,malloc/new,printf這些函數是不能使用的.
規避方法4:使用pthread_atfork函數,在即將fork之前調用事先准備的回調函數.apue中詳細介紹了它
使用pthread_atfork函數,在即將 fork之前調用事先准備的回調函數,在這個回調函數內,協商清除進程的內存數據.但是關於OS提供的函數 (例:malloc),在回調函數里沒有清除它的方法.因為malloc里使用的數據結構在外部是看不見的.因此,pthread_atfork函數幾乎 是沒有什么實用價值的.
規避方法5:在多線程程序里,不使用fork
就是不使用fork的方法.即用pthread_create來代替fork.這跟規避策2一樣都是比較實際的方法,值得推薦.

*1:生成子進程的系統調用
*2:全局變量和函數內的靜態變量
*3:如果使用Linux的話,查看pthread_atfork函數的man手冊比較好.關於這些流程都有一些解釋.
*4:Solaris和HP-UX等
*5:從fork后到exec執行的這段時間
*6:≒execve系統調用
*7:僅僅做四則演算的話就是fork-safe的

  總結:在程序正常運行時出現不能正常工作,而在調試時又能正常工作。則可以考慮死鎖的情況!

 

      


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM