天天看點

淺談程序的建立函數fork及vfork

在Linux中,當我們需要建立一個程序的時候,常常會用到fork()函數,以及它的姐妹函數vfork(),下面我們就來談一下這兩個函數分别是在幹什麼。

fork函數

fork函數是最常用的程序建立的函數,它從一個已知的程序中建立一個新的程序,新程序即為子程序,原來的程序即為父程序。函數基本的用法如下

pid_t pid=fork();
if(pid < )
{
    //程序建立失敗
    perror("fork");
    return -;
}
else if(pid == )
{
    //pid==0為子程序
    printf("child");
}
else
{
    //pid>0為父程序,此時的pid表示的是從傳回到父程序的子程序的pid
    printf("parent");
}       
           

對于fork來說,關于傳回值的問題上面的代碼已經說明清楚了。需要注意的是如果fork調用失敗,可能是有兩種原因

  • 記憶體不夠多了
  • 系統中的程序數量已經達到上限

接下來我們來看一下它在記憶體中是如何表示的。首先需要說明一下的是,fork出來的父子程序,共享一份代碼,但各自有一份資料,代碼和資料是寫時拷貝的。

淺談程式的建立函數fork及vfork

每一個程序都有各自的PCB(在Linux下即為task_struct),每一個PCB中都有一個指向頁表的指針,頁表再對應映射在實體位址上。關于fork出來的程序,父子程序雖然代碼一樣,但是他們各自的頁表對應的是不同的實體位址,是以對于資料而言各有一份,并且這些資料和代碼是寫時拷貝的。

什麼是寫時拷貝

也就是說,fork出來的子程序,父程序希望将代碼和資料原封不動的拷貝過去,這可能是一個很耗時的事情,如果子程序隻執行了一件事,比如說進入子程序立即調用exec函數進行程式替換,那麼之前拷貝的所有代碼和資料都是白費工夫。是以引入了寫時拷貝,即在子程序建立的時候,并不立即拷貝父程序中所有的内容,而是在要用的時候才回去拷貝,這樣就大大提高了效率。

怎麼了解資料是各有一份的

先來看看下面的代碼

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>

int glob=;

int main()
{
    pid_t pid=fork();
    if(pid > )//parent
    {
        printf("parent glob %d\n",glob);
    }
    else if(pid == )//child
    {
        glob=;
        printf("child glob %d\n",glob);
    }
    else//調用失敗
    {
        perror("fork");
        exit();
    }
    return ;
}
           

這裡我們有一個全局變量glob,在父程序中我們直接輸出它,在子程序我們變動了glob的值并輸出它,那麼我們來看一下輸出結果

淺談程式的建立函數fork及vfork

子程序輸出的是200,父程序輸出的100,那麼既然是全局變量,為何兩者輸出的值會是不同的呢?那是因為他們的資料是各自有一份的,子程序修改了自己的資料,并不影響父程序的資料的值,因為他們對應的是不同的實體位址空間。

關于父子程序誰先執行的問題

父子程序誰先執行取決于作業系統排程器,每個人的系統不一樣可能就不一樣,這并不是固定的。

子程序繼承了父程序的PC指針,從fork的地方繼續執行

關于這個問題,我們可以來看下面的代碼

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    int i;
    for(i=;i<;++i)
    {
        int pid=fork();
        if(pid>)
        {
            printf("father=%d,childpid=%d||",getpid(),pid);
        }
        else if(pid==)
        {
            printf("child=%d||",getpid());
        }
        else
        {
            perror("fork");
        }
    }
    printf("END\n");
    return ;
}
           

我們先來看下結果再來說明問題

淺談程式的建立函數fork及vfork

具體地怎麼執行怎麼輸出的如下圖

淺談程式的建立函數fork及vfork

再通過一段代碼來簡單地看一下上段代碼中有’\n’和沒有’\n\的差別。

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    int i;
    for(i=;i<;++i)
    {
        int pid=fork();
        if(pid>)
        {
            printf("#");
        }
        else if(pid==)
        {
            printf("@");
        }
        else
        {
            perror("fork");
        }
    }
    return ;
}
           

讓父程序輸出#,讓子程序輸出@,結果如下

淺談程式的建立函數fork及vfork

這個是沒有’\n’的測試代碼和結果,總共輸出了4個#,4個@。下面來看下有’\n’的代碼和結果

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    int i;
    for(i=;i<;++i)
    {
        int pid=fork();
        if(pid>)
        {
            printf("#\n");
        }
        else if(pid==)
        {
            printf("@\n");
        }
        else
        {
            perror("fork");
        }
    }
    return ;
}
           
淺談程式的建立函數fork及vfork

這就隻有3個#,3個@了,具體的原因之前的那張分析圖中都有講解到。

vfork函數

關于vfork函數,實際上用的不多。它相對于fork函數主要有幾點不同

  • vfork出的子程序一定必父程序先執行,在子程序被調用exec或exit之後父程序才有可能被執行
  • vfork出的子程序和父程序共享位址空間,而fork的子程序具有獨立的位址空間。

關于vfork,需要注意幾個地方

  • 子程序不應該用return傳回,否則會産生邏輯混亂的重複vfork
  • vfork誕生的原因是因為它沒有給子程序開辟新的位址空間,而是直接共享了父程序的,當然不是希望子程序做和父程序一樣的事,是以vfork出的子程序一般來說建立後立即執行exec函數進行程式替換,在子程序退出或開始新程序之前,核心保證父程序處于阻塞狀态。

    關于vfork共享位址空間和fork重新建立一塊位址空間,用之前的代碼來解釋一下,隻不過用的是 vfork

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>

int glob=;

int main()
{
    pid_t pid=vfork();
    if(pid > )//parent
    {
        printf("parent glob %d\n",glob);
    }
    else if(pid == )
    {
        glob=;
        printf("child glob %d\n",glob);
        exit();
    }
    else
    {
        perror("vfork");
        exit();
    }
    return ;
}
           

在子程序中修改全局變量glob的值,看到如下結果

淺談程式的建立函數fork及vfork

父程序中的glob的值也變成了子程序中修改的值,這是因為父子程序是共享一塊位址空間的,這與fork顯然是不同的。

繼續閱讀