天天看點

Linux中fork,vfork和clone詳解(差別與聯系) fork,vfork,clone fork 寫時複制 vfork fork與vfork clone clone, fork, vfork差別與聯系

unix标準的複制程序的系統調用時fork(即分叉),但是linux,bsd等作業系統并不止實作這一個,确切的說linux實作了三個,fork,vfork,clone(确切說vfork創造出來的是輕量級程序,也叫線程,是共享資源的程序)

系統調用

描述

fork

fork創造的子程序是父程序的完整副本,複制了父親程序的資源,包括記憶體的内容task_struct内容

vfork

vfork建立的子程序與父程序共享資料段,而且由vfork()建立的子程序将先于父程序運作

clone

linux上建立線程一般使用的是pthread庫 實際上linux也給我們提供了建立線程的系統調用,就是clone

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

Linux中fork,vfork和clone詳解(差別與聯系) fork,vfork,clone fork 寫時複制 vfork fork與vfork clone clone, fork, vfork差別與聯系

從運作結果裡面可以看出父子兩個程序的pid不同,堆棧和資料資源都是完全的複制

子程序改變了count的值,而父程序中的count沒有被改變。

子程序與父程序count的位址(虛拟位址)是相同的(注意他們在核心中被映射的實體位址不同)

有人認為這樣大批量的複制會導緻執行效率過低。其實在複制過程中,linux采用了寫時複制的政策。

子程序複制了父程序的task_struct,系統堆棧空間和頁面表,這意味着上面的程式,我們沒有執行count++前,其實子程序和父程序的count指向的是同一塊記憶體。而當子程序改變了變量時候(即對變量進行了寫操作),會通過copy_on_write的手段為所涉及的頁面建立一個新的副本。

是以當我們執行++count後,這時候子程序才建立了一個頁面複制原來頁面的内容,基本資源的複制是必須的,而且是高效的。整體看上去就像是父程序的獨立存儲空間也複制了一遍。

寫入時複制(copy-on-write)是一個被使用在程式設計領域的最佳化政策。其基礎的觀念是,如果有多個呼叫者(callers)同時要求相同資源,他們會共同取得相同的名額指向相同的資源,直到某個呼叫者(caller)嘗試修改資源時,系統才會真正複制一個副本(private copy)給該呼叫者,以避免被修改的資源被直接察覺到,這過程對其他的呼叫隻都是通透的(transparently)。此作法主要的優點是如果呼叫者并沒有修改該資源,就不會有副本(private copy)被建立。

第一代unix系統實作了一種傻瓜式的程序建立:當發出fork()系統調用時,核心原樣複制父程序的整個位址空間并把複制的那一份配置設定給子程序。這種行為是非常耗時的,因為它需要:

為子程序的頁表配置設定頁幀

為子程序的頁配置設定頁幀

初始化子程序的頁表

把父程序的頁複制到子程序相應的頁中

這種建立位址空間的方法涉及許多記憶體通路,消耗許多cpu周期,并且完全破壞了高速緩存中的内容。在大多數情況下,這樣做常常是毫無意義的,因為許多子程序通過裝入一個新的程式開始它們的執行,這樣就完全丢棄了所繼承的位址空間。

現在的linux核心采用一種更為有效的方法,稱之為寫時複制(copy on write,cow)。這種思想相當簡單:父程序和子程序共享頁幀而不是複制頁幀。然而,隻要頁幀被共享,它們就不能被修改,即頁幀被保護。無論父程序還是子程序何時試圖寫一個共享的頁幀,就産生一個異常,這時核心就把這個頁複制到一個新的頁幀中并标記為可寫。原來的頁幀仍然是寫保護的:當其他程序試圖寫入時,核心檢查寫程序是否是這個頁幀的唯一屬主,如果是,就把這個頁幀标記為對這個程序是可寫的。

當程序a使用系統調用fork建立一個子程序b時,由于子程序b實際上是父程序a的一個拷貝,

是以會擁有與父程序相同的實體頁面.為了節約記憶體和加快建立速度的目标,fork()函數會讓子程序b以隻讀方式共享父程序a的實體頁面.同時将父程序a對這些實體頁面的通路權限也設成隻讀.

