在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出來的父子程序,共享一份代碼,但各自有一份資料,代碼和資料是寫時拷貝的。
每一個程序都有各自的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的值并輸出它,那麼我們來看一下輸出結果
子程序輸出的是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 ;
}
我們先來看下結果再來說明問題
具體地怎麼執行怎麼輸出的如下圖
再通過一段代碼來簡單地看一下上段代碼中有’\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 ;
}
讓父程序輸出#,讓子程序輸出@,結果如下
這個是沒有’\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 ;
}
這就隻有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的值,看到如下結果
父程序中的glob的值也變成了子程序中修改的值,這是因為父子程序是共享一塊位址空間的,這與fork顯然是不同的。