淺析fork()和底層實現

記得之前初次接觸fork()函數的時候,一直被「printf」輸出多少次的問題弄得比較暈乎。不過,「黃天不負留心人"。哈~ 終於在學習進程和進程建立fork相關知識後,總算是大體摸清了其中的前因後果。廢話很少講,下面來談談本人的一點小小積累linux

 
 一個現有的進程能夠調用fork函數建立一個新進程。原型以下:
  1. #include<unistd.h>  
  2.   
  3. pid_t fork(void);  
  4.   
  5. 返回值:自進程中返回0,父進程返回進程id,出錯返回-1  

fork()系統調用會經過複製一個現有進程來建立一個全新的進程. 進程被存放在一個叫作任務隊列的雙向循環鏈表當中.鏈表當中的每一項都是類型爲 task_struct成爲進程描述符的結構.也就是咱們寫過的進程PCB
 
小知識:內核經過一個位置的進程標識值或PID來標識每個進程.同時其最大值默認爲32768,short int短整型的最大值. 他就是系統中容許同時存在的進程最大的數目.能夠去linux下的proc目錄中尋找一個 pid_max的文件,並打開它加以驗證. 如
 
 

 

fork()運行時作的事情


首先咱們來看一段代碼,不過這裏會有一點奇怪的現象:
 
  1 /*************************************************************************
  2   > File Name: 1.c
  3   > Author: tp
  4   > Mail: 
  5   > Created Time: Mon 07 May 2018 07:57:28 PM CST
  6  ************************************************************************/
  7 
  8 #include <stdio.h>
  9 #include <stdlib.h>
 10 #include <unistd.h>
 11 int main( void)
 12 {
 13     printf("change world!\n");
 14     pid_t pid = fork();
 15     if( pid == -1) {perror("fork"),exit(1); }
 16 
 17     printf( "pid=%d, returnVal=%d\n", getpid(), pid);
 18     sleep( 1);
 19     exit(0);
 20 }
~     

 


這段代碼的運行結果,你們若是像我當時不瞭解fork的時候,必定會覺得輸出結果是兩個"change world!",而後2個printf裏面的內容. 由於
 
咱們複製出來了兩個如出一轍的進程,那麼他們就應該作一樣的事情. But!!! 咱們看運行結果:
 
 
結果並不是咱們想的那樣,這個時候咱們就須要知道fork出子進程以後,程序的運行細節。能夠來一張圖幫助咱們理解:
 
 
 
 
通常來講,在fork以後是父進程先執行仍是子進程先執行是不肯定的.這取決於內核所使用的調度算法.若是要求父,子進程之間相互同步.則要求某種形式的進程間通訊. 好了咱們繼續,當進程調用fork後,當控制轉移到內核中的fork代碼後,內核會作4件事情:
 
  1.分配新的內存塊和內核數據結構給子進程
 
  2.將父進程部分數據結構內容(數據空間,堆棧等)拷貝至子進程
 
  3.添加子進程到系統進程列表當中
 
  4.fork返回,開始調度器調度
 
 
爲何fork成功調用後返回兩個值? 
 
