//示例: 父子進程中數據的關系(其實基本沒關系) int main(int argc, char *argv[]) { signal(SIGCHLD, SIG_IGN); int count = 10; pid_t pid = fork(); if (pid == -1) err_exit("fork error"); else if (pid == 0)//子進程 { ++ count; cout << "In child: pid = " << getpid() << ", ppid = " << getppid() << endl; cout << "count = " << count << endl; } else if (pid > 0)//父進程 { ++ count; cout << "In parent: pid = " << getpid() << ", child pid = " << pid << endl; cout << "count = " << count << endl; } exit(0); }
//深入理解: Hello World 為什麼會打印8次 int main(int argc, char *argv[]) { signal(SIGCHLD, SIG_IGN); fork();//每個fork創建一個子進程,然後復制父親的進程,繼續向下執行,所以就像一個二叉樹,有4層,所以一共執行了8次 hello wold fork(); fork(); cout << "Hello World" << endl; exit(0); }
//示例: 產生N個子進程 int main(int argc, char *argv[]) { signal(SIGCHLD, SIG_IGN); int processCount; cin >> processCount; for (int i = 0; i < processCount; ++i) { pid_t pid = fork(); if (pid < 0) err_exit("fork error"); else if (pid == 0) { cout << "Child ..." << endl; exit(0); } } exit(0); }
COW初窺: 在Linux程序中,fork()會產生一個和父進程完全相同的子進程,但子進程在此後多會exec系統調用,出於效率考慮,Linux中引入了“寫時復制“技術,也就是只有進程空間的各段的內容要發生變化時,才會將父進程的內容復制一份給子進程。 那麼子進程的物理空間沒有代碼,怎麼去取指令執行exec系統調用呢? 在fork之後exec之前兩個進程用的是相同的物理空間(內存區),子進程的代碼段、數據段、堆棧都是指向父進程的物理空間,也就是說,兩者的虛擬空間不同,但其對應的物理空間是同一個。當父子進程中有更改相應段的行為發生時,再為子進程相應的段分配物理空間,如果不是因為exec,內核會給子進程的數據段、堆棧段分配相應的物理空間(至此兩者有各自的進程空間,互不影響),而代碼段繼續共享父進程的物理空間(兩者的代碼完全相同)。而如果是因為exec,由於兩者執行的代碼不同,子進程的代碼段也會分配單獨的物理空間。 COW詳述: 現在有一個父進程P1,這是一個主體,那麼它是有靈魂也就身體的。現在在其虛擬地址空間(有相應的數據結構表示)上有:正文段,數據段,堆,棧這四個部分,相應的,內核要為這四個部分分配各自的物理塊。即:正文段塊,數據段塊,堆塊,棧塊。 1. 現在P1用fork()函數為進程創建一個子進程P2, 內核: (1)復制P1的正文段,數據段,堆,棧這四個部分,注意是其內容相同。 (2)為這四個部分分配物理塊,P2的:正文段->P1的正文段的物理塊,其實就是不為P2分配正文段塊,讓P2的正文段指向P1的正文段塊,數據段->P2自己的數據段塊(為其分配對應的塊),堆->P2自己的堆塊,棧->P2自己的棧塊。 如下圖所示:從左到右大的方向箭頭表示復制內容。 2.寫時復制技術:內核只為新生成的子進程創建虛擬空間結構,它們復制於來自父進程的虛擬空間結構,但是不為這些段分配物理內存,它們共享父進程的物理空間,當父子進程中有更改相應段的行為發生時,再為子進程相應的段分配物理空間。 3. vfork():這個做法更加火爆,內核連子進程的虛擬地址空間結構也不創建了,直接共享了父進程的虛擬空間,當然了,這種做法就順水推舟的共享了父進程的物理空間 小結: 進程是一個主體,那麼它就有靈魂與身體,系統必須為實現它創建相應的實體, 靈魂實體與物理實體。這兩者在系統中都有相應的數據結構表示,物理實體更是體現了它的物理意義。 傳統的fork()系統調用直接把所有的資源復制給新創建的進程。這種實現過於簡單並且效率低下,因為它拷貝的數據也許並不共享,更糟的情況是,如果新進程打算立即執行一個新的映像,那麼所有的拷貝都將前功盡棄。Linux的fork()使用寫時拷貝(copy-on-write)頁實現。寫時拷貝是一種可以推遲甚至免除拷貝數據的技術。內核此時並不復制整個進程地址空間,而是讓父進程和子進程共享同一個拷貝。只有在需要寫入的時候,數據才會被復制,從而使各個進程擁有各自的拷貝。也就是說,資源的復制只有在需要寫入的時候才進行,在此之前,只是以只讀方式共享。這種技術使地址空間上的頁的拷貝被推遲到實際發生寫入的時候。在頁根本不會被寫入的情況下{舉例來說:fork()後立即調用exec()}它們就無需復制了。fork()的實際開銷就是復制父進程的頁表以及給子進程創建惟一的進程描述符。在一般情況下,進程創建後都會馬上運行一個可執行的文件,這種優化可以避免拷貝大量根本就不會被使用的數據(地址空間裡常常包含數十兆的數據)。由於Unix強調進程快速執行的能力,所以這個優化是很重要的。這裡補充一點:Linux COW與exec沒有必然聯系。 string str1 = "hello world"; string str2 = str1; 之後執行代碼: str1[1]='q'; str2[1]='w'; 在開始的兩個語句後,str1和str2存放數據的地址是一樣的,而在修改內容後,str1的地址發生了變化,而str2的地址還是原來的,這就是C++中的COW技術的應用;