關於fork函數中的記憶體複製和共用,fork函數記憶體共用

來源:互聯網
上載者:User

關於fork函數中的記憶體複製和共用,fork函數記憶體共用

  原來剛剛開始做linux下面的多進程編程的時候,對於下面這段代碼感到很奇怪,

 1 #include<unistd.h> 2 #include<stdio.h> 3 #include<string.h> 4 #include<stdlib.h> 5 #include<stdarg.h> 6 #include<errno.h> 7 #define LEN 2 8 void err_exit(char *fmt,...); 9 int main(int argc,char *argv[])10 {11     pid_t pid;12     int loop; 13 14     for(loop=0;loop<LEN;loop++)15     {16     if((pid=fork()) < 0)17         err_exit("[fork:%d]: ",loop);18     else if(pid == 0)19     {20        printf("Child process\n"); 21     }22     else23     {24         sleep(5);25     }26     }27 28     return 0;29 }

    為什麼這段程式會建立3個子進程,而不是兩個,為什麼在第20行後面加上一個return 0;就建立的又是兩個子進程了?原來一直搞不明白,後來瞭解了C語言程式的儲存空間布局以及在fork之後父子進程是共用本文段(程式碼片段CS)之後才明白這其中的緣由!具體原理是啥,且容我慢慢道來!

 

    首先得明白一個東西就是C程式的儲存空間布局,如所示:

  (原圖出自《UNIX環境進階編程》7.6節)

    當一個C程式執行之後,它會被載入到記憶體之中,它在記憶體中的布局如,分為這麼幾個部分,環境變數和命令列參數、棧、堆、資料區段(初始化和未初始化的)、本文段,下面挨個來說明這幾段分別代表了什麼:

    環境變數和命令列參數:這些指的就是Unix系統上的環境變數(比如$PATH)和傳給main函數的參數(argv指標所指向的內容)。

    資料區段:這個是指在C程式中定義的全域變數,如果沒有初始化,那麼就存放在未初始化的資料區段中,程式運行時統一由exec賦值為0。否則就存放在初始化的資料區段中,程式運行時由exec統一從程式檔案中讀取。(瞭解彙編的朋友們想必知道組合語言中的資料區段DS,這和彙編中的資料區段其實是一個東西)。

    堆:這一部分主要用來動態分配空間。比如在C語言中用malloc申請的空間就是在這個地區申請的。

    本文段:C語言代碼並不是直接執行的,而是被編譯成了機器指令才能夠在電腦上執行,最終產生的機器指令就是存放在這個地區(彙編中的程式碼片段CS指的就是這片地區)。

    棧:個人感覺這是C程式記憶體布局最關鍵的部分了。這個部分主要用來做函數調用。具體而言怎麼說呢,程式剛開始棧中只有main這一個函數的內容(即main的棧幀),如果main函數要調用func函數,那麼func函數的返回地址(main函數的地址),func函數的參數,func函數中定義的局部變數,還有func函數的傳回值等等這些都會被壓入棧中,這時棧中就多了func函數的內容(func的棧幀)。然後func函數運行完了之後再來彈棧,把它原來壓的內容去掉(即清除掉func棧幀),此時棧中又只剩下了main的棧幀。(這片地區就是彙編中的棧段SS)

    OK,這就是C程式的儲存空間布局。這裡我聯想到另外一點,就是全域變數和靜態變數是儲存在資料區段中的,而局部變數是儲存在棧中的,棧中資料在函數調用完之後一彈棧就沒了,這就是為什麼全域變數的生存周期比局部變數的生存周期要長的原因。

 

    瞭解了C程式在儲存空間的布局之後,我們再來瞭解fork的記憶體複製機制,關於這個,我們只需要瞭解一句話就夠了,“子進程複製父進程的資料空間(資料區段)、棧和堆,父、子進程共用本文段。”也就是說,對於程式中的資料,子進程要複製一份,但是對於指令,子進程並不複製而是和父進程共用。具體來看下面這段代碼(這是我在上面那段代碼上稍微添加了一點東西):

 1 /*  這個程式會建立3個子進程,理解這句話,父子進程複製資料區段、棧、堆,共用本文段 2  * 3  */ 4 #include<unistd.h> 5 #include<stdio.h> 6 #include<string.h> 7 #include<stdlib.h> 8 #include<stdarg.h> 9 #include<errno.h>10 #define BUFSIZE 51211 #define LEN 212 void err_exit(char *fmt,...);13 int main(int argc,char *argv[])14 {15     pid_t pid;16     int loop; 17 18     for(loop=0;loop<LEN;loop++)19     {20     printf("Now is No.%d loop:\n",loop);21 22     if((pid=fork()) < 0)23         err_exit("[fork:%d]: ",loop);24     else if(pid == 0)25     {26        printf("[Child process]P:%d C:%d\n",getpid(),getppid()); 27     }28     else29     {30         sleep(5);31     }32     }33 34     return 0;35 }

    為什麼上面那段代碼會建立三個子進程?我們來具體分析一下它的執行過程:

    首先父進程執行迴圈,通過fork建立一個子進程,然後sleep5秒。

    再來看父進程建立的這個子進程,這裡我們記為子進程1.子進程1完全複製了這個父進程的資料部分,但是需要注意的是它的本文段是和父進程共用的。也就是說,子進程1開始執行代碼的部分並不是從main的 { 開始執行的,而是主函數執行到哪裡了,它就接著執行,具體而言就是它會執行fork後面的代碼。所以子進程1首先會列印出它的ID和它的父進程的ID。然後繼續第二遍迴圈,然後這個子進程1再來建立一個子進程,我們記為子進程11,子進程1開始sleep。

    子進程11接著子進程1執行的代碼開始執行(即fork後面),它也是列印出它的ID和父進程ID(子進程1),然後此時loop的值再加1就等於2了,所以子進程2直接就返回了。

    那個子進程1sleep完了之後也是loop的值加1之後變成了2,所以子進程1也返回了!

    然後我們再返回去看父進程,它僅僅迴圈了一次,sleep完之後再來進行第二次迴圈,這次又建立了一個子進程我們記為子進程2。然後父進程開始sleep,sleep完了之後也結束了。

    那麼那個子進程2怎麼樣了呢?它從fork後開始執行,此時loop等於1,它列印完它的ID和父進程ID之後,就結束迴圈了,整個子進程2就直接結束了!

    這就是上面那段代碼的運行流程,進程間的關係如所示:

    

    中那個loop=%d就是當這個進程開始執行的時候loop的值。上面那段代碼的運行結果如:

    

    這裡這個3498進程就是我們的主進程,3499就是子進程1,3500就是子進程11,3501就是子進程2。

 

    最後,我們再來回答一下我們開始的時候提出的那個問題,為什麼在子進程的處理部分“ if(pid == 0) ”最後加一個return 0,就會建立兩個子進程了,就是因為子進程1運行到這裡直接就結束了,不再進行第二遍迴圈了,所以就不會再去建立那個子進程11了,所以最後一共就是建立了兩個子進程啊!

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.