線程與fork(2) (進程):把它們一起使用前,請謹慎考慮 (翻譯)[轉載]


本文譯自: http://www.linuxprogrammingblog.com/threads-and-fork-think-twice-before-using-them

作者: Damian Pietras

轉自: http://xorcerer.iteye.com/blog/1409958

譯者:Xorcerer

 

某天,我(原作者:Damian Pietras,下同。下面所有全角括號內的文字,沒有說明都爲譯者所加 ── 譯者 周翀)遇到了一個在多線程程序裏使用 fork(2) 的 bug。我想這值得我寫下這篇關於混合 POSIX 線程和 fork(2) 的文章,因爲這種做法容易導致潛在的問題。

 

當一個多線程程序 fork(2) 之後

 

fork(2) 程序 創建了當前進程的副本,包括所有的內存頁,還有打開文件的句柄等。所有這些工作對於一個 UNIX 程序員來說,都不陌生。子進程和父進程之間一個非常重要的區別是,子進程只有一個線程。 一個程序員也許不希望複製包括所有線程在內的整個進程,而且,這也容易出問題。想想:所有的線程都因爲一個系統調用(這裏指的是 fork(2))而被暫停(Suspended)。所以,fork(2) 僅僅會複製調用它的那個線程。

 

那麼(當前的實現方式)會遇到什麼問題呢?

 

關鍵部分,互斥鎖(mutex)

 

這種做法一個潛在的問題是,當 fork(2) 被調用的時候,某些線程可以正在執行關鍵部分的代碼,在互斥鎖的保護下對數據進行非原子操作。在子進程裏,這些線程消失了,只留下一些修改到一半卻沒有可能“修正”的數據,不可能去確定 “其他線程正在做什麼”和“怎麼做可以保持數據一致”。此外,那些(複製過來的互斥鎖)的狀態是未定義,他們也許不能用(unusable),除非子進程調用 pthread_mutex_init() 去重置他們的狀態爲一個可用的值。它( pthread_mutex_init() )的實現取決於互斥鎖在 fork(2) 執行之後的具體行爲。在我的 Linux 機器上,被鎖定(locked)的互斥鎖的狀態(重置之後)在子進程中仍是(locked)。

 

庫函數

 

