fork的基本知識
函數原型:pid_t fork( void);
返回值: 若成功調用一次則返回兩個值,子進程返回0,父進程返回子進程ID;否則,出錯返回-1
一個現有進程可以調用fork函數創建一個新進程。由fork創建的新進程被稱爲子進程(child process)。fork函數被調用一次但返回兩次。兩次返回的唯一區別是子進程中返回0值,而父進程中返回子進程ID。
如果想知道具體實現和介紹,戳這裏->進程創建
注意要點:
1、 子進程是父進程的副本,它將獲得父進程數據空間、堆、棧等資源的副本。
此處先簡要介紹下COW(Copy-on-write)機制,大致原理如下:
在複製一個對象的時候並不是真正的把原先的對象複製到內存的另外一個位置上,而是在新對象的內存映射表中設置一個指針,指向源對象的位置,並把那塊內存的Copy-On-Write位設置爲1.這樣,在對新的對象執行讀操作的時候,內存數據不發生任何變動,直接執行讀操作;
而在對新的對象執行寫操作時,將真正的對象複製到新的內存地址中,並修改新對象的內存映射表指向這個新的位置,並在新的內存位置上執行寫操作。
linux內核下fork使用COW機制工作原理:
進程0(父進程)創建進程1(子進程)後,進程0和進程1同時使用着共享代碼區內相同的代碼和數據內存頁面, 只是執行代碼不在一處,因此他們也同時使用着相同的用戶堆棧區。在爲進程1(子進程)複製其父進程(進程0)的頁目錄和頁表項時,進程0的640KB頁表項的屬性沒有改動過(仍然可讀寫),但是進程1的640KB對應的頁表項卻被設置成只讀。因此當進程1(子進程)開始執行時,對用戶堆棧的入棧操作將導致頁面寫保護異常,從而使得內核的內存管理程序爲進程1在主內存區中分配一內存頁面,並把進程0中的頁面內容複製到新的頁面上。從此時開始,進程1開始有自己獨立的內存頁面,
由於此時的內存頁面在主內存區,因此進程1中繼續創建新的子進程時也可以採用COW技術。
內核調度進程運行時次序是隨機的,進程0創建進程1後,可能先於進程1修改共享區,進程0是可讀寫的,在未分開前,進程1是隻讀的,由於兩個進程共享內存空間,
爲了不出現衝突問題,就必須要求進程0在進程1執行堆棧操作(進程1的堆棧操作會導致頁面保護異常,從而使得進程1在主內存區得到新的用戶頁面區,此時進程1和進程0纔算是真正獨立,
如前面所述)之前禁止使用用戶堆棧區。所以進程0在執行了fork(創建了進程1)之後的pause使用內嵌的方式,保證進程0(主進程)不會弄亂堆棧。
fork()後立即執行exec(),地址空間就無需被複制了,一個進程一旦調用exec類函數,它本身就“死亡”了,系統把代碼段替換成新的程序的代碼,廢棄原有的 數據段和堆棧段,併爲新程序分配新的數據段與堆棧段,唯一留下的,就是進程號,也就是說,對系統而言,還是同一個進程,不過已經是另一個程序了。
fork()的實際開銷就是複製父進程的頁表以及給子進程創建一個進程描述符。在一般情況下,進程創建後都爲馬上運行一個可執行的文件,採用COW這種優化,
可以避免拷貝大量根本就不會被使用的數據(地址空間裏常常包含數十兆的數據)。由於Unix強調進程快速執行的能力,所以這個優化是很重要的。
2、在linux中存在緩衝區的問題。當寫printf函數,但是沒有換行的話,它是不會輸出的,而是先將要輸出的內容存放再緩衝區中,當碰到換行時,將緩衝區中的內容再一起輸出。
所以fork後子進程會複製父進程的緩衝區,因此也將待輸出內容複製到自己“與父進程獨立的緩衝區中”,因此當子進程執行遇到換行,就將緩衝區中的內容全都輸出來。
代碼1:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(){
int i;
pid_t pid;
for(i = 0; i < 2; ++i){
printf("-");
pid = fork();
}
return 0;
}
輸出:- - - - - - - -
8箇中劃線
分析:
原因分析:如果不清楚fork的深拷貝機制,估計看到上述代碼的第一感覺是:死循環,永遠fork下去。一定要仔細理解上面的注意要點1。在整個執行週期內,一共會產生4個進程,由於是遍歷所以主進程會產生兩個子進程(假設i=0時fork出的子進程記爲:first,i=1時fork出的子進程記爲:second),first進程在遍歷時會再產生一個自己的子進程,記錄爲:third,而second進程,i的值已經爲1,不會再繼續執行下去,其它進程同理,所以此處不會形成死循環。
主進程會輸出2箇中劃線(總計:2個)
first進程會輸出1個自己的中劃線,考慮到注意要點2,還會輸出從主進程緩衝區拷貝過來的1箇中劃線(總計:2個)
second進程沒有自己的中劃線輸出,考慮到注意要點2,會輸出從主進程緩衝區拷貝過來的2箇中劃線(總計:2個)
third進程沒有自己的中劃線輸出,考慮到注意要點2,會輸出從first進程緩衝區拷貝過來的2箇中劃線 (總計:2個)
所以一共會輸出8個,怎麼確定會產生4個進程呢,只要把進程的id號給打印出來,就非常清晰明瞭了,具體代碼參考代碼5
代碼2:
int main(){
int i;
pid_t pid;
for(i = 0; i < 2; ++i){
pid = fork();
printf("-");
}
return 0;
}
輸出:——–
8箇中劃線
解析:雖然同代碼1一樣,也是輸出8箇中劃線,但是內部運行機制相差很遠,具體原因分析參考代碼1原因分析
主進程會輸出2箇中劃線(總計:2個)
first進程會輸出2個自己的中劃線,無主進程緩衝區拷貝輸出(總計:2個)
second進程會輸出1個自己的中劃線,考慮到注意要點2,會輸出從主進程緩衝區拷貝過來的1箇中劃線(總計:2個)
third進程會輸出1個自己的中劃線,考慮到注意要點2,會輸出從first進程緩衝區拷貝過來的1箇中劃線 (總計:2個)
代碼3:
int main(){
int i;
pid_t pid;
for(i = 0; i < 2; ++i){
printf("-\n");
pid = fork();
}
return 0;
}
輸出:
-
-
-
3箇中劃線
解析:具體原因分析參考代碼1原因分析
主進程會輸出2箇中劃線(總計:2個)
first進程會輸出1個自己的中劃線,無主進程程緩衝區拷貝輸出(總計:1個)
second進程沒有自己的中劃線輸出,無主進程緩衝區拷貝輸出(總計:0個)
third進程會沒有自己的中劃線輸出,無first進程緩衝區拷貝輸出(總計:0個)
代碼4:
int main(){
int i;
pid_t pid;
for(i = 0; i < 2; ++i){
pid = fork();
printf("-\n");
}
return 0;
}
輸出:
-
-
-
-
-
-
6箇中劃線
解析:具體原因分析參考代碼1原因分析
主進程會輸出2箇中劃線(總計:2個)
first進程會輸出2個自己的中劃線,無主進程緩衝區拷貝輸出(總計:2個)
second進程會輸出1個自己的中劃線,無主進程緩衝區拷貝輸出(總計:1個)
third進程會輸出1個自己的中劃線,無first進程緩衝區拷貝輸出(總計:1個)
代碼5:
int main(){
int i;
pid_t pid;
for(i = 0; i < 2; ++i){
pid = fork();
if(pid == 0)
printf("child process:%d", getpid());
else if(pid > 0)
printf("parent process:%d", getpid());
else
printf("error");
}
printf("\n");
return 0;
}
輸出:
parent process:5141parent process:5141
child process:5142parent process:5142
child process:5142child process:5144