Linux進程管理——fork()和寫時複製

寫時複製技術最初產生於Unix系統,用於實現一種傻瓜式的進程建立:當發出fork(  )系統調用時,內核原樣複製父進程的整個地址空間並把複製的那一份分配給子進程。這種行爲是很是耗時的,由於它須要:node

 

·      爲子進程的頁表分配頁面linux

·      爲子進程的頁分配頁面xcode

·      初始化子進程的頁表緩存

·      把父進程的頁複製到子進程相應的頁中數據結構

 

建立一個地址空間的這種方法涉及許多內存訪問,消耗許多CPU週期,而且徹底破壞了高速緩存中的內容。在大多數狀況下,這樣作經常是毫無心義的,由於許多子進程經過裝入一個新的程序開始它們的執行,這樣就徹底丟棄了所繼承的地址空間。app

 

如今的Unix內核(包括Linux),採用一種更爲有效的方法稱之爲寫時複製(或COW)。這種思想至關簡單:父進程和子進程共享頁面而不是複製頁面。然而,只要頁面被共享,它們就不能被修改。不管父進程和子進程什麼時候試圖寫一個共享的頁面,就產生一個錯誤,這時內核就把這個頁複製到一個新的頁面中並標記爲可寫。原來的頁面仍然是寫保護的:當其它進程試圖寫入時,內核檢查寫進程是不是這個頁面的惟一屬主;若是是,它把這個頁面標記爲對這個進程是可寫的。函數

 

1. Linux的fork()使用寫時複製

 

      傳統的fork()系統調用直接把全部的資源複製給新建立的進程。這種實現過於簡單而且效率低下,由於它拷貝的數據或許能夠共享(This approach is significantly naïve and inefficient in that it copies much data that might otherwise be shared.)。更糟糕的是,若是新進程打算當即執行一個新的映像,那麼全部的拷貝都將前功盡棄。Linux的fork()使用寫時拷貝(copy-on-write)頁實現。寫時拷貝是一種能夠推遲甚至避免拷貝數據的技術。內核此時並不複製整個進程的地址空間,而是讓父子進程共享同一個地址空間。只用在須要寫入的時候纔會複製地址空間,從而使各個進行擁有各自的地址空間。也就是說,資源的複製是在須要寫入的時候纔會進行,在此以前,只有以只讀方式共享。這種技術使地址空間上的頁的拷貝被推遲到實際發生寫入的時候。在頁根本不會被寫入的狀況下---例如,fork()後當即執行exec(),地址空間就無需被複制了。fork()的實際開銷就是複製父進程的頁表以及給子進程建立一個進程描述符。在通常狀況下,進程建立後都爲立刻運行一個可執行的文件,這種優化,能夠避免拷貝大量根本就不會被使用的數據(地址空間裏經常包含數十兆的數據)。因爲Unix強調進程快速執行的能力,因此這個優化是很重要的。測試

COW技術初窺:優化

 

     在Linux程序中,fork()會產生一個和父進程徹底相同的子進程,但子進程在此後多會exec系統調用,出於效率考慮,linux中引入了「寫時複製「技術,也就是隻有進程空間的各段的內容要發生變化時,纔會將父進程的內容複製一份給子進程。spa

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

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

      在網上看到還有個細節問題就是,fork以後內核會經過將子進程放在隊列的前面,以讓子進程先執行,以避免父進程執行致使寫時複製,然後子進程執行exec系統調用,因無心義的複製而形成效率的降低。

COW詳述:

     如今有一個父進程P1,這是一個主體,那麼它是有靈魂也就身體的。如今在其虛擬地址空間(有相應的數據結構表示)上有:正文段,數據段,堆,棧這四個部 分,相應的,內核要爲這四個部分分配各自的物理塊。即:正文段塊,數據段塊,堆塊,棧塊。至於如何分配,這是內核去作的事,在此不詳述。

1.      如今P1用fork()函數爲進程建立一個子進程P2,

內核:

(1)複製P1的正文段,數據段,堆,棧這四個部分,注意是其內容相同。

(2)爲這四個部分分配物理塊,P2的:正文段->PI的正文段的物理塊,其實就是不爲P2分配正文段塊,讓P2的正文段指向P1的正文段塊,數據段->P2本身的數據段塊(爲其分配對應的塊),堆->P2本身的堆塊,棧->P2本身的棧塊。以下圖所示:同左到右大的方向箭頭表示複製內容。

 

