一)fork的概述
.操作系統(tǒng)對進程的管理,是通過進程表完成的.進程表中的每一個表項,記錄的是當前操作系統(tǒng)中一個進程的信息.
.進程在系統(tǒng)的唯一標識是PID,PID是一個從1到32768的正整數(shù),其中1一般是特殊進程init,其它進程從2開始依次編號.當用完32768后,從2重新開始.
.一個稱為“程序計數(shù)器(program counter, pc)”的寄存器,指出當前占用 CPU的進程要執(zhí)行的下一條指令的位置
.當分給某個進程的 CPU時間已經(jīng)用完,操作系統(tǒng)將該進程相關(guān)的寄存器的值,保存到該進程在進程表中對應的表項里面,把將要接替這個進程占用 CPU的那個進程的上下文,從進程表中讀出,并更新相應的寄存器.
二)fork的一個例子:
#include <sys/types.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
pid_t pid;
pid=fork();
if(pid<0)
printf("error in fork!");
else if(pid==0)
printf("I am the child process,ID is %d\n",getpid());
else
printf("I am the parent process,ID is %d\n",getpid());
}
gcc test1.c -o test1
debian:/tmp# ./test1
I am the child process,ID is 2723
I am the parent process,ID is 2722
程序分析:
1)pid=fork();
先來看看子進程的表現(xiàn):
操作系統(tǒng)調(diào)用fork()函數(shù)創(chuàng)建一個新的進程(子進程),并且在進程表中相應為它建立一個新的表項,
此時子進程得到CPU的調(diào)度,它的上下文被換入,占據(jù) CPU,操作系統(tǒng)對fork的實現(xiàn),使得子進程中fork調(diào)用返回0
所以在這個進程中pid=0,這個進程繼續(xù)執(zhí)行的過程中,if語句中 pid<0不滿足,但是pid= =0是true。所以輸出i am the child process...
父進程的表現(xiàn):
操作系統(tǒng)對fork的實現(xiàn),使這個調(diào)用在父進程中返回剛剛創(chuàng)建的子進程的pid(一個正整數(shù)),所以下面的if語句中pid<0,
pid==0的兩個分支都不會執(zhí)行。所以輸出i am the parent process...
2)對子進程來說,fork返回給它0,但它的pid絕對不會是0,之所以fork返回0給它,是因為它隨時可以調(diào)用getpid()來獲取自己的pid
3)fork之后父子進程除非采用了同步手段,否則不能確定誰先運行,也不能確定誰先結(jié)束.認為子進程結(jié)束后父進程才從fork返回的,這是不對的,fork不是這樣的,vfork才這樣。
4)父進程執(zhí)行了所有的進程,而子進程只執(zhí)行了fork()后面的程序,這是因為子進程繼承了父進程的PC(程序計數(shù)器).
三)fork的另一個例子:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid1;
pid_t pid2;
pid1 = fork();
pid2 = fork();
printf("pid1:%d, pid2:%d\n", pid1, pid2);
}
gcc test2.c -o test2
./test2
pid1:18938, pid2:0
pid1:0, pid2:0
pid1:18938, pid2:18939
pid1:0, pid2:18940
程序分析:
1)執(zhí)行test2時,啟動一個進程,設這個進程為P0,PID為xxxxx
2)當執(zhí)行到pid1 = fork();時,P0啟動了一個進程,設這個進程為P1,它的PID為18938,暫且不管P1.
3)P0中的fork返回18938給pid1,繼續(xù)執(zhí)行到pid2 = fork();此時啟動另一個新的進程,設為P2,P2的PID為18939 ,同樣暫且不管P2.
4)P0的第二個fork返回18939給p2,最后P0的執(zhí)行結(jié)果為pid1:18938, pid2:18939
5)再看P2,P2生成時,P0中的pid1=18938,所以P2中的pid1繼承P0的pid1=18938,而作為子進程pid2=0,P2從第二個fork后開始執(zhí)行,
最后輸出pid1:18938, pid2:0.
6)回頭看P1,P1中第一條fork返回0給pid1,然后接著執(zhí)行后面的語句.而后面接著的語句是pid2 = fork();執(zhí)行到這里,P1又產(chǎn)生了一個新進程,設為P3,先不管P3.
7)P1中第二條fork將P3的PID返回給pid2,P3的PID為18940,所以P1的pid2=18940。P1繼續(xù)執(zhí)行后續(xù)程序,結(jié)束,輸出“pid1:0, pid2:18940”.
8)P3作為P1的子進程,繼承P1中pid1=0,并且第二條fork將0返回給pid2,所以P3最后輸出“pid1:0, pid2:0”.
9)所有的進程都執(zhí)行完畢.
四)vfork與fork的區(qū)別
vfork與fork主要有三點區(qū)別:
.fork():子進程拷貝父進程的數(shù)據(jù)段,堆棧段
vfork():子進程與父進程共享數(shù)據(jù)段
.fork()父子進程的執(zhí)行次序不確定vfork 保證子進程先運行,在調(diào)用 exec 或 exit 之前與父進程數(shù)據(jù)是共享的,在它調(diào)用 exec或 exit 之后父進程才可能被調(diào)度運行。
.vfork()保證子進程先運行,在它調(diào)用 exec 或 exit 之后父進程才可能被調(diào)度運行.如果在調(diào)用這兩個函數(shù)之前子進程依賴于父進程的進一步動作,則會導致死鎖。
1)先用fork()進行試驗
#include <unistd.h>
#include <stdio.h>
int main(void)
{
pid_t pid;
int count=0;
pid=fork();
count++;
printf("count= %d\n",count);
return 0;
}
分析:
通過上面fork()的說明,這個程序的輸出應該是:
./test
count= 1
count= 1
2)而將fork()換成vfork()呢,程序如下
#include <unistd.h>
#include <stdio.h>
int main(void)
{
pid_t pid;
int count=0;
pid=vfork();
count++;
printf("count= %d\n",count);
return 0;
}
執(zhí)行結(jié)果:
./test
count= 1
count= 1
Segmentation fault (core dumped)
分析:
通過將fork()換成vfork(),由于vfork()是共享數(shù)據(jù)段,為什么結(jié)果不是2呢,答案是:
vfork保證子進程先運行,在它調(diào)用 exec 或 exit 之后父進程才可能被調(diào)度運行.如果在調(diào)用這兩個函數(shù)之前子進程依賴于父進程的進一步動作,則會導致死鎖.
3)做最后的修改,在子進程執(zhí)行時,調(diào)用_exit(),程序如下:
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
int main(void)
{
pid_t pid;
int count=0;
pid=vfork();
if(pid==0)
{
count++;
_exit(0);
}
else
{
count++;
}
printf("count= %d\n",count);
return 0;
}
執(zhí)行結(jié)果:
./test
count= 2
分析:如果子進程中如果沒有調(diào)用_exit(0),則父進程不可能被執(zhí)行,在子進程調(diào)用exec(),exit()之后父進程才可能被調(diào)用.
所以加上_exit(0),使子進程退出,父進程執(zhí)行.
這樣 else 后的語句就會被父進程執(zhí)行,又因在子進程調(diào)用 exec 或 exit 之前與父進程數(shù)據(jù)是共享的,
所以子進程退出后把父進程的數(shù)據(jù)段 count 改成1了,子進程退出后,父進程又執(zhí)行,最終就將count 變成了 2.
五)寫拷貝技術(shù)
寫拷貝或叫做寫時拷貝,就是子進程在創(chuàng)建后共享父進程的虛存內(nèi)存空間,只是在兩個進程中某一個進程需要向虛擬內(nèi)存寫入數(shù)據(jù)時才拷貝相應部分的虛擬內(nèi)存.
寫拷貝的目的是通過消除不必要的復制來提高效率,當運行一個fork進程時,兩個進程將盡可能長地共享相同的物理內(nèi)存,也就是說內(nèi)核只復制頁表入口地址和標記所有寫拷貝的頁面.
當有一個進程修改內(nèi)存時,將會引起缺頁,這時內(nèi)核將分配一個新的物理存儲頁,并在它被修改之前復制該頁.
這樣對像init,xinetd,sshd這樣的進程將非常有用,因為他們的工作也只是調(diào)用fork和exec.
六)clone
.clone函數(shù)是Linux所特有的,可以用于創(chuàng)建進程和線程,所有可移植代碼從來不使用clone系統(tǒng)調(diào)用.
.clone是一個復雜的系統(tǒng)調(diào)用,它給予應用程序很大的權(quán)限,可以控制父進程共享哪些子進程,它可以將一個線程當作一個特定進程,與其父進程共享用戶空間.
七)最后的總結(jié):
1)fork()系統(tǒng)調(diào)用是創(chuàng)建一個新進程的首選方式,fork的返回值要么是0,要么是非0,父進程與子進程的根本區(qū)別在于fork函數(shù)的返回值.
2)vfork()系統(tǒng)調(diào)用除了能保證用戶空間內(nèi)存不會被復制之外,它與fork幾乎是完全相同的.vfork存在的問題是它要求子進程立即調(diào)用exec,而不用修改任何內(nèi)存,這在真正實現(xiàn)的時候要困難的多,尤其是考慮到exec調(diào)用有可能失敗.
3)vfork()的出現(xiàn)是為了解決當初fork()浪費用戶空間內(nèi)存的問題,因為在fork()后,很有可能去執(zhí)行exec(),vfork()的思想就是取消這種復制.
4)現(xiàn)在的所有unix變量都使用一種寫拷貝的技術(shù)(copy on write),它使得一個普通的fork調(diào)用非常類似于vfork.因此vfork變得沒有必要.
|