1. 程式人生 > >謹慎使用多執行緒中的fork

謹慎使用多執行緒中的fork

前言

在單核時代,大家所編寫的程式都是單程序/單執行緒程式。隨著計算機硬體技術的發展,進入了多核時代後,為了降低響應時間,重複充分利用多核cpu的資源,使用多程序程式設計的手段逐漸被人們接受和掌握。然而因為建立一個程序代價比較大,多執行緒程式設計的手段也就逐漸被人們認可和喜愛了。

記得在我剛剛學習執行緒程序的時候就想,為什麼很少見人把多程序和多執行緒結合起來使用呢,把二者結合起來不是更好嗎?現在想想當初真是too young too simple,後文就主要討論一下這個問題。

程序與執行緒模型

程序的經典定義就是一個執行中的程式的例項。系統中的每個程式都是執行在某個程序的context中的。context是由程式正確執行所需的狀態組成的,這個狀態包括存放在儲存器中的程式的程式碼和資料,它的棧、通用目的暫存器的內容、程式計數器(PC)、環境變數以及開啟的檔案描述符的集合。

程序主要提供給上層的應用程式兩個抽象:

  • 一個獨立的邏輯控制流,它提供一個假象,好像我們程式獨佔的使用處理器。
  • 一個私有的虛擬地址空間,它提供一個假象,好像我們的程式獨佔的使用儲存器系統。

執行緒,就是執行在程序context中的邏輯流。執行緒由核心自動排程。每個執行緒都有它自己的執行緒context,包括一個唯一的整數執行緒ID、棧、棧指標、程式計數器(PC)、通用目的暫存器和條件碼。每個執行緒和執行在同一程序內的其他執行緒一起共享程序context的剩餘部分。這包括整個使用者虛擬地址空間,它是由只讀文字(程式碼)、讀/寫資料、堆以及所有的共享庫程式碼和資料區域組成。執行緒也同樣共享開啟檔案的集合。

即程序是資源管理的最小單位,而執行緒是程式執行的最小單位。

在linux系統中,posix執行緒可以“看做”為一種輕量級的程序,pthread_create建立執行緒和fork建立程序都是在核心中呼叫__clone函式建立的,只不過建立執行緒或程序的時候選項不同,比如是否共享虛擬地址空間、檔案描述符等。

fork與多執行緒

我們知道通過fork建立的一個子程序幾乎但不完全與父程序相同。子程序得到與父程序使用者級虛擬地址空間相同的(但是獨立的)一份拷貝,包括文字、資料和bss段、堆以及使用者棧等。子程序還獲得與父程序任何開啟檔案描述符相同的拷貝,這就意味著子程序可以讀寫父程序中任何開啟的檔案,父程序和子程序之間最大的區別在於它們有著不同的PID。

但是有一點需要注意的是,在Linux中,fork的時候只複製當前執行緒到子程序,在fork(2)-Linux Man Page中有著這樣一段相關的描述:

The child process is created with a single thread--the one that called fork(). The entire virtual address space of the parent is replicated in the child, including the states of mutexes, condition variables, and other pthreads objects; the use of pthread_atfork(3) may be helpful for dealing with problems that this can cause.

也就是說除了呼叫fork的執行緒外,其他執行緒在子程序中“蒸發”了。

這就是多執行緒中fork所帶來的一切問題的根源所在了。

互斥鎖

互斥鎖,就是多執行緒fork大部分問題的關鍵部分。

在大多數作業系統上,為了效能的因素,鎖基本上都是實現在使用者態的而非核心態(因為在使用者態實現最方便,基本上就是通過原子操作或者之前文章中提到的memory barrier實現的),所以呼叫fork的時候,會複製父程序的所有鎖到子程序中。

問題就出在這了。從作業系統的角度上看,對於每一個鎖都有它的持有者,即對它進行lock操作的執行緒。假設在fork之前,一個執行緒對某個鎖進行的lock操作,即持有了該鎖,然後另外一個執行緒呼叫了fork建立子程序。可是在子程序中持有那個鎖的執行緒卻"消失"了,從子程序的角度來看,這個鎖被“永久”的上鎖了,因為它的持有者“蒸發”了。

那麼如果子程序中的任何一個執行緒對這個已經被持有的鎖進行lock操作話,就會發生死鎖。

當然了有人會說可以在fork之前,讓準備呼叫fork的執行緒獲取所有的鎖,然後再在fork出的子程序的中釋放每一個鎖。先不說現實中的業務邏輯以及其他因素允不允許這樣做,這種做法會帶來一個問題,那就是隱含了一種上鎖的先後順序,如果次序和平時不同,就會發生死鎖。

如果你說自己一定可以按正確的順序上鎖而不出錯的話,還有一個隱含的問題是你所不能控制的,那就是庫函式。