2.       寫時複製技術:內核只爲新生成的子進程建立虛擬空間結構,它們來複制於父進程的虛擬究竟結構,可是不爲這些段分配物理內存,它們共享父進程的物理空間,當父子進程中有更改相應段的行爲發生時,再爲子進程相應的段分配物理空間。

 

 

3.       vfork():這個作法更加火爆,內核連子進程的虛擬地址空間結構也不建立了,直接共享了父進程的虛擬空間,固然了,這種作法就順水推舟的共享了父進程的物理空間

 

經過以上的分析,相信你們對進程有個深刻的認識,它是怎麼一層層體現出本身來的,進程是一個主體,那麼它就有靈魂與身體,系統必須爲實現它建立相應的實體, 靈魂實體與物理實體。這二者在系統中都有相應的數據結構表示,物理實體更是體現了它的物理意義。

     補充一點:Linux COW與exec沒有必然聯繫

PS:實際上COW技術不只僅在Linux進程上有應用,其餘例如C++的String在有的IDE環境下也支持COW技術,即例如:

string str1 = "hello world";
string str2 = str1;

以後執行代碼:

str1[1]='q';
str2[1]='w';

在開始的兩個語句後,str1和str2存放數據的地址是同樣的,而在修改內容後,str1的地址發生了變化,而str2的地址仍是原來的,這就是C++中的COW技術的應用,不過VS2005彷佛已經不支持COW。

2. fork()函數

頭文件

[objc]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. #include<unistd.h>  
  2. #include<sys/types.h>  

函數原型

[objc]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. pid_t fork( void);  

 

