fork,vfork,clone函數的區別及其聯繫

fork,vfork,clone函數的區別及其聯繫

fork

 fork函數用於創建子進程,典型的調用一次,返回兩次的函數,其中返回子進程的PID和0,其中調用進程返回了子進程的PID,而子進程則返回了0。

當發出fork()系統調用時,內核原樣複製父進程的整個地址空間並把複製的那一份分配給子進程(把所有的資源複製給新創建的進程,,進程的pid號不一樣)。但這種複製行爲非常耗時,因爲它需要:爲子進程的頁表分配頁面,爲子進程的頁分配頁面,初始化子進程的頁表,把父進程的頁複製到子進程相應的頁中。由於創建一個物理地址空間的這種方法涉及許多內存訪問,消耗許多CPU週期,並且完全破壞了高速緩存中的內容。在大多數情況下,這樣做常常是毫無意義的,因爲許多子進程通過裝入一個新的程序開始它們的執行,這樣就完全丟棄了所繼承的地址空間。故引入了寫時複製技術(COW)

  • COW技術

內核只爲新生成的子進程創建虛擬空間,父進程和子進程共享頁面而不是複製頁面。然而,只要頁面被共享,它們就不能被修改。無論父進程和子進程何時試圖寫一個共享的頁面,就產生一個錯誤,這時內核就把這個頁複製到一個新的頁面中並標記爲可寫。原來的頁面仍然是寫保護的:當其它進程試圖寫入時,內核檢查寫進程是否是這個頁面的唯一屬主;如果是,它把這個頁面標記爲對這個進程是可寫的。從而fork複製的開銷就是:複製父進程的頁表以及給子進程創建一個進程描述符。在一般情況下,進程創建後都會馬上運行一個可執行的文件,這種優化,可以避免拷貝大量根本就不會被使用的數據(地址空間裏常常包含數十兆的數據)。

故COW的核心是只有進程空間的各段的內容要發生變化時,纔會將父進程的內容複製一份給子進程。

  • 子進程的物理空間沒有代碼,怎麼去取指令執行exec系統調用呢?

在fork之後exec之前兩個進程用的是相同的物理空間(內存區),子進程的代碼段、數據段、堆棧都是指向父進程的物理空間,也就是說,兩者的虛擬空間不同,但其對應的物理空間是同一個。當父子進程中有更改相應段的行爲發生時,再爲子進程相應的段分配物理空間,如果不是因爲exec,內核會給子進程的數據段、堆棧段分配相應的物理空間(至此兩者有各自的進程空間,互不影響),而代碼段繼續共享父進程的物理空間(兩者的代碼完全相同)。而如果是因爲exec,由於兩者執行的代碼不同,子進程的代碼段也會分配單獨的物理空間。    

一般情況下,fork之後內核會通過將子進程放在隊列的前面,以讓子進程先執行,以免父進程執行導致寫時複製,而後子進程執行exec系統調用,因無意義的複製而造成效率的下降。 

vfork

內核連子進程的虛擬地址空間也不創建,直接共享了父進程的虛擬空間

vfork()跟fork()類似,都是創建一個子進程,這兩個函數的的返回值也具有相同的含義。但是vfork()創建的子進程基本上只能做一件事,那就是立即調用_exit()函數或者exec函數族成員,調用任何其它函數(包括exit())、修改任何數據(除了保存vfork()返回值的那個變量)、執行任何其它語句(包括return)都是不應該的。此外,調用vfork()之後,父進程會一直阻塞,直到子進程調用_exit()終止,或者調用exec函數族成員,父進程纔可能被調度運行(由於子進程在父進程中執行(棧中),故父進程會一直阻塞)。子進程對任何數據變量進行修改,都會影響到父進程——故在子進程中不能隨便調用別函數。

  • 爲什麼vfork()子進程中可以調用_exit(),卻不可以調用exit(),也不可以直接return呢?

exit()是對_exit()的封裝,它自己在調用_exit()前會做很多清理工作,其中包括刷新並關閉當前進程使用的流緩衝(比如stdio.h裏面的printf等),由於vfork()的子進程完全共享了父進程地址空間,子進程裏面的流也是共享的父進程的流,所以子進程裏面是不能做這些事的。
直接return就更不行了,子進程return以後,會從當前函數的外部調用點後面繼續執行,這後面子進程可能將會執行很多語句,結果就沒法預料了。

#include <stdio.h>
#include <unistd.h>

void stack1() {
	vfork();
	printf("%d\n",getpid());
}

void stack2() {
	printf("%d",getpid());
	_exit(0);
	printf("%d\n",getpid());//當退出後,此句不執行,即_exit(0)後續的不執行。
	
}

int main() {

	stack1();
	printf("%d goes 1\n", getpid());
	stack2();
	printf("%d goes 2\n", getpid());
	return 0;
}

運行結果爲:(若父進程號爲11,子進程號爲12)12 \n 12 goes 1 1211  \n 11 goes 2。說明了父進程仍然也保存了創建進程的函數棧,並從此處開始執行,當返回到主函數時,若子進程執行過的語句,則不執行(由於IP指針發生了變化,而父子進程共享IP指針)。

故vfork有限制,子進程生成後,父進程在vfork中被內核掛起,直到子進程有了自己的內存空間(exec**{裝載其它執行程序})或退出(_exit)。在此之前,子進程不能從調用vfork的函數中返回(同時,不能修改棧上變量、不能繼續調用除_exit或exec系列之外的函數,否則父進程的數據可能被改寫)。

  • vfork使用說明

  • 由vfork創造出來的子進程還會導致父進程掛起,除非子進程exit或者execve纔會喚起父進程
  • 由vfok創建出來的子進程共享了父進程的所有內存,包括棧地址,直至子進程使用execve啓動新的應用程序爲止
  • 由vfork創建出來得子進程不應該使用return返回調用者,或者使用exit()退出,但是它可以使用_exit()函數來退出
  • fork與vfork的區別

  • fork會複製父進程的頁表,而vfork不會複製,讓子進程共享父進程的頁表
  • fork使用了寫時複製技術,而vfork沒有,即它任何時候都不會複製父進程地址空間
  • fork父子進程執行次序不確定,一般先是子進程執行;vfork保證子進程現在執行。

vfork()保證子進程先運行,在她調用exec或_exit之後父進程纔可能被調度運行。如果在 調用這兩個函數之前子進程依賴於父進程的進一步動作,則會導致死鎖。

clone

clone函數功能強大,帶了衆多參數,因此由他創建的進程要比前面2種方法要複雜,而fork與vfork都是無參數的,即共享那些資源早已規定。

clone可以讓你有選擇性的繼承父進程的資源,你可以選擇想vfork一樣和父進程共享一個虛存空間,從而使創造的是線程,你也可以不和父進程共享,你甚至可以選擇創造出來的進程和父進程不再是父子關係,而是兄弟關係。

三者實現方式

系統調用服務例程sys_clone, sys_fork, sys_vfork三者最終都是調用do_fork函數完成。do_fork的參數與clone系統調用的參數類似, 不過多了一個regs(內核棧保存的用戶模式寄存器), 實際上其他的參數也都是用regs取的。

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章