http://www.boluor.com/summary-of-fork-in-linux.html
fork函數在linux中非常重要,因為進程大多是通過它來建立的,比如linux系統在啟動時首先建立了進程0,之後的很多進程藉助do_fork得到建立.這兩天在看匿名管道時瞭解了下fork,其應用畢竟廣,這裡只說些我才學到的吧.
首先來看例1.
#include "stdio.h"
#include "unistd.h"
#include "stdlib.h"
int main(){
int i;
printf("hello world %d\n",getpid());
i=3;
fork();
printf("var %d in %d\n",i,getpid());
return 0;
}
輸出是什麼呢?
這是在我的機器上一次執行的結果:
hello world 8168
var 3 in 8169
var 3 in 8168
為什麼會有兩次輸出var 3 一行呢?看似不可思議吧…要解釋原因,就牽涉到了我們要討論的fork,它到底做了什麼?
fork英文是叉的意思.在這裡的意思是進程從這裡開始分叉,分成了兩個進程,一個是父進程,一個子進程.子進程拷貝了父進程的絕大部分.棧阿,緩衝區阿等等.系統為子進程建立一個新的進程表項,其中進程id與父進程是不相同的,這也就是說父子進程是兩個獨立的進程,雖然父子進程共用代碼空間.但是在牽涉到寫資料時子進程有自己的資料空間,這是因為copy on write機制,在有資料修改時,系統會為子進程申請新的頁面.
再來複習下進程的有關知識.系統通過進程式控制制塊PCB來管理進程.進程的執行,可以看作是在它的上下文中執行.一個進程的上下文(context)由三部分組成:使用者級上下文,寄存器上下文和系統級上下文.使用者級上下文中有本文,資料,使用者棧和共用儲存區;寄存器上下文中有個非常重要的程式計數器(傳說中的)PC,還有棧指標和通用寄存器等;系統級上下文分靜態和動態,PCB中進程表項,U區,還有本進程的表項,頁表,系統區表項等都屬於靜態部分,而核心棧等則屬於動態部分.
回到fork上來.fork在核心中對應的是do_fork函數,本來想自己寫下函數說明的,發現已經有了.詳見:核心 do_fork 函數原始碼淺析 . 上面已經提到,fork後,子進程拷貝了父進程的進程表項,還有棧阿,緩衝區,U區等等.當然在這之前會去檢查系統有沒有可用的資源,取一個閒置進程表項和唯一的PID號等工作.(後面的例子會體現子進程到底拷貝了父進程的哪些東西.)需要指出的是,這裡所說的拷貝,並不是說子進程再申請頁面,將父進程中的全部拷貝過來.而是,他們共用一個空間,子進程只是作一層映射而已,這個時候進程頁面標記為唯讀.在有資料修改時,才會申請新的頁面,拷貝過來,並標記為可寫.
fork執行後,對父進程和子進程不同的地方還有,對父進程返回子進程的pid號,對子進程返回的是0.大致的演算法描述為:
if (當前正在執行的是父進程){
將子進程的狀態設定為”就緒狀態”;
return (子進程的pid號);
}else{ /*正在執行的是子進程*/
初始化U區等工作;
return 0;
}
現在來看例1,是不是已經清晰了很多? 在執行了fork之後,父子進程分別都執行了下一步printf語句.由於fork拷貝走了pc,所以在子進程中不會再從main入口重新執行,而是執行fork後的下一條指令.而i是儲存在進程棧空間中的,所以子進程中也存在.
有了前面的基礎,再看下面一個例2:
#include <stdio.h>
#include <unistd.h>
int main()
{
int i=0;
pid_t fork_result;
printf("pid : %d --> main begin()\n",getpid());
fork_result = fork();
if(fork_result < 0) {
printf("Fork Failure\n");
return 0;
}
for(i=0;i<3;i++){
if(fork_result == 0){ //在子進程中.
printf("child process : %d\n",i);
}else{
printf("Father process : %d\n",i);
}
}
return 0;
}
這次輸出可以更明確的顯示出子進程到底拷貝了些什麼.我機器上的兩次執行結果:
boluor@boluor-laptop:~/programs/pipe/fork$./a.out
pid : 16567 –> mainbegin()
child process : 0
child process : 1
child process : 2
Father process : 0
Father process : 1
Father process : 2
boluor@boluor-laptop:~/programs/pipe/fork$ ./a.out
pid : 16569 –> main begin()
Father process : 0
Father process : 1
Father process : 2
child process : 0
child process : 1
child process : 2
同時也可以說明,父子進程到底哪個先執行,是跟cpu調度有關係的.如果想固定順序,那麼就要用wait或vfork函數.
繼續看例3:
#include "stdio.h"
#include "unistd.h"
#include "stdlib.h"
int main()
{
printf("hello world %d",getpid());
//fflush(0);
fork();
return 0;
}
執行上面的程式,可以發現輸出了兩遍hello world.而且兩次的pid號都是一樣的.這是為什麼呢? 這其實是因為printf的行緩衝的問題,printf語句執行後,系統將字串放在了緩衝區內,並沒有輸出到stdout.不明白的話看下面的例子:
#include "stdio.h"
int main(){
printf("hello world");
while(1);
return 0;
}
執行上面的程式你會發現,程式陷入死迴圈,並沒有輸出”hello world”.這就是因為把”hello world”放入了緩衝區.我們平常加’\n’的話,就會重新整理緩衝區,那樣就會直接輸出到stdout了.
因為子進程將這些緩衝也拷貝走了,所以子進程也列印了一遍.父進程直到最後才輸出.他們的輸出是一樣的,輸出的pid是一致的,因為子進程拷貝走的是printf語句執行後的結果.如果利用setbuf設定下,或者在printf語句後調用fflush(0);強制重新整理緩衝區,就不會有這個問題了.這個例子從側面顯示出子進程也拷貝了父進程的緩衝區.
關於fork的應用還很多很多,在實際項目中需要了再去深入研究.關於fork和exec的區別,exec是將本進程的映像給替換掉了,跟fork差別還是很大的,其實fork建立子進程後,大部分情況下,子進程會調用exec去執行不同的程式的.
先說到這裡了.如果需要更多fork的知識就google一下^.^.