(pid_t 是一個宏定義,其實質是int 被定義在#include<sys/types.h>中)
返回值: 若成功調用一次則返回兩個值,子進程返回0,父進程返回子進程ID;不然,出錯返回-1

口訣: 父返子,子返0,fork出錯返-1
示例代碼

[objc]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. #include<sys/types.h> //對於此程序而言此頭文件用不到  
  2. #include<unistd.h>  
  3. #include<stdio.h>  
  4. #include<stdlib.h>  
  5. int main(int argc, charchar ** argv ){  
  6.   
  7.       //因爲會返回兩次,下面的代碼會被執行兩遍  
  8.       //若是成功建立子進程:  
  9.       //1. 父進程返回子進程ID,所以(父進程)會走一遍「分支3」  
  10.       //2. 子進程返回0,所以(子進程)會走一遍「分支2」  
  11.       pid_t pid = fork();  
  12.   
  13.       if (pid < 0){ //分支1  
  14.             fprintf(stderr, "error!");  
  15.       }else if( 0 == pid ){//分支2  
  16.             printf("This is the child process!");  
  17.             _exit(0);  
  18.       }else{//分支3  
  19.             printf("This is the parent process! child process id = %d", pid);  
  20.       }  
  21.       //可能須要時候wait或waitpid函數等待子進程的結束並獲取結束狀態  
  22.       exit(0);  
  23. }  

 

  注意!樣例代碼僅供參考,樣例代碼存在着父進程在子進程結束前結束的可能性。必要的時候可使用wait或 waitpid函數讓父進程等待子進程的結束並獲取子進程的返回狀態。
      fork的另外一個特性是全部由父進程打開的描述符都被複制到子進程中。父、子進程中相同編號的文件描述符在內核中指向同一個file結構體,也就是說,file結構體的引用計數要增長

 

3. Linux的fork()使用寫時複製(詳)

      fork函數用於建立子進程,典型的調用一次,返回兩次的函數,其中返回子進程的PID和0,其中調用進程返回了子進程的PID,而子進程則返回了0,這是一個比較有意思的函數,可是兩個進程的執行順序是不定的。fork()函數調用完成之後父進程的虛擬存儲空間被拷貝給了子進程的虛擬存儲空間,所以也就實現了共享文件等操做。可是虛擬的存儲空間映射到物理存儲空間的過程當中採用了寫時拷貝技術(具體的操做大小是按着頁控制的),該技術主要是將多進程中一樣的對象(數據)在物理存儲其中只有一個物理存儲空間,而當其中的某一個進程試圖對該區域進行寫操做時,內核就會在物理存儲器中開闢一個新的物理頁面,將須要寫的區域內容複製到新的物理頁面中,而後對新的物理頁面進行寫操做。這時就是實現了對不一樣進程的操做而不會產生影響其餘的進程,同時也節省了不少的物理存儲器。

C代碼  
[objc]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. #include<stdio.h>  
  2. #include<stdlib.h>  
  3. #include<unistd.h>  
  4. #include<fcntl.h>  
  5. #include<sys/types.h>  
  6. #include<sys/stat.h>  
  7.   
  8. int main(){  
  9.         char p = 'p';  
  10.         int number = 11;  
  11.   
  12.         if(fork()==0)      /*子進程*/  
  13.         {  
  14.                 p = 'c';      /*子進程對數據的修改*/  
  15.                 printf("p = %c , number = %d \n ",p,number);  
  16.                 exit(0);  
  17.         }  
  18.        /*父進程*/  
  19.         number = 14;  /*父進程對數據修改*/  
  20.         printf("p = %c , number = %d \n ",p,number);  
  21.         exit(0);  
  22. }  

 

[objc]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. $ gcc -g TestWriteCopyTech.c -o TestWriteCopyTech  
  2. $ ./TestWriteCopyTech  
  3. p = p , number = 14    -----父進程打印內容  
  4. $ p = c , number = 11    -----子進程打印內容   


緣由分析:
       因爲存在企圖進行寫操做的部分,所以會發生寫時拷貝過程,子進程中對數據的修改,內核就會建立一個新的物理內存空間。而後再次將數據寫入到新的物理內存空間中。可知,對新的區域的修改不會改變原有的區域,這樣不一樣的空間就區分開來。可是沒有修改的區域仍然是多個進程之間共享。
       fork()函數的代碼段基本是隻讀類型的,並且在運行階段也只是複製,並不會對內容進行修改,所以父子進程是共享代碼段,而數據段、Bss段、堆棧段等會在運行的過程當中發生寫過程,這樣就致使了不一樣的段發生相應的寫時拷貝過程,實現了不一樣進程的獨立空間。
       可是須要注意的是文件操做,因爲文件的操做是經過文件描述符表、文件表、v-node表三個聯繫起來控制的,其中文件表、v-node表是全部的進程共享,而每一個進程都存在一個獨立的文件描述符表。父子進程虛擬存儲空間的內容是大體相同的,父子進程是經過同一個物理區域存儲文件描述符表,但若是修改文件描述符表,也會發生寫時拷貝操做,只有這樣才能保證子進程中對文件描述符的修改,不會影響到父進程的文件描述符表。例如close操做,由於close會致使文件的描述符的值發生變化,至關於發生了寫操做,這是產生了寫時拷貝過程,實現新的物理空間,而後再次發生close操做,這樣就不會產生子進程中文件描述符的關閉而致使父進程不能訪問文件。

測試函數:

 

[objc]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. #include<stdio.h>  
  2. #include<stdlib.h>  
  3. #include<unistd.h>  
  4. #include<sys/types.h>  
  5. #include<sys/stat.h>  
  6. #include<fcntl.h>  
  7. #include<sys/wait.h>  
  8.   
  9. int main(){  
  10.         int fd;  
  11.         char c[3];  
  12.         charchar *s = "TestFs";  
  13.   
  14.         fd = open("foobar.txt",O_RDWR,0);  
  15.   
  16.         if(fork()==0)   //子進程  
  17.         {  
  18.                 fd = 1;//stdout  
  19.                 write(fd,s,7);  
  20.                 exit(0);  
  21.         }  
  22.        //父進程  
  23.         read(fd,c,2);  
  24.         c[2]='\0';  
  25.         printf("c = %s\n",c);  
  26.         exit(0);  
  27. }  

 

 

編譯運行:

Shell代碼
[objc]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. $ gcc -g fileshare2.c -o fileshare2  
  2. $ ./fileshare2  
  3. c = fo    ----foobar.txt中的內容  
  4. $ TestFs   ---標準輸出   

  緣由分析:因爲父子進程的文件描述符表是相同的,可是在子進程中對fd(文件描述符表中的項)進行了修改,這時會發生寫時拷貝過程,內核在物理內存中分配一個新的頁面存儲子進程原文件描述符fd存在頁面的內容,而後再進修寫操做,實現將fd修改成1,也就是標準輸出。可是父進程的fd並無發生改變,仍是與其餘的子進程共享文件描述符表,所以仍然是對文件foobar.txt進行操做。       所以須要注意fork()函數實質上是按着寫時拷貝的方式實現文件的映射,並非共享,寫時拷貝操做使得內存的需求量大大的減小了,具體的寫時拷貝實現,請參看很是經典的「深刻理解計算機系統」的第622頁。

相關文章
相關標籤/搜索