多線程中fork與mutex

  在多線程程序中fork出一個新進程,發現新的進程沒法正常工做。由於:在使用fork時會將原來進程中的全部內存數據複製一份保存在子進程中。可是在拷貝的時候,可是線程是沒法被拷貝的。若是在原來線程中加了鎖,在使用的時候會形成死鎖。能夠將開線程的代碼放在fork之後。也就是放在新的子進程中進行建立。promise

  在多線程程序裏,在」自身之外的線程存在的狀態」下一使用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秒
    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形成什麼影響.

  以上程序執行流程異步

  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.」這樣的話.好像就是這樣.async

malloc函數就是一個維持自身固有mutex的典型例子,一般狀況下它是fork-unsafe的.依賴於malloc函數的函數有不少,例如printf函數等,也是變成fork-unsafe的.函數


直目前爲止,已經寫上了thread+fork是危險的,可是有一個特例須要告訴你們.」fork後立刻調用exec的場合,是做爲一個特列不會產生問題的」. 什麼緣由呢..?exec函數*6一被調用,進程的」內存數據」就被臨時重置成很是漂亮的狀態.所以,即便在多線程狀態的進程裏,fork後不立刻調用一切危險的函數,只是調用exec函數的話,子進程將不會產生任何的誤動做.可是,請注意這裏使用的」立刻」這個詞.即便exec前僅僅只是調用一回printf(「I’m child process」),也會有死鎖的危險.
譯者注:exec函數裏指明的命令一被執行,該命令的內存映像就會覆蓋父進程的內存空間.因此,父進程裏的任何數據將不復存在.spa

本blog的理解:查看前面進程建立中,子進程在建立後,是寫時複製的,也就是子進程剛建立時,與父進程同樣的副本,當exce後,那麼老的地址空間被丟棄,而被新的exec的命令的內存的印像覆蓋了進程的內存空間,因此鎖的狀態可有可無了。

規避方法1:作fork的時候,在它以前讓其餘的線程徹底終止.
  在fork以前,讓其餘的線程徹底終止的話,則不會引發問題.但這僅僅是可能的狀況.還有,由於一些緣由而其餘線程不能結束就執行了fork的時候,就會是產生出一些解析困難的不具合的問題.
規避方法2:fork後在子進程中立刻調用exec函數
  不用使用規避方法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的

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

相關文章
相關標籤/搜索