上面關於互斥鎖和關鍵代碼的問題,又引出了另一個潛在的問題。理論上,寫一些在多線程上運行並且在調用 fork(2) 之後不會出錯的代碼,是可行的。但是,實踐中,卻有一個問題──庫函數。你不能確認你正在用的庫函數不會使用到全局數據。即使它(用到的庫函數)是線程安全的,它也可能是通過在內部使用互斥鎖來達到目的。你永遠無法確認。即使系統的線程安全的庫函數,也可能使用了互斥鎖。一個潛在的例子是,malloc() 函數,至少在我的多線程程序裏,內部使用了鎖。所以,在其他線程調用 malloc() 的時候調用 fork(2) 是不安全的!一般來說,我們應該怎麼做呢?在一個多線程程序調用 fork(2) 之後,你只應該調用異步安全(async-safe)的函數(在signal(7) http://www.kernel.org/doc/man-pages/online/pages/man7/signal.7.html 列出)。這個列表與你在一個消息回調函數(signal hanlder)裏面可以調用的函數的列表是相似的,而原因也相似:在兩種情況下,在調用一個函數時,線程會被終止(原文爲帶引號的interrupted,由於該線程在新的子進程裏已經不存在,所以翻譯爲終止)。

這裏是幾個在我的系統裏,使用類內部鎖的函數,僅僅是想讓你知道,幾乎沒有東西是安全的:

 

   * malloc()

   * stdio的函數,比如printf() - 這是標準要求的

   * syslog()

 

execve() 和文件句柄

 

似乎使用 execve(2) 來啓動一個需要調用fork(2)的多線程程序,是你唯一明智的選擇。但即使這樣做,也還有一點不足。當調用execve(2) 時,需要注意的是,打開的文件句柄還將維持打開的狀態(在新的子進程中 —— 譯者Xorcerer),可以繼續被讀取和寫入數據。你在調用 execve(2) 之前打開了一個你不希望在新的子進程裏被使用的文件,問題就出現了。這甚至會產生安全方便的問題。對此,有一個解決方案,你必須使用 fcntl(2) 來對每一個打開的文件句柄設施 FD_CLOEXEC 標記,這樣,它們會在新的進程中被自動關閉。不幸的是,在多線程程序裏,這沒那麼簡單。當我們使用 fcntl(2) 去設置 FD_CLOEXEC 時,會有一個間隙:

 

C代碼  收藏代碼
  1. fd = open ("file", O_RDWR | O_CREAT | O_TRUNC,0600);  
  2.   
  3. if(fd <0){  
  4.     perror ("open()");  
  5.   
  6.     return0;  
  7. }  
  8.   
  9. fcntl (fd, F_SETFD, FD_CLOEXEC);  

 

如果另一個線程正好在當前線程執行 open(2) 之後 fcntl(2) 之前調用 fork(2) 和 execve(2) ,那麼得到的新進程將獲得這個文件句柄的副本。這不是我們想要的。一個解決方案已經隨着新標準(如:POSIX.1-2008)和新的 Linux 內核(2.6.23以及之後的版本)到來了。我們現在可以在 open(2) 使用 O_CLOEXEC 標記,所以,“開打文件與設置 FD_CLOEXEC” 已經成爲了一個原子操作。

 

除了使用 open(2) 之外,還有其他的創建文件句柄的方法:使用 dup(2) 複製它們,使用 socket(2) 創建socket,等。所有這些函數現在都有一個相似的標記如O_CLOEXEC或者其他更新的版本(其中某些函數,如dup2(2)沒有一個用於標記位的參數,所以dup3(2)爲此產生了)。

 

值得提到的一點是同樣的東西在單線程程序裏也可能發生,如果它在同一個消息處理函數(singal handler)中使用 fork(2) 和 execve(2) 。這個操作是完全合法的,因爲這兩個函數是異步安全並且允許在消息處理函數中被調用,但是問題是這個程序也許會在調用 open(2) 和 fcntl(2) 之間時,被中斷。

 

想知道更多關於設置 FD_CLOEXEC 新API的信息,請參考 《Ulrich Drepper's blog: Secure File Descriptor Handling》。

 

一個有用的系統函數:pthread_atfork()

 

其中一個嘗試解決多線程程序中使用 fork(2) 的問題的函數是 pthread_atfork()。它擁有如下原型:

 

 

C代碼  收藏代碼
  1. int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));  
 

 

它允許指定在 fork 被調用時的處理函數:

  • prepare 新進程產生之前被調用。
  • parent 新進程產生之後在父進程被調用。
  • child 新進程產生之後,在子進程被調用。

調用的目的是在 fork(2) 被調用時,處理多線程程序的關鍵部分(本文開始部分提及)。一個常見的場景時在 prepare 處理函數中加鎖,在 parent 處理函數解鎖和在 child 處理函數重新初始化鎖。

 

總結

 

在我看來,fork(2) 在多線程程序中有太多的問題,幾乎沒有辦法去正確地執行它。唯一清晰的方式是在調用 fork(2) 之後立即在子進程中調用 execve(2) 。如果你需要做更多的東西,請使用別的方式(而不是這種多線程混搭多進程的方式),真的。從我的經驗看來,並不值得去嘗試使用 fork(2) ,即使有pthread_atfork() 的情況下。我真的希望你在遇到本文提到的問題之前,讀到這篇文章。

 

引用

本文譯自: http://www.linuxprogrammingblog.com/threads-and-fork-think-twice-before-using-them

作者: Damian Pietras

轉載請著名作者,譯者和出處。


發佈了3 篇原創文章 · 獲贊 14 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章