1.fork
fork:創建一個和當前進程映像一樣的進程就可以通過fork()系統調用。其定義如下:
#include<sys/types.h>
#include<unistd.h>
pid_t fork(void);
成功調用fork()會創建一個新的進程,它幾乎與調用fork()的進程一模一樣,這兩個進程都會繼續進行。
劃重點!!!
fork函數每調用一次都返回兩次
在子進程中,成功的fork()調用會返回0。
在父進程中fork()返回子進程的pid。
如果出現錯誤,fork()返回一個負值。
在fork函數執行完畢後,如果創建新進程成功,我們可以通過fork返回的值來判斷當前進程是子進程還是父進程。在父進程中,fork返回新創建子進程的進程ID;在子進程中,fork函數返回0。( pid的值爲什麼在父子進程中不同。“其實就相當於鏈表,進程形成了鏈表,父進程的pid(p 意味point)指向子進程的進程id, 因爲子進程沒有子進程,所以其pid爲0。)
fork函數複製當前進程,在內核進程表中創建一個新的進程表項。新的進程表項有很多屬性和原進程相同,比如堆指針、棧指針和標誌寄存器的值。但也有許多屬性被賦予了新的值,比如該進程的ppid被設置成原進程的pid,信號位圖被清除(原進程設置的信號處理函數不再對新進程起作用)。
子進程的代碼與父進程完全相同,同時它還會複製父進程的數據(堆數據、棧數據和靜態數據)。數據的複製採用的是寫實複製(copy on writte),即只有在任一進程(父進程或子進程)對數據執行了寫操作,複製纔會發生(先是缺頁中斷,然後操作系統給子進程分配內存並複製父進程的數據)。即便如此,如果我們在程序中分配了大量內存,那麼使用fork時也應當十分謹慎,儘量避免沒必要的內存分配和數據複製。
此外,創建子進程後,父進程中打開的文件描述符默認在子進程中也是打開的,且文件描述符的引用計數加1.不僅如此,父進程的用戶根目錄、當前工作目錄等變量的引用計數均會加1。
有事我們需要在子進程中執行其他程序,即替換當前進程映像,這就需要使用exec系列函數。
下面舉例看看fork的應用吧
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
1.
int main()
{
int i = 0;
for( ; i<2; i++)
{
fork();
printf("A\n");
}
}
分析屏幕上會打印出幾個A?
運行結果如下:
打印出了6個A,爲什麼呢?
分析結果:
2.
int main()
{
int i = 0;
for( ; i<2; i++)
{
fork();
printf("A");
}
}
這段代碼會打印出幾個A呢?
結果是8個 ,爲什麼呢
分析結果
根據上面的兩道題,思考打印的結果爲什麼不一樣呢?
這是因爲printf打印數據也是需要條件的:1.緩衝區放滿後,會自帶打印出;2.強制刷出緩衝區中的數據,可以使用"\n",fflush(stdout),3.程序結束會將緩衝區中的數據刷出。而我們在fork時也會將該進程緩衝區中的數據拷貝過來,所以導致了結果的不同。
下面還有兩道題可以繼續練習一下
3.
int main()
{
printf("A");
write(1,"B",1);
fork();
}
直接打印出BAA
4.
int main()
{
fork()||fork();
printf("A\n");
}
2.vfork
在實現寫時複製之前,設計者們就一直很關注在fork後立刻執行exec所造成的地址空間的浪費。BSD的開發者們在3.0的BSD系統中引入了vfork()系統調用。
pid_t vfork(void)
除了子進程必須要立刻執行一次對exec的系統調用,或者調用_exec()退出,對vfork()的成功調用所昌盛的結果和fork()是一樣的。vfork()會掛起父進程知道子進程終止或者停止運行了一個新的可執行文件的映像。通過這樣的方式,vfork()避免了地址空間的按頁複製。在這個過程中,父進程和子進程共享相同的地址空間和頁表項。實際上vfork()只完成了一件事:複製內部的內核數據結構。因此,子進程也就不能修改地址空間中的任何內存。
vfork()是一個歷史遺留產物,Linux本不應該去實現它。需要注意的是,即使增加了寫時複製,vfork也要比fork快,因爲它沒有進行頁表項的複製。然而,寫時複製的出現減少了對於替換fork的爭論。