fork()、vfork()、clone()的區別(三)

原文鏈接:https://blog.csdn.net/gogokongyin/article/details/51178257

  在linux源碼中這三個調用的執行過程是執行fork(),vfork(),clone()時,通過一個系統調用表映射到sys_fork(),sys_vfork(),sys_clone(),再在這三個函數中去調用do_fork()去做具體的創建進程工作。

一 fork

  fork創造的子進程複製了父親進程的資源(寫時複製技術),包括內存的內容task_struct內容(2個進程的pid不同)。這裏是資源的複製不是指針的複製。說到fork(),就不得不說一個技術:(Copy-On-Write)寫時複製技術。盜用一張圖,感覺描述的確實挺到位:
在這裏插入圖片描述
  我們都知道fork創建進程的時候,並沒有真正的copy內存(聽着好像矛盾了,資源的賦值爲什麼有沒有真正的賦值呢?),因爲我們知道,對於fork來講,有一個很討厭的東西叫exec系列的系統調用,它會勾引子進程另起爐竈。如果創建子進程就要內存拷貝的的話,一執行exec,辛辛苦苦拷貝的內存又被完全放棄了。由於fork()後會產生一個和父進程完全相同的子進程,但子進程在此後多會exec系統調用,處於效率考慮,linux中引入了“寫時複製技術-Copy-On-Write”。
   換言之,在fork()之後exec之前兩個進程用的是相同的物理空間(內存區),先把頁表映射關係建立起來,並不真正將內存拷貝。子進程的代碼段、數據段、堆棧都是指向父進程的物理空間,也就是說,兩者的虛擬空間不同,但其對應的物理空間是同一個。當父進程中有更改相應段的行爲發生時,如進程寫訪問,再爲子進程相應的段分配物理空間,如果不是因爲exec,內核會給子進程的數據段、堆棧段分配相應的物理空間(至此兩者有各自的進程空間,互不影響),而代碼段繼續共享父進程的物理空間(兩者的代碼完全相同)。而如果是因爲exec,由於兩者執行的代碼不同,子進程的代碼段也會分配單獨的物理空間。fork時子進程獲得父進程數據空間、堆和棧的複製所以變量的地址(當然是虛擬地址)是一樣的。
  具體過程是這樣的:
   fork子進程完全複製父進程的棧空間,也複製了頁表,但沒有複製物理頁面,所以這時虛擬地址相同,物理地址也相同,但是會把父子共享的頁面標記爲“只讀”,如果父子進程一直對這個頁面是同一個頁面,直到其中任何一個進程要對共享的頁面“寫操作”,這時內核會複製一個物理頁面給這個進程使用,同時修改頁表。而把原來的只讀頁面標記爲“可寫”,留給另外一個進程使用。這就是所謂的“寫時複製”。
  但實際上,linux爲了提高fork的效率,採用了copy-on-write技術,fork後,這兩個虛擬地址實際上指向相同的物理地址。(內存頁),只有任何一個進程試圖修改這個虛擬地址裏的內容前,兩個虛擬地址纔會指向不同的物理地址。新的物理地址的內容從源物理地址中複製得到。
問題:fork採用了這種寫時複製的機制,那麼fork出來子進程後,理論上子進程和父進程哪個先調度呢(理論效率分析,個人覺得有一定的道理)?
  fork之後內核一般會通過將子進程放在隊列的前面,以讓子進程先執行,因爲很多情況下子進程要馬上執行exec,會清空棧、堆,這些和父進程共享的空間,加載新的代碼段。這就避免了父進程“寫時複製”拷貝共享頁面的機會。如果父進程先調度很可能寫共享頁面,而子進程什麼也沒做,會產生“寫時複製”的無用功。所以,一般子進程先調度。避免因無意義的複製而造成效率的下降。
  如下示例:

#include <stdio.h>

int main()
{
	int count = 1;
	int child;
	int i;
	if(!(child = fork()))
	{

		for(i = 0; i <20; i++)
		{
			printf("This is son, his count is: %d. and his pid is: %d\n", i, getpid());
		}
	} 
	else 
	{
		for(i=0;i<20;i++)
			printf("This is father, his count is: %d, his pid is: %d\n", count, getpid());
	}
}

  而當子進程改變了父進程的變量時候,會通過copy_on_write的手段爲所涉及的頁面建立一個新的副本。子進程才新建了一個頁面複製原來頁面的內容,基本資源的複製是必須的,而且是高效的。整體看上去就像是父進程的獨立存儲空間也複製了一遍。其次,我們看到子進程和父進程直接沒有互相干擾,明顯2者資源都獨立了。從運行的結果可以看出父子2個進程是同步運行的,其實不分先後。

二 vfork

  vfork是一個過時的應用,vfork也是創建一個子進程,但是子進程共享父進程的空間。在vfork創建子進程之後,父進程阻塞,直到子進程執行了exec()或者exit()。vfork最初是因爲fork沒有實現COW機制,而很多情況下fork之後會緊接着exec,而exec的執行相當於之前fork複製的空間全部變成了無用功,所以設計了vfork。而現在fork使用了COW機制,唯一的代價僅僅是複製父進程頁表的代價,所以vfork不應該出現在新的代碼之中。
  vfork創建出來的不是真正意義上的進程,而是一個線程,因爲它缺少進程要素(4),獨立的內存資源,看下面的程序:
  另外由vfork創建的子進程要先於父進程執行,子進程執行時,父進程處於掛起狀態,子進程執行完,喚醒父進程。除非子進程exit或者execve纔會喚起父進程。

三 clone

#define _GNU_SOURCE
#include <sched.h>
int clone(int (*fn)(void *), void *child_stack,
          int flags, void *arg, ...
          /* pid_t *ptid, void *newtls, pid_t *ctid */ );

  clone是Linux爲創建線程設計的(雖然也可以用clone創建進程)。所以可以說clone是fork的升級版本,不僅可以創建進程或者線程,還可以指定創建新的命名空間(namespace)、有選擇的繼承父進程的內存、甚至可以將創建出來的進程變成父進程的兄弟進程等等。
  clone函數功能強大,帶了衆多參數,它提供了一個非常靈活自由的常見進程的方法。因此由他創建的進程要比前面2種方法要複雜。clone可以讓你有選擇性的繼承父進程的資源,你可以選擇想vfork一樣和父進程共享一個虛存空間,從而使創造的是線程,你也可以不和父進程共享,你甚至可以選擇創造出來的進程和父進程不再是父子關係,而是兄弟關係。先有必要說下這個函數的結構:
  
  

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