轉載目的主要了解fork原理,實際fork的使用愈來愈少,緣由也能夠經過本文了解。html
實際在當前的多線程狀況下,fork已經基本無太多可取之處了。linux
fork的設計之出應該就是爲了更方便地使用多進程程序,提升併發性。編程
然而對於多個併發須要共享大量數據時,多線程擁有的內部通訊每每比較高效,而fork只實現了多進程,只能經過相似共享內存方式通訊。安全
在單核時代,你們所編寫的程序都是單進程/單線程程序。隨着計算機硬件技術的發展,進入了多核時代後,爲了下降響應時間,重複充分利用多核cpu的資源,使用多進程編程的手段逐漸被人們接受和掌握。然而由於建立一個進程代價比較大,多線程編程的手段也就逐漸被人們承認和喜好了。多線程
記得在我剛剛學習線程進程的時候就想,爲何不多見人把多進程和多線程結合起來使用呢,把兩者結合起來不是更好嗎?如今想一想當初真是too young too simple,後文就主要討論一下這個問題。併發
進程的經典定義就是一個執行中的程序的實例。系統中的每一個程序都是運行在某個進程的context中的。context是由程序正確運行所需的狀態組成的,這個狀態包括存放在存儲器中的程序的代碼和數據,它的棧、通用目的寄存器的內容、程序計數器(PC)、環境變量以及打開的文件描述符的集合。函數
進程主要提供給上層的應用程序兩個抽象:高併發
線程,就是運行在進程context中的邏輯流。線程由內核自動調度。每一個線程都有它本身的線程context,包括一個惟一的整數線程ID、棧、棧指針、程序計數器(PC)、通用目的寄存器和條件碼。每一個線程和運行在同一進程內的其餘線程一塊兒共享進程context的剩餘部分。這包括整個用戶虛擬地址空間,它是由只讀文本(代碼)、讀/寫數據、堆以及全部的共享庫代碼和數據區域組成。線程也一樣共享打開文件的集合。性能
即進程是資源管理的最小單位,而線程是程序執行的最小單位。學習
在linux系統中,posix線程能夠「看作」爲一種輕量級的進程,pthread_create建立線程和fork建立進程都是在內核中調用__clone函數建立的,只不過建立線程或進程的時候選項不一樣,好比是否共享虛擬地址空間、文件描述符等。
咱們知道經過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以前被某一個線程所持有了,而那個線程卻在子進程中消失了。
按照上文的分析,彷佛多線程中在fork出的子進程中馬上調用exec函數是惟一明智的選擇了,其實即便這樣作仍是有一點不足。由於子進程會繼承父進程中全部已打開的文件描述符,因此在執行exec以前子進程仍然能夠讀寫父進程中的文件,但若是你不但願子進程能讀寫父進程裏的某個已打開的文件該怎麼辦?
或許fcntl設置文件屬性是一種辦法:
1
2
3
4
5
6
|
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以前就不能讀寫父進程中已打開的文件了。
若是你不幸真的碰到了一個要解決多線程中fork的問題的時候,能夠嘗試使用pthread_atfork:
1
|
int
pthread_atfork(
void
(*prepare)(
void
),
void
(*parent)
void
(),
void
(*child)(
void
));
|
由於子進程繼承的是父進程的鎖的拷貝,全部上述並非解鎖了兩次,而是各自獨自解鎖。能夠屢次調用pthread_atfork函數從而設置多套fork處理程序,可是使用多個處理程序的時候。處理程序的調用順序並不相同。parent和child是以它們註冊時的順序調用的,而prepare的調用順序與註冊順序相反。這樣能夠容許多個模塊註冊它們本身的處理程序而且保持鎖的層次(相似於多個RAII對象的構造析構層次)。
須要注意的是pthread_atfork只能清理鎖,但不能清理條件變量。在有些系統的實現中條件變量不須要清理。可是在有的系統中,條件變量的實現中包含了鎖,這種狀況就須要清理。可是目前並無清理條件變量的接口和方法。