fork函數:
一個進程,包括代碼、數據和分配給進程的資源。fork()函數通過系統調用創建一個與原來進程幾乎完全相同的進程,也就是兩個進程可以做完全相同的事,但如果初始參數或者傳入的變量不同,兩個進程也可以做不同的事。
一個進程調用fork()函數后,系統先給新的進程分配資源,例如存儲數據和代碼的空間。然后把原來的進程的所有值都復制到新的新進程中,只有少數值與原來的進程的值不同。相當于克隆了一個自己。
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);fork調用的一個奇妙之處就是它僅僅被調用一次
卻能夠返回兩次,它可能有三種不同的返回值:
1)在父進程中,fork返回新創建子進程的進程ID;
2)在子進程中,fork返回0;
3)如果出現錯誤,fork返回一個負值;
在fork函數執行完畢后,如果創建新進程成功
則出現兩個進程,一個是子進程,一個是父進程
在子進程中,fork函數返回0,在父進程中,fork返回新創建子進程的進程ID
我們可以通過fork返回的值來判斷當前進程是子進程還是父進程。fork出錯可能有兩種原因:
1)當前的進程數已經達到了系統規定的上限,這時errno的值被設置為EAGAIN。
2)系統內存不足,這時errno的值被設置為ENOMEM。
創建新進程成功后,系統中出現兩個基本完全相同的進程,這兩個進程執行沒有固定的先后順序,哪個進程先執行要看系統的進程調度策略。
子父進程執行過程:
#include<stdio.h>
#include <sys/types.h>
#include <unistd.h>int main()
{pid_t pid;pid_t fpid;pid=getpid();printf("before fork:pid=%d\n",getpid());fpid=fork();printf("after fork:pid=%d\n",getpid());if(pid==getpid()){printf("This is father printf,pid=%d\n",pid);}else{printf("This son printf,pid:%d\n",getpid());}return 0;
}運行結果:
before fork:pid=14396
after fork:pid1=4396
This is father printf,pid=14396
after fork:pid14397
This son printf,pid:14397由結果可知在程序中父進程會把符合條件的整個代碼
都執行一遍,然后子進程開始執行fork()函數之后的
代碼,子進程執行過程中不會執行fork()函數之前的
代碼,但是可以訪問fork()函數之前父進程中的變量。
在語句fpid=fork()之前,只有一個進程在執行這段代
碼,但在這條語句之后,就變成兩個進程在執行了
fork函數創建的新進程的存儲空間是如何分配的?
每一個進程都有自己的存儲空間,創建的新的進程也不例外,在早期linux系統中會把父進程中的命令行參數、堆、棧、未初始化數據、初始化數據和正文全部拷貝一份到自己開辟的內存空間,后來隨著linux內核技術的更新,并不是把所有的東西全部拷貝到自己的內存,而是寫時拷貝,什么時候會寫時才拷貝?很顯然,當然是在共享同一塊內存的類發生內容改變時,才會發生。
寫時拷貝技術:
學習過fork我們都知道是父進程創建出一個子進程,子進程作為父進程的副本, 是父進程的拷貝。
可是每次fork出的子進程難道還要把父進程的各種數據拷貝一份?有人會說不是父子進程不共享各種數據段嗎?如全局變量區 ,棧區 , 堆區 。如果不拷貝那不就成共享的嗎?其實有關子進程拷貝父進程的數據是這樣的。如果子進程只是對父進程的數據進行讀取操作,那么子進程用的就是父進程的數據。如果子進程需要對某數據進行修改,那么在修改前,子進程才會拷貝出需要修改的這份數據,對這份備份進行修改。這就滿足了父子進程的數據相互獨立,互不影響的要求。這么做的初衷也是為了節省內存。
舉個栗子如果一份代碼中,定義了10個數據。父進程執行的部分對這10個數據全部進行修改,而子進程執行的部分只修改了一個數據,子進程明明用不到其他9個數據,那還何必讓子進程拷貝全部數據,多占用9個永遠使用不到的數據內存?
因此創建子進程只是將原父進程的pcb拷貝了一份。父子進程的pcb全部指向的是父進程原本就有的數據,如果子進程里對數據進行了修改,那么子進程的pcb里指向 被修改的數據的指針會指向一個自己新開辟的內存,新開辟的內存里將父進程的數據拷貝過來,然后再進行修改。這就是寫時拷貝技術,顧名思義,只在寫的時候才拷貝的技術。
關于參數的修改問題:
#include<stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{pid_t pid;pid_t fpid;pid=getpid();int data=10;printf("before fork:pid=%d\n",getpid());fpid=fork();printf("after fork:pid%d\n",getpid());if(pid==getpid()){printf("This is father printf,pid=%d\n",pid);}else{printf("This son printf,pid:%d\n",getpid());data=data+10;}printf("data=%d\n",data);return 0;
}
運行結果:
before fork:pid=14462
after fork:pid14462
This is father printf,pid=14462
data=10
after fork:pid14463
This son printf,pid:14463
data=20//當數據發生改變時才會從父進程中將要改變的值拷貝一份到子進程自己開辟的內存中去。//不影響父進程中的值
fork創建一個子進程的一般目的:
- .一個父進程希望復制自己,使父子進程同時執行不同的代碼段,在這個網絡服務進程中是常見的。父進程等待客戶端的服務請求。當這種請求到達時,父進程調用fork,使子進程處理此請求。父進程則繼續等待下一個服務請求到達。
- 一個進程要執行一個不同的程序,這對shell常見的情況。在這種情況下,子進程從fork返回后立即調用exec。
簡單使用fork(有bug后續完善):
#include<stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <unistd.h>
int main()
{pid_t pid;pid_t fpid1,fpid2;pid=getpid();int data=10;while(1){printf("請輸入數字:\n");scanf("%d",&data);if(data==1){fpid1=fork();if(fpid1==0){printf("這是創建的第一個子進程\n");while(1){printf("-----------,pid=%d\n",getpid());sleep(3);}}}else if(data==2){fpid2=fork();if(fpid2==0){printf("這是創建的第二個子進程\n");while(1){printf("-----------,pid=%d\n",getpid());sleep(3);}}}}return 0;
}
vfork函數:
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
功能:vfork() 函數和 fork() 函數一樣都是在已有的進程中創建一個新的進程,但它們創建的子進程是有區別的。返回值:成功:子進程中返回 0,父進程中返回子進程 ID。pid_t,為無符號整型。失敗:返回 -1。
fork() 與 vfock() 都是創建一個進程,那它們有什么區別呢?
- fork(): 父子進程的執行次序不確定。
vfork():保證子進程先運行,在它調用 exec(進程替換) 或 exit(退出進程)之后父進程才可能被調度運行。 - fork(): 子進程拷貝父進程的地址空間,子進程是父進程的一個復制品。
vfork():子進程共享父進程的地址空間(準確來說,在調用 exec(進程替換) 或 exit(退出進程) 之前與父進程數據是共享的)
示例演示:
#include<stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include<stdlib.h>
int main()
{pid_t pid;pid_t fpid;pid=getpid();int count=0;fpid=vfork();if(fpid>0){while(1){printf("這是父進程,PID=%d,count=%d\n",pid,count);sleep(1);}}else if(fpid==0){while(1){printf("這是子進程,PID=%d\n",getpid());sleep(1);count++;if(count==3){exit(0);}}}return 0;
}
以下是程序運行的結果:
這是子進程,PID=17935
這是子進程,PID=17935
這是子進程,PID=17935
這是父進程,PID=17934,count=3
這是父進程,PID=17934,count=3
這是父進程,PID=17934,count=3
由此可看出由vfork創建的子進程在退出前共享父進程地址空間
因為在子進程退出時父進程沒有收集子進程的狀態,所以子進程變為僵尸進程。z+表示僵尸進程,s+表示正在運行。
fhn 17999 0.0 0.0 0 0 pts/2 Z+ 21:03 0:00 [vfork] <defunct>
進程的退出方式:
(1)正常退出
- 在main函數中執行return
- 調用exit()函數
- 調用_exit()或者_Exit()函數
- 進程最后一個線程返回
- 最后一個線程調用pthread_exit
(2)異常退出
- 調用about函數
- 進程受到某個信號(如ctrl+c),而該信號使程序終止
總結:不管是那種退出方式,最終都會執行內核中的同一段代碼。這段代碼用來關閉進程中所有打開的文件描述符,釋放它所占用的內存和其他資源。
退出方式比較:
- exit和return的區別:exit是一個函數,有參數;而return是函數執行完后的返回。exit把控制權交給系統,而return將控制權交給調用函數。
- exit和abort的區別:exit是正常終止進程,而about是異常終止。
- exit(int exit_cod):exit中的參數exit_code為0代表進程正常終止,若為其他值表示程序執行過程中有錯誤發生,比如溢出,除數為0。
- exit()和_exit()的區別:exit頭文件stdlib.h中聲明,而_exit()聲明在頭文件unistd.h中。兩個函數均能正常終止進程,但是_exit()會執行后立即返回給內核,而exit()要先執行一些清除操作,然后將控制權交給內核。
父子進程終止的先后順序不同會產生不同的結果。在子進程退出前父進程退出,則系統會讓init進程接管子進程。當子進程先于父進程終止,而父進程又沒有調用wait函數等待子進程結束,子進程進入僵死狀態,并且會一直保持下去除非系統重啟。子進程處于僵死狀態是,內核只保存該進程的一些必要信息以備父進程所需。此時子進程始終占用著資源,同時也減少了系統可以創建的最大進程數。如果子進程先于父進程終止,且父進程調用了wait或waitpid函數,則父進程會等待子進程結束。
等待子進程退出:
為什么要等待子進程退出?因為創建子進程的目的就是為了執行別的代碼,然而子進程代碼的執行情況我門不了解,也不知道子進程是不是正常退出,所以我們要等待子進程的退出收集子進程退出時返回的狀態(正常退出時:根據退出碼查看退出是代碼的執行情況,異常退出時:查看異常退出的原因)。如果父進程在子進程退出時沒有收集子進程的退出狀態,則子進程就會變為僵尸進程(創建子進程后,子進程退出狀態不被收集,變成僵尸進程。爹不要它了除非爹死后變孤兒init進程養父接收。如果父進程是死循環,那么該僵尸進程就變成游魂野鬼消耗空間。)。
wait函數:
進程一旦調用了wait,就立即阻塞自己,由wait自動分析是否當前進程的某個子進程已經退出,如果讓它找到了這樣一個已經變成僵尸的子進程,wait就會收集這個子進程的信息,并把它徹底銷毀后返回;如果沒有找到這樣一個子進程,wait就會一直阻塞在這里,直到有一個出現為止。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
參數status用來保存被收集進程退出時的一些狀態
它是一個指向int類型的指針。但如果我們對這個子進程是如何死掉的毫不在意
只想把這個僵尸進程消滅掉,(事實上絕大多數情況下,我們都會這樣想),我們就可以設定這個參數為NULL。可使用wait函數傳出參數status來保存進程的退出狀態。借助宏函數來進一步判斷進程終止的具體原因。宏函數可分為如下三組:
1. WIFEXITED(status) 為非0 → 進程正常結束WEXITSTATUS(status) 如上宏為真,使用此宏 → 獲取進程退出狀態 (exit的參數)2. WIFSIGNALED(status) 為非0 → 進程異常終止WTERMSIG(status) 如上宏為真,使用此宏 → 取得使進程終止的那個信號的編號。3. WIFSTOPPED(status) 為非0 → 進程處于暫停狀態WSTOPSIG(status) 如上宏為真,使用此宏 → 取得使進程暫停的那個信號的編號。WIFCONTINUED(status) 為真 → 進程暫停后已經繼續運行//下面是使用方法:注意&status是指針
wpid = wait(&status)
if(WIFEXITED(status)){ //正常退出printf("I'm parent, The child process ""%d exit normally\n", wpid);printf("return value:%d\n", WEXITSTATUS(status));} 返回值:
如果成功,wait會返回被收集的子進程的進程ID
如果調用進程沒有子進程,調用就會失敗,此時wait返回-1,同時errno被置為ECHILD。
waitpid函數:
pid_t waitpid(pid_t pid, int *wstatus, int options);
從本質上講,系統調用waitpid和wait的作用是完全相同的但waitpid多出了兩個可由用戶控制的參數pid和options。
- pid:從參數的名字pid和類型pid_t中就可以看出這里需要的是一個進程ID,但當pid取不同的值時,在這里有不同的意義。
- pid>0時,只等待進程ID等于pid的子進程,不管其它已經有多少子進程運行結束退出了,只要指定的子進程還沒有結束,waitpid就會一直等下去。
- pid=-1時,等待任何一個子進程退出,沒有任何限制,此時waitpid和wait的作用一模一樣。
- pid=0時,等待同一個進程組中的任何子進程,如果子進程已經加入了別的進程組,waitpid不會對它做任何理睬。
- pid<-1時,等待一個指定進程組中的任何子進程,這個進程組的ID等于pid的絕對值。
options:options提供了一些額外的選項來控制waitpid,目前在Linux中只支持WNOHANG和WUNTRACED兩個選項,這是兩個常數,可以用"|"運算符把它們連接起來使用,比如:
ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);
如果我們不想使用它們,也可以把options設為0,如:ret=waitpid(-1,NULL,0);
如果使用了WNOHANG(不掛起)參數調用waitpid,即使沒有子進程退出,它也會立即返回,不會像wait那樣永遠等下去,就像當于在父進程執行的閑暇時間檢查有沒有退出的進程。雖然使用了這個收集到子進程退出的信息,但是子進程還會變為僵尸進程。
而WUNTRACED參數,由于涉及到一些跟蹤調試方面的知識,加之極少用到,這里就不多費筆墨了,有興趣的讀者可以自行查閱相關材料。
waitpid的返回值比wait稍微復雜一些,一共有3種情況:
- 當正常返回的時候,waitpid返回收集到的子進程的進程ID;
- 如果設置了選項WNOHANG,而調用中waitpid發現沒有已退出的子進程可收集,則返回0;
- 如果調用中出錯,則返回-1,這時errno會被設置成相應的值以指示錯誤所在;
- 當pid所指示的子進程不存在,或此進程存在,但不是調用進程的子進程,waitpid就會出錯返回,這時errno被設置為ECHILD;