天天看點

fork()、vfork()、clone()的差別

            因為生活的複雜,這是一個并行的世界,在同一時刻,會發生很多奇妙的事情,北方下雪,南方下雨,這裡在吃飯,那邊在睡覺,有人在學習,有人在運動,是以這時一個多彩多姿的世界,每天都發生着很多事情,是以要想很好的表現這個世界,協調完成一件事兒,就得用到多程序或者多線程。是以程序是程式猿一定會接觸到的一個東西,他能使我們的程式效率提高,高效的完成多任務,并行執行。下面主要看看産生程序或線程的三個函數。

       fork,vfork,clone都是linux的系統調用,這三個函數分别調用了sys_fork、sys_vfork、sys_clone,最終都調用了do_fork函數,差别在于參數的傳遞和一些基本的準備工作不同,主要用來linux建立新的子程序或線程(vfork創造出來的是線程)。

       程序的四要素:

       (1)有一段程式供其執行(不一定是一個程序所專有的),就像一場戲必須有自己的劇本。

       (2)有自己的專用系統堆棧空間(私有财産)

       (3)有程序控制塊(task_struct)(“有身份證,PID”)

       (4)有獨立的存儲空間。

          缺少第四條的稱為線程,如果完全沒有使用者空間稱為核心線程,共享使用者空間的稱為使用者線程。

一、fork()

           fork()函數調用成功:傳回兩個值; 父程序:傳回子程序的PID;子程序:傳回0;

                                    失敗:傳回-1;

          fork 創造的子程序複制了父親程序的資源(寫時複制技術),包括記憶體的内容task_struct内容(2個程序的pid不同)。這裡是資源的複制不是指針的複制。

          說到fork(),就不得不說一個技術:(Copy-On-Write)寫時複制技術。

          盜用一張圖,感覺描述的确實挺到位:

fork()、vfork()、clone()的差別

          我們都知道fork建立程序的時候,并沒有真正的copy記憶體(聽着好像沖突了,資源的指派為什麼有沒有真正的指派呢?),因為我們知道,對于fork來講,有一個很讨厭的東西叫exec系列的系統調用,它會勾引子程序另起爐竈。如果建立子程序就要記憶體拷貝的的話,一執行exec,辛辛苦苦拷貝的記憶體又被完全放棄了。由于fork()後會産生一個和父程序完全相同的子程序,但子程序在此後多會exec系統調用,處于效率考慮,linux中引入了“寫時複制技術-Copy-On-Write”。

            換言之,在fork()之後exec之前兩個程序用的是相同的實體空間(記憶體區),先把頁表映射關系建立起來,并不真正将記憶體拷貝。子程序的代碼段、資料段、堆棧都是指向父程序的實體空間,也就是說,兩者的虛拟空間不同,但其對應的實體空間是同一個。當父程序中有更改相應段的行為發生時,如程序寫通路,再為子程序相應的段配置設定實體空間,如果不是因為exec,核心會給子程序的資料段、堆棧段配置設定相應的實體空間(至此兩者有各自的程序空間,互不影響),而代碼段繼續共享父程序的實體空間(兩者的代碼完全相同)。而如果是因為exec,由于兩者執行的代碼不同,子程序的代碼段也會配置設定單獨的實體空間。fork時子程序獲得父程序資料空間、堆和棧的複制是以變量的位址(當然是虛拟位址)是一樣的。

          具體過程是這樣的:

         fork子程序完全複制父程序的棧空間,也複制了頁表,但沒有複制實體頁面,是以這時虛拟位址相同,實體位址也相同,但是會把父子共享的頁面标記為“隻讀”,如果父子程序一直對這個頁面是同一個頁面,直到其中任何一個程序要對共享的頁面“寫操作”,這時核心會複制一個實體頁面給這個程序使用,同時修改頁表。而把原來的隻讀頁面标記為“可寫”,留給另外一個程序使用。這就是所謂的“寫時複制”。

      在了解上:可以認為fork後,這兩個相同的虛拟位址指向的是不同的實體位址,這樣友善了解父程序之間的獨立性。

      但實際上,linux為了提高fork的效率,采用了copy-on-write技術,fork後,這兩個虛拟位址實際上指向相同的實體位址。(記憶體頁),隻有任何一個程序試圖修改這個虛拟位址裡的内容前,兩個虛拟位址才會指向不同的實體位址。新的實體位址的内容從源實體位址中複制得到。