因爲在複製時複製了父進程的堆棧段,因此兩個進程都停留在fork函數中,等待返回。因此fork函數會返回兩次,一次是在父進程中返回,另外一次是在子進程中返回,這兩次的返回值不一樣
其中父進程返回子進程pid,這是因爲一個進程能夠有多個子進程,可是卻沒有一個函數可讓一個進程來得到這些子進程id,那談何給別人你建立出來的進程。而子進程返回0,這是因爲子進程能夠調用getppid得到其父進程進程ID,但這個父進程ID卻不可能爲0,由於進程ID0老是有內核交換進程所用,故返回0就可表明正常返回了。
 
 
從fork函數開始之後的代碼父子共享,既父進程要執行這段代碼,子進程也要執行這段代碼.(子進程得到父進程數據空間,堆和棧的副本. 可是父子進程並不共享這些存儲空間部分. (即父,子進程共享代碼段.)。如今不少實現並不執行一個父進程數據段,堆和棧的徹底複製. 而是採用寫時拷貝技術(不懂能夠戳進去看一看).這些區域有父子進程共享,並且內核地他們的訪問權限改成只讀的.若是父子進程中任一個試圖修改這些區域,則內核值爲修改區域的那塊內存製做一個副本, 也就是若是你不修改咱們一塊兒用,你修改了以後對於修改的那部份內容咱們分開各用個的.
 
 
 
 

父子進程文件共享問題


 
來看個例子
  1 /*************************************************************************
  2   > File Name: 2.c
  3   > Author: tp
  4   > Mail: 
  5   > Created Time: Mon 07 May 2018 12:40:39 PM CST
  6  ************************************************************************/
  7 
  8 #include <stdio.h>
  9 #include <stdlib.h>
 10 #include <unistd.h>
 11 #include <fcntl.h>
 12 
 13 int set = 110;
 14 int main( void)
 15 {
 16     printf( "before fork\n");
 17     pid_t pid = fork( );
 18     if( pid < 0){ perror(" fork"),exit( 1);}
 19 
 20     if( pid == 0)
 21     {
 22         ++set;
 23         printf( "son pid=%d, %d\n", getpid(), set);
 24     }
 25     else
 26     {
 27         sleep( 1);
 28         printf( "parent pid=%d , %d\n", getpid( ), set);
 29     }
 30     exit( 0);
 31 }

看一下結果:算法

不難注意到  before fork」這句話只是被打印了一次,這個從上面的例子,這不難理解;與此同時子進程中的set的值被改變了。此時再進行一個重定向操做會發生什麼網絡

 

 

出現很神奇的現象! 這個時候打印了出了兩次「before fork」,不只僅是如此,上述針對父進程的標準輸出執行重定向操做還致使了子進程也執行重定向操做。數據結構

透過現象看本質,來細細分析一下。針對打印兩次「before fork」,首先,先要知道標準IO庫是是帶緩衝的,而像printf這種直接輸出到標準輸出時,這個緩衝區是由換行符刷新的;而當執行了重定向操做,這裏就是將標準輸出重定向到文件,文件就不會當即去刷新緩衝區(全緩衝的方式);好,因爲在fork以前調用了一次printf,但fork以後,該行數據仍存留在緩衝區中,而後父進程數據空間被複制到子進程中,該行數據去也被複制了過去,這樣父子進程都各自帶有該行內容的緩衝區了,至關於子進程緩衝區添加了一行「before fork」,而後在每一個進程exit以後,每一個緩衝區的內容就被寫到了相應的文件中。函數

  再一個就是,在重定向父進程的標準輸出時,子進程標準輸出也被重定向。這就源於父子進程會共享全部的打開文件。 由於fork的特性就是將父進程全部打開文件描述符複製到子進程中。當父進程的標準輸出被重定向,子進程本是寫到標準輸出的時候,此時天然也改寫到那個對應的地方;與此同時,在父進程等待子進程執行時,子進程被改寫到文件show.out中,而後又更新了與父進程共享的該文件的偏移量;那麼在子進程終止後,父進程也寫到show.out中,同時其輸出還會追加在子進程所寫數據以後,這也就解釋了上面爲何「before fork」會在一個文件中打印兩次。性能

 

 

在fork以後處理文件描述符通常又如下兩種狀況:學習

  1.父進程等待子進程完成。此種狀況,父進程無需對其描述符做任何處理。當子進程終止後,它曾進行過讀,寫操做的任一共享描述符的文件偏移已發生改變。spa

  2.父子進程各自執行不一樣的程序段。這樣fork以後,父進程和子進程各自關閉它們再也不使用的文件描述符,這樣就避免干擾對方使用的文件描述符了。這相似於網絡服務進程。.net

 

同時父子進程也是有區別的:它們不只僅是兩個返回值不一樣;它們各自的父進程也不一樣,父進程的父進程是ID不變的;還有子進程不繼承父進程設置的文件鎖,子進程未處理的信號集會設置爲空集等不一樣code

 

 

fork()函數在底層中作了什麼?


 
   linux平臺經過clone()系統調用實現fork(). fork(),vfork()和clone()庫函數都根據各自須要的參數標誌去調用clone(),而後由clone()去調用
do_fork(). 再而後do_fork()完成了建立中的大部分工做,他定義在kernel/fork.c當中.該函數調用copy_process(). 而後重點來了,咱們看看這個
copy_process函數到底作了那些事情?? 我畫一張圖幫咱們理解:
 
 

 

 

vfork和fork的之間的比較:


vfork()的誕生是在fork()尚未寫時拷貝的時候,由於那個時候建立一個子進程的成本太大了,若是一會兒建立好多了那麼程序的效率必定會降低. 而後就有人提出了vfork(). vfork的實現原理很是簡單,就是子進程,父進程徹底公用一個資源. 就是是有人修改了內容,甚至main()函數退出了也不會新開闢一個空間. 因此這裏裏會有問題的,若是你的一個子進程沒有使用exit()退出,那麼程序就會出現段錯誤. 不相信能夠去試一試~ 
 
爲何會出現段錯誤? 
 
  在函數棧上面,子進程運行結束了,main的函數棧被子進程釋放了,而後父進程在使用的時候,就訪問不到了,一旦vfork出子進程,退出的時候須要使用exit來結束.
 
 

vfork和fork之間的區別:

 
1.fork父子進程交替運行,vfork保證子進程先運行,父進程阻塞,直到子進程結束(或子進程調用了exec或exit).
 
2.fork實現了寫時拷貝. 而vfork直接讓父子進程共用公用資源,避免多開闢空間拷貝,
 
3,vfork必須使用exit或者excl退出.
 
4.就算是fork使用了寫時拷貝,也沒有vfork性能高.
 
5.每一個系統上的vfork都有問題,推薦不要使用.
相關文章
相關標籤/搜索