因為你不能確定你所用到的所有庫函式都不會使用共享資料,即他們都是完全執行緒安全的。有相當一部分執行緒安全的庫函式都是在內部通過持有互斥鎖的方式來實現的,比如幾乎所有程式都會用到的C/C++標準庫函式malloc、printf等等。

比如一個多執行緒程式在fork之前難免會分配動態記憶體,這就必然會用到malloc函式;而在fork之後的子程序中也難免要分配動態記憶體,這也同樣要用到malloc,可這卻是不安全的,因為有可能malloc內部的鎖已經在fork之前被某一個執行緒所持有了,而那個執行緒卻在子程序中消失了。

exec與檔案描述符

按照上文的分析,似乎多執行緒中在fork出的子程序中立刻呼叫exec函式是唯一明智的選擇了,其實即使這樣做還是有一點不足。因為子程序會繼承父程序中所有已開啟的檔案描述符,所以在執行exec之前子程序仍然可以讀寫父程序中的檔案,但如果你不希望子程序能讀寫父程序裡的某個已開啟的檔案該怎麼辦?

或許fcntl設定檔案屬性是一種辦法:

int fd = open("file", O_RDWR | O_CREAT);

if (fd < 0)

{

    perror("open");

}

fcntl(fd, F_SETFD, FD_CLOEXEC);

但是如果在open開啟file檔案之後,呼叫fcntl設定CLOEXEC屬性之前有其他執行緒fork出了子程序了的話,這個子程序仍然是可以讀寫file檔案。如果用鎖的話,就又回到了上文所討論的情況了。

從Linux 2.6.23版本的核心開始,我們可以在open中設定O_CLOEXEC標誌了,相當於“開啟檔案再設定CLOEXEC”成為了一個原子操作。這樣在fork出的子程序執行exec之前就不能讀寫父程序中已開啟的檔案了。

pthread_atfork

如果你不幸真的碰到了一個要解決多執行緒中fork的問題的時候,可以嘗試使用pthread_atfork:

1	int pthread_atfork(void (*prepare)(void), void (*parent)void(), void (*child)(void));
  • prepare處理函式由父程序在fork建立子程序前呼叫,這個函式的任務是獲取父程序定義的所有鎖。
  • parent處理函式是在fork建立了子程序以後,但在fork返回之前在父程序環境中呼叫的。它的任務是對prepare獲取的所有鎖解鎖。
  • child處理函式在fork返回之前在子程序環境中呼叫,與parent處理函式一樣,它也必須解鎖所有prepare中所獲取的鎖。

因為子程序繼承的是父程序的鎖的拷貝,所有上述並不是解鎖了兩次,而是各自獨自解鎖。可以多次呼叫pthread_atfork函式從而設定多套fork處理程式,但是使用多個處理程式的時候。處理程式的呼叫順序並不相同。parent和child是以它們註冊時的順序呼叫的,而prepare的呼叫順序與註冊順序相反。這樣可以允許多個模組註冊它們自己的處理程式並且保持鎖的層次(類似於多個RAII物件的構造析構層次)。

需要注意的是pthread_atfork只能清理鎖,但不能清理條件變數。在有些系統的實現中條件變數不需要清理。但是在有的系統中,條件變數的實現中包含了鎖,這種情況就需要清理。但是目前並沒有清理條件變數的介面和方法。

結語

  • 在多執行緒程式中最好只用fork來執行exec函式,不要對fork出的子程序進行其他任何操作。
  • 如果確定要在多執行緒中通過fork出的子程序執行exec函式,那麼在fork之前開啟檔案描述符時需要加上CLOEXEC標誌。

參考文獻

  1. Randal E.Bryant, David O'Hallaron. 深入理解計算機系統(第2版). 機械工業出版社, 2010
  2. W.Richard Stevens. UNIX環境高階程式設計(第3版), 人民郵電出版社, 2014
  3. Linux Man Page. fork(2)
  4. Damian Pietras. Threads and fork(): think twice before mixing them, 2009
  5. 雲風. 極不和諧的 fork 多執行緒程式, 2011

 

結語

  • 在多執行緒程式中最好只用fork來執行exec函式,不要對fork出的子程序進行其他任何操作。
  • 如果確定要在多執行緒中通過fork出的子程序執行exec函式,那麼在fork之前開啟檔案描述符時需要加上CLOEXEC標誌。

參考文獻

  1. Randal E.Bryant, David O'Hallaron. 深入理解計算機系統(第2版). 機械工業出版社, 2010
  2. W.Richard Stevens. UNIX環境高階程式設計(第3版), 人民郵電出版社, 2014
  3. Linux Man Page. fork(2)
  4. Damian Pietras. Threads and fork(): think twice before mixing them, 2009
  5. 雲風. 極不和諧的 fork 多執行緒程式, 2011

轉自:https://www.cnblogs.com/liyuan989/p/4279210.html