這樣,當父程序a或子程序b任何一方對這些已共享的實體頁面執行寫操作時,都會産生頁面出錯異常(page_fault int14)中斷,此時cpu會執行系統提供的異常處理函數do_wp_page()來解決這個異常.

do_wp_page()會對這塊導緻寫入異常中斷的實體頁面進行取消共享操作,為寫程序複制一新的實體頁面,使父程序a和子程序b各自擁有一塊内容相同的實體頁面.最後,從異常處理函數中傳回時,cpu就會重新執行剛才導緻異常的寫入操作指令,使程序繼續執行下去.

如果fork簡單的vfork()的做法更加火爆,核心連子程序的虛拟位址空間結構也不建立了,直接共享了父程序的虛拟空間,當然了,這種做法就順水推舟的共享了父程序的實體空間

30

31

32

33

Linux中fork,vfork和clone詳解(差別與聯系) fork,vfork,clone fork 寫時複制 vfork fork與vfork clone clone, fork, vfork差別與聯系

從運作結果可以看到vfork建立出的子程序(線程)共享了父程序的count變量,2者的count指向了同一個記憶體,是以子程序修改了count變量,父程序的 count變量同樣受到了影響。

由vfork創造出來的子程序還會導緻父程序挂起,除非子程序exit或者execve才會喚起父程序

由vfok建立出來的子程序共享了父程序的所有記憶體,包括棧位址,直至子程序使用execve啟動新的應用程式為止

由vfork建立出來得子程序不應該使用return傳回調用者,或者使用exit()退出,但是它可以使用_exit()函數來退出

如果我們使用return來退出,你會發現程式陷入一種邏輯混亂的重複vfork狀态

參見下面的代碼

我們會發現vfork的子程序在使用return後,傳回到了調用處,是以父程序又建立出一個新的vfork程序,

Linux中fork,vfork和clone詳解(差別與聯系) fork,vfork,clone fork 寫時複制 vfork fork與vfork clone clone, fork, vfork差別與聯系

解決這種問題的方法就是不要在程序中使用return,而是使用exit或者_exit來代替

vfork()用法與fork()相似.但是也有差別,具體差別歸結為以下3點

fork() 子程序拷貝父程序的資料段,代碼段.

vfork() 子程序與父程序共享資料段.|

fork() 父子程序的執行次序不确定.

vfork():保證子程序先運作,

vfork()保證子程序先運作,在她調用exec或_exit之後父程序才可能被排程運作。如果在

調用這兩個函數之前子程序依賴于父程序的進一步動作,則會導緻死鎖。

在調用exec或_exit之前與父程序資料是共享的,在它調用exec或_exit之後父程序才可能被排程運作。如果在調用這兩個函數之前子程序依賴于父程序的進一步動作,則會導緻死鎖。當需要改變共享資料段中變量的值,則拷貝父程序

vfork用于建立一個新程序,而該新程序的目的是exec一個新程序,vfork和fork一樣都建立一個子程序,但是它并不将父程序的位址空間完全複制到子程序中,不會複制頁表。因為子程序會立即調用exec,于是也就不會存放該位址空間。不過在子程序中調用exec或exit之前,他在父程序的空間中運作。

如果在調用vfork時子程序依賴于父程序的進一步動作,則會導緻死鎖。由此可見,這個系統調用是用來啟動一個新的應用程式。其次,子程序在vfork()傳回後直接運作在父程序的棧空間,并使用父程序的記憶體和資料。這意味着子程序可能破壞父程序的資料結構或棧,造成失敗。

為了避免這些問題,需要確定一旦調用vfork(),子程序就不從目前的棧架構中傳回,并且如果子程序改變了父程序的資料結構就不能調用exit函數。

子程序還必須避免改變全局資料結構或全局變量中的任何資訊,因為這些改變都有可能使父程序不能繼續。通常,如果應用程式不是在fork()之後立即調用exec(),就有必要在fork()被替換成vfork()之前做仔細的檢查。

因為以前的fork當它建立一個子程序時,将會建立一個新的位址空間,并且拷貝父程序的資源,而往往在子程序中會執行exec調用,這樣,前面的拷貝工作就是白費力氣了,這種情況下,聰明的人就想出了vfork,它産生的子程序剛開始暫時與父程序共享位址空間(其實就是線程的概念了),因為這時候子程序在父程序的位址空間中運作,是以子程序不能進行寫操作,

