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