問題:fork采用了這種寫時複制的機制,那麼fork出來子程序後,理論上子程序和父程序那個先排程呢(理論效率分析,個人覺得有一定的道理)?

       fork之後核心一般會通過将子程序放在隊列的前面,以讓子程序先執行,因為很多情況下子程序要馬上執行exec,會清空棧、堆,這些和父程序共享的空間,加載新的代碼段。。這就避免了父程序“寫時複制”拷貝共享頁面的機會。如果父程序先排程很可能寫共享頁面,而子程序什麼也沒做,會産生“寫時複制”的無用功。是以,一般子程序先排程。避免因無意義的複制而造成效率的下降。

下面來看一個例子:

#include"stdio.h"
int main() 
{
		int count = 1;
		int child;
		if(0== fork()) //子程序成功傳回0;
		{ //開始建立子程序
				printf("This is son, his count is: %d. and his pid is: %d\n", ++count, getpid());//子程序的内容
		} 
		else 
		{
				printf("This is father, his count is: %d, his pid is: %d\n", count, getpid());
		}
}
           

運作結果:

fork()、vfork()、clone()的差別

         從結果可以看出子程序和父程序的 PID不同,記憶體資源count是值得複制,子程序改變了count的值,而父程序中的count沒有被改變。有人認為這樣大批量的複制會導緻執行效率過低。其實在複制過程中,子程序複制了父程序的task_struct,系統堆棧空間和頁面表,這意味着上面的程式,我們沒有執行count++前,其實子程序和父程序的count指向的是同一塊記憶體。而當子程序改變了父程序的變量時候,會通過 copy_on_write的手段為所涉及的頁面建立一個新的副本。是以當我們執行++count後,這時候子程序才建立了一個頁面複制原來頁面的内容, 基本資源的複制是必須的,而且是高效的。整體看上去就像是父程序的獨立存儲空間也複制了一遍。這将和下面的vfork有一定的差別。

其次,我們看到子程序和父程序直接 沒有互相幹擾,明顯2者 資源都獨立了。我們看下面程式

#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());
        }
}
           

運作結果:

fork()、vfork()、clone()的差別
fork()、vfork()、clone()的差別

從運作的結果可以看出父子2個程序是同步運作的, 其實不分先後。

二、vfork() 

    vfork是一個過時的應用,vfork也是建立一個子程序,但是子程序共享父程序的空間。在vfork建立子程序之後,父程序阻塞,直到子程序執行了exec()或者exit()。vfork最初是因為fork沒有實作COW機制,而很多情況下fork之後會緊接着exec,而exec的執行相當于之前fork複制的空間全部變成了無用功,是以設計了vfork。而現在fork使用了COW機制,唯一的代價僅僅是複制父程序頁表的代價,是以vfork不應該出現在新的代碼之中。      

     vfork建立出來的不是真正意義上的程序,而是一個線程,因為它缺少經常要素(4),獨立的記憶體資源,看下面的程式:

#include "stdio.h"
int main() {
        int count = 1;
        int child;
        printf("Before create son, the father's count is:%d\n", count);
        if(!(child = vfork()))
         {
                printf("This is son, his pid is: %d and the count is: %d\n", getpid(), ++count);
                exit(1);
        } 
        else
         {
                printf("After son, This is father, his pid is: %d and the count is: %d, and the child is: %d\n", getpid(), count, child);
        }
}
           

運作結果:

fork()、vfork()、clone()的差別

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

    另外由vfork建立的子程序要先于父程序執行,子程序執行時,父程序處于挂起狀态,子程序執行完,喚醒父程序。除非子程序exit或者execve才會喚起父程序,看下面程式:

#include "stdio.h"
int main() 
{
        int count = 1;
        int child;
        printf("Before create son, the father's count is:%d\n", count);
        if(!(child = vfork()))
         {
                int i;
                for(i = 0; i < 100; i++) 
                {
                        printf("This is son, The i is: %d\n", i);
			count++;
                      if(i == 20)
                      {
                        printf("This is son, his pid is: %d and the count is: %d\n", getpid(), ++count);
                        exit(1);
                      }
                }
               
              
        } 
      else 
        {
                printf("After son, This is father, his pid is: %d and the count is: %d, and the child is: %d\n", getpid(), count, child);
        }
}
           

運作結果:

fork()、vfork()、clone()的差別

從運作的結果可以看到父程序總是等子程序執行完畢後才開始繼續執行。

3.clone

       clone是Linux為建立線程設計的(雖然也可以用clone建立程序)。是以可以說clone是fork的更新版本,不僅可以建立程序或者線程,還可以指定建立新的命名空間(namespace)、有選擇的繼承父程序的記憶體、甚至可以将建立出來的程序變成父程序的兄弟程序等等。

        clone函數功能強大,帶了衆多參數,它提供了一個非常靈活自由的常見程序的方法。是以由他建立的程序要比前面2種方法要複雜。clone可以讓你有選擇性的繼承父程序的資源,你可以選擇想vfork一樣和父程序共享一個虛存空間,進而使創造的是線程,你也可以不和父程序共享,你甚至可以選擇創造出來的程序和父程序不再是父子關系,而是兄弟關系。先有必要說下這個函數的結構:

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

     fn為函數指針,此指針指向一個函數體,即想要建立程序的靜态程式(我們知道程序的4要素,這個就是指向程式的指針,就是所謂的“劇本", );

     child_stack為給子程序配置設定系統堆棧的指針(在linux下系統堆棧空間是2頁面,就是8K的記憶體,其中在這塊記憶體中,低位址上放入了值,這個值就是程序控制塊task_struct的值);

      arg就是傳給子程序的參數一般為(0);

     flags為要複制資源的标志,描述你需要從父程序繼承那些資源(是資源複制還是共享,在這裡設定參數:

下面是flags可以取的值

  标志                    含義

  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線程标準,子程序與父程序共享相同的線程群

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

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sched.h>
#define FIBER_STACK 8192
int a;
void * stack;
int do_something(){
		a=10;
		printf("This is son, the pid is:%d, the a is: %d\n", getpid(), a);
		free(stack); 
		exit(1);
}
int main() {
		void * stack;
		a = 1;
		stack = malloc(FIBER_STACK);//為子程序申請系統堆棧
		if(!stack) {
				printf("The stack failed\n");
				exit(0);
		}
		printf("creating son thread!!!\n");
		clone(&do_something, (char *)stack + FIBER_STACK, CLONE_VM|CLONE_VFORK, 0);//建立子線程
		printf("This is father, my pid is: %d, the a is: %d\n", getpid(), a);
		exit(1);
}
           

運作的結果:

son的PID:10692;

father的PID:10691;

parent和son中的a都為10;是以證明他們公用了一份變量a,是指針的複制,而不是值的複制。

問題:clone和fork的差別:

     (1) clone和fork的調用方式很不相同,clone調用需要傳入一個函數,該函數在子程序中執行。

      (2)clone和fork最大不同在于clone不再複制父程序的棧空間,而是自己建立一個新的。 (void *child_stack,)也就是第二個參數,需要配置設定棧指針的空間大小,是以它不再是繼承或者複制,而是全新的創造。

部落格資料參考:

http://blog.csdn.net/xy010902100449/article/details/44851453

http://www.cnblogs.com/blankqdb/archive/2012/08/23/2652386.html

http://blog.chinaunix.net/uid-24774106-id-3361500.html

http://www.linuxidc.com/Linux/2015-03/114888.htm

http://igaozh.iteye.com/blog/1677969

http://blog.chinaunix.net/uid-24410388-id-195503.html

http://blog.chinaunix.net/uid-18921523-id-265538.html

http://blog.csdn.net/wdjhzw/article/details/25614969

感謝各位部落客的分享!