并且在兒子“霸占”着老子的房子時候,要委屈老子一下了,讓他在外面歇着(阻塞),一旦兒子執行了exec或者exit後,相當于兒子買了自己的房子了,這時候就相當于分家了。此時vfork保證子程序先運作,在她調用exec或exit之後父程序才可能被排程運作。

是以vfork設計用以子程序建立後立即執行execve系統調用加載新程式的情形。在子程序退出或開始新程式之前,核心保證了父程序處于阻塞狀态

用vfork函數建立子程序後,子程序往往要調用一種exec函數以執行另一個程式,當程序調用一種exec函數時,該程序完全由新程式代換,而新程式則從其main函數開始執行,因為調用exec并不建立新程序,是以前後的程序id 并未改變,exec隻是用另一個新程式替換了目前程序的正文,資料,堆和棧段。

參見 <a href="http://linux.die.net/man/2/clone">man手冊</a>

clone函數功能強大,帶了衆多參數,是以由他建立的程序要比前面2種方法要複雜。

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

先有必要說下這個函數的結構

···c

int clone(int (fn)(void ), void *child_stack, int flags, void *arg);

···

這裡fn是函數指針,我們知道程序的4要素,這個就是指向程式的指針,就是所謂的“劇本”, child_stack明顯是為子程序配置設定系統堆棧空間(在linux下系統堆棧空間是2頁面,就是8k的記憶體,其中在這塊記憶體中,低位址上放入了值,這個值就是程序控制塊task_struct的值),flags就是标志用來描述你需要從父程序繼承那些資源, arg就是傳給子程序的參數)。下面是flags可以取的值

牛客oj

九度oj

csdn題解

github代碼

标志

含義

clone_parent

建立的子程序的父程序是調用者的父程序,新程序與建立它的程序成了“兄弟”而不是“父子”

clone_fs

子程序與父程序共享相同的檔案系統,包括root、目前目錄、umask

clone_files

子程序與父程序共享相同的檔案描述符(file descriptor)表

clone_newns

在新的namespace啟動子程序,namespace描述了程序的檔案hierarchy

clone_sighand

子程序與父程序共享相同的信号處理(signal handler)表

clone_ptrace

若父程序被trace,子程序也被trace

clone_vfork

父程序被挂起,直至子程序釋放虛拟記憶體資源

clone_vm

子程序與父程序運作于相同的記憶體空間

clone_pid

子程序在建立時pid與父程序一緻

clone_thread

linux 2.4中增加以支援posix線程标準,子程序與父程序共享相同的線程群

下面的例子是建立一個線程(子程序共享了父程序虛存空間,沒有自己獨立的虛存空間不能稱其為程序)。父程序被挂起當子線程釋放虛存資源後再繼續執行。

34

35

36

37

38

39

Linux中fork,vfork和clone詳解(差別與聯系) fork,vfork,clone fork 寫時複制 vfork fork與vfork clone clone, fork, vfork差別與聯系
<a href="http://lxr.free-electrons.com/source/kernel/fork.c#l1785">實作參見</a>

實作方式思路

系統調用服務例程sys_clone, sys_fork, sys_vfork三者最終都是調用do_fork函數完成.

do_fork的參數與clone系統調用的參數類似, 不過多了一個regs(核心棧儲存的使用者模式寄存器). 實際上其他的參數也都是用regs取的

具體實作的參數不同

clone:

clone的api外衣, 把fn, arg壓入使用者棧中, 然後引發系統調用. 傳回使用者模式後下一條指令就是fn.

sysclone: parent_tidptr, child_tidptr都傳到了 do_fork的參數中

sysclone: 檢查是否有新的棧, 如果沒有就用父程序的棧 (開始位址就是regs.esp)

fork, vfork:

服務例程就是直接調用do_fork, 不過參數稍加修改

clone_flags:

sys_fork: sigchld, 0, 0, null, null, 0

sys_vfork: clone_vfork | clone_vm | sigchld, 0, 0, null, null, 0

使用者棧: 都是父程序的棧.

parent_tidptr, child_ctidptr都是null.

轉載:http://blog.csdn.net/gatieme/article/details/51417488

繼續閱讀