最近項目中,在使用多線程和多進程時,遇到了些問題。
問題描述:在多線程程序中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的
總結:在程序正常運行時出現不能正常工作,而在調試時又能正常工作。則可以考慮死鎖的情況!