fork函式中的記憶體複製和共享

黑翼天使23發表於2014-12-29

原來剛剛開始做Linux下面的多程式程式設計的時候,對於下面這段程式碼感到很奇怪:

#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<stdarg.h>
#include<errno.h>
#define LEN 2
void err_exit(char *fmt,...);
int main(int argc,char *argv[])
{
    pid_t pid;
    int loop; 

    for(loop=0;loop<LEN;loop++)
    {
    if((pid=fork()) < 0)
        err_exit("[fork:%d]: ",loop);
    else if(pid == 0)
    {
       printf("Child process\n"); 
    }
    else
    {
        sleep(5);
    }
    }

    return 0;
}

為什麼這段程式會建立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的記憶體複製機制,關於這個,我們只需要瞭解一句話就夠了,“子程式複製父程式的資料空間(資料段)、棧和堆,父、子程式共享正文段。”也就是說,對於程式中的資料,子程式要複製一份,但是對於指令,子程式並不複製而是和父程式共享。具體來看下面這段程式碼(這是我在上面那段程式碼上稍微新增了一點東西):

/*  這個程式會建立3個子程式,理解這句話,父子程式複製資料段、棧、堆,共享正文段
 *
 */
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<stdarg.h>
#include<errno.h>
#define BUFSIZE 512
#define LEN 2
void err_exit(char *fmt,...);
int main(int argc,char *argv[])
{
    pid_t pid;
    int loop; 

    for(loop=0;loop<LEN;loop++)
    {
    printf("Now is No.%d loop:\n",loop);

    if((pid=fork()) < 0)
        err_exit("[fork:%d]: ",loop);
    else if(pid == 0)
    {
       printf("[Child process]P:%d C:%d\n",getpid(),getppid()); 
    }
    else
    {
        sleep(5);
    }
    }

    return 0;
}

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

首先父程式執行迴圈,通過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了,所以最後一共就是建立了兩個子程式啊!

相關文章