一、進(jìn)程生成
初探fork函數(shù)
在linux系統(tǒng)中,fork函數(shù)扮演著至關(guān)重要的角色,它能夠從已存在的進(jìn)程中衍生出一個(gè)全新的進(jìn)程。這個(gè)新進(jìn)程被稱為子進(jìn)程,而原始進(jìn)程則成為父進(jìn)程。
返回值解析:
當(dāng)fork函數(shù)在子進(jìn)程中執(zhí)行時(shí),它會(huì)返回0;而在父進(jìn)程中,它則會(huì)返回新生成子進(jìn)程的PID。如果子進(jìn)程的創(chuàng)建失敗,fork函數(shù)將返回-1。
當(dāng)進(jìn)程調(diào)用fork函數(shù)并將控制權(quán)轉(zhuǎn)移到內(nèi)核中的fork代碼段時(shí),內(nèi)核會(huì)執(zhí)行以下操作:
- 為子進(jìn)程分配新的內(nèi)存塊和必要的內(nèi)核數(shù)據(jù)結(jié)構(gòu)。
- 將父進(jìn)程的部分數(shù)據(jù)結(jié)構(gòu)內(nèi)容復(fù)制到子進(jìn)程中。
- 將子進(jìn)程添加到系統(tǒng)進(jìn)程列表中。
- fork函數(shù)返回,調(diào)度器開(kāi)始調(diào)度新的進(jìn)程。
在執(zhí)行fork函數(shù)之后,父子進(jìn)程會(huì)共享相同的代碼段。舉個(gè)例子:
運(yùn)行結(jié)果如下:
這里可以看到,Before只輸出了一次,而After輸出了兩次。其中,Before是由父進(jìn)程打印的,而調(diào)用fork函數(shù)之后打印的兩個(gè)After,則分別由父進(jìn)程和子進(jìn)程兩個(gè)進(jìn)程執(zhí)行。也就是說(shuō),fork之前父進(jìn)程獨(dú)立執(zhí)行,而fork之后父子兩個(gè)執(zhí)行流分別執(zhí)行。
注意: fork之后,父進(jìn)程和子進(jìn)程誰(shuí)先執(zhí)行完全由調(diào)度器決定。
fork函數(shù)返回值
?
fork函數(shù)為什么要給子進(jìn)程返回0,給父進(jìn)程返回子進(jìn)程的PID?
一個(gè)父進(jìn)程可以創(chuàng)建多個(gè)子進(jìn)程,而一個(gè)子進(jìn)程只能有一個(gè)父進(jìn)程。因此,對(duì)于子進(jìn)程來(lái)說(shuō),父進(jìn)程是不需要被標(biāo)識(shí)的;而對(duì)于父進(jìn)程來(lái)說(shuō),子進(jìn)程是需要被標(biāo)識(shí)的,因?yàn)楦高M(jìn)程創(chuàng)建子進(jìn)程的目的是讓其執(zhí)行任務(wù)的,父進(jìn)程只有知道了子進(jìn)程的PID才能很好的對(duì)該子進(jìn)程指派任務(wù)。
?
為什么fork函數(shù)有兩個(gè)返回值?
父進(jìn)程調(diào)用fork函數(shù)后,為了創(chuàng)建子進(jìn)程,fork函數(shù)內(nèi)部將會(huì)進(jìn)行一系列操作,包括創(chuàng)建子進(jìn)程的進(jìn)程控制塊、創(chuàng)建子進(jìn)程的進(jìn)程地址空間、創(chuàng)建子進(jìn)程對(duì)應(yīng)的頁(yè)表等等。子進(jìn)程創(chuàng)建完畢后,操作系統(tǒng)還需要將子進(jìn)程的進(jìn)程控制塊添加到系統(tǒng)進(jìn)程列表當(dāng)中,此時(shí)子進(jìn)程便創(chuàng)建完畢了。
也就是說(shuō),在fork函數(shù)內(nèi)部執(zhí)行return語(yǔ)句之前,子進(jìn)程就已經(jīng)創(chuàng)建完畢了,那么之后的return語(yǔ)句不僅父進(jìn)程需要執(zhí)行,子進(jìn)程也同樣需要執(zhí)行,這就是fork函數(shù)有兩個(gè)返回值的原因。
寫(xiě)時(shí)拷貝
當(dāng)子進(jìn)程剛剛被創(chuàng)建時(shí),子進(jìn)程和父進(jìn)程的數(shù)據(jù)和代碼是共享的,即父子進(jìn)程的代碼和數(shù)據(jù)通過(guò)頁(yè)表映射到物理內(nèi)存的同一塊空間。只有當(dāng)父進(jìn)程或子進(jìn)程需要修改數(shù)據(jù)時(shí),才將父進(jìn)程的數(shù)據(jù)在內(nèi)存當(dāng)中拷貝一份,然后再進(jìn)行修改。
這種在需要進(jìn)行數(shù)據(jù)修改時(shí)再進(jìn)行拷貝的技術(shù),稱為寫(xiě)時(shí)拷貝技術(shù)。
1、為什么數(shù)據(jù)要進(jìn)行寫(xiě)時(shí)拷貝?
進(jìn)程具有獨(dú)立性。多進(jìn)程運(yùn)行,需要獨(dú)享各種資源,多進(jìn)程運(yùn)行期間互不干擾,不能讓子進(jìn)程的修改影響到父進(jìn)程。
2、為什么不在創(chuàng)建子進(jìn)程的時(shí)候就進(jìn)行數(shù)據(jù)的拷貝?
子進(jìn)程不一定會(huì)使用父進(jìn)程的所有數(shù)據(jù),并且在子進(jìn)程不對(duì)數(shù)據(jù)進(jìn)行寫(xiě)入的情況下,沒(méi)有必要對(duì)數(shù)據(jù)進(jìn)行拷貝,我們應(yīng)該按需分配,在需要修改數(shù)據(jù)的時(shí)候再分配(延時(shí)分配),這樣可以高效的使用內(nèi)存空間。
3、代碼會(huì)不會(huì)進(jìn)行寫(xiě)時(shí)拷貝?
90%的情況下是不會(huì)的,但這并不代表代碼不能進(jìn)行寫(xiě)時(shí)拷貝,例如在進(jìn)行進(jìn)程替換的時(shí)候,則需要進(jìn)行代碼的寫(xiě)時(shí)拷貝。
fork常規(guī)用法
- 一個(gè)進(jìn)程希望復(fù)制自己,使子進(jìn)程同時(shí)執(zhí)行不同的代碼段。例如父進(jìn)程等待客戶端請(qǐng)求,生成子進(jìn)程來(lái)處理請(qǐng)求。
- 一個(gè)進(jìn)程要執(zhí)行一個(gè)不同的程序。例如子進(jìn)程從fork返回后,調(diào)用exec函數(shù)。
fork調(diào)用失敗的原因
fork函數(shù)創(chuàng)建子進(jìn)程也可能會(huì)失敗,有以下兩種情況:
- 系統(tǒng)中有太多的進(jìn)程,內(nèi)存空間不足,子進(jìn)程創(chuàng)建失敗。
- 實(shí)際用戶的進(jìn)程數(shù)超過(guò)了限制,子進(jìn)程創(chuàng)建失敗。
二、進(jìn)程終止
進(jìn)程退出場(chǎng)景
進(jìn)程退出只有三種情況:
- 代碼運(yùn)行完畢,結(jié)果正確。
- 代碼運(yùn)行完畢,結(jié)果不正確。
- 代碼異常終止(進(jìn)程崩潰)。
進(jìn)程退出碼
我們都知道m(xù)ain函數(shù)是代碼的入口,但實(shí)際上main函數(shù)只是用戶級(jí)別代碼的入口,main函數(shù)也是被其他函數(shù)調(diào)用的,例如在VS2013當(dāng)中main函數(shù)就是被一個(gè)名為_(kāi)_tmainCRTStartup的函數(shù)所調(diào)用,而__tmainCRTStartup函數(shù)又是通過(guò)加載器被操作系統(tǒng)所調(diào)用的,也就是說(shuō)main函數(shù)是間接性被操作系統(tǒng)所調(diào)用的。
既然main函數(shù)是間接性被操作系統(tǒng)所調(diào)用的,那么當(dāng)main函數(shù)調(diào)用結(jié)束后就應(yīng)該給操作系統(tǒng)返回相應(yīng)的退出信息,而這個(gè)所謂的退出信息就是以退出碼的形式作為main函數(shù)的返回值返回,我們一般以0表示代碼成功執(zhí)行完畢,以非0表示代碼執(zhí)行過(guò)程中出現(xiàn)錯(cuò)誤,這就是為什么我們都在main函數(shù)的最后返回0的原因。
當(dāng)我們的代碼運(yùn)行起來(lái)就變成了進(jìn)程,當(dāng)進(jìn)程結(jié)束后main函數(shù)的返回值實(shí)際上就是該進(jìn)程的進(jìn)程退出碼,我們可以使用echo $?命令查看最近一次進(jìn)程退出的退出碼信息。
例如,對(duì)于下面這個(gè)簡(jiǎn)單的代碼:
代碼運(yùn)行結(jié)束后,我們可以查看該進(jìn)程的進(jìn)程退出碼。
[cl@VM-0-15-centos?procTermination]$?echo?$?
這時(shí)便可以確定main函數(shù)是順利執(zhí)行完畢了。
?
為什么以0表示代碼執(zhí)行成功,以非0表示代碼執(zhí)行錯(cuò)誤?
因?yàn)榇a執(zhí)行成功只有一種情況,成功了就是成功了,而代碼執(zhí)行錯(cuò)誤卻有多種原因,例如內(nèi)存空間不足、非法訪問(wèn)以及棧溢出等等,我們就可以用這些非0的數(shù)字分別表示代碼執(zhí)行錯(cuò)誤的原因。
c語(yǔ)言當(dāng)中的strerror函數(shù)可以通過(guò)錯(cuò)誤碼,獲取該錯(cuò)誤碼在C語(yǔ)言當(dāng)中對(duì)應(yīng)的錯(cuò)誤信息:
運(yùn)行代碼后我們就可以看到各個(gè)錯(cuò)誤碼所對(duì)應(yīng)的錯(cuò)誤信息:
實(shí)際上Linux中的ls、pwd等命令都是可執(zhí)行程序,使用這些命令后我們也可以查看其對(duì)應(yīng)的退出碼。
可以看到,這些命令成功執(zhí)行后,其退出碼也是0。
但是命令執(zhí)行錯(cuò)誤后,其退出碼就是非0的數(shù)字,該數(shù)字具體代表某一錯(cuò)誤信息。
注意: 退出碼都有對(duì)應(yīng)的字符串含義,幫助用戶確認(rèn)執(zhí)行失敗的原因,而這些退出碼具體代表什么含義是人為規(guī)定的,不同環(huán)境下相同的退出碼的字符串含義可能不同。
進(jìn)程正常退出
return退出
在main函數(shù)中使用return退出進(jìn)程是我們常用的方法。
例如,在main函數(shù)最后使用return退出進(jìn)程。
運(yùn)行結(jié)果:
exit函數(shù)
使用exit函數(shù)退出進(jìn)程也是我們常用的方法,exit函數(shù)可以在代碼中的任何地方退出進(jìn)程,并且exit函數(shù)在退出進(jìn)程前會(huì)做一系列工作:
- 執(zhí)行用戶通過(guò)atexit或on_exit定義的清理函數(shù)。
- 關(guān)閉所有打開(kāi)的流,所有的緩存數(shù)據(jù)均被寫(xiě)入。
- 調(diào)用_exit函數(shù)終止進(jìn)程。
例如,以下代碼中exit終止進(jìn)程前會(huì)將緩沖區(qū)當(dāng)中的數(shù)據(jù)輸出。
運(yùn)行結(jié)果:
_exit函數(shù)
使用_exit函數(shù)退出進(jìn)程的方法我們并不經(jīng)常使用,_exit函數(shù)也可以在代碼中的任何地方退出進(jìn)程,但是_exit函數(shù)會(huì)直接終止進(jìn)程,并不會(huì)在退出進(jìn)程前會(huì)做任何收尾工作。
例如,以下代碼中使用_exit終止進(jìn)程,則緩沖區(qū)當(dāng)中的數(shù)據(jù)將不會(huì)被輸出。
運(yùn)行結(jié)果:
return、exit和_exit之間的區(qū)別與聯(lián)系
?
return、exit和_exit之間的區(qū)別
只有在main函數(shù)當(dāng)中的return才能起到退出進(jìn)程的作用,子函數(shù)當(dāng)中return不能退出進(jìn)程,而exit函數(shù)和_exit函數(shù)在代碼中的任何地方使用都可以起到退出進(jìn)程的作用。
使用exit函數(shù)退出進(jìn)程前,exit函數(shù)會(huì)執(zhí)行用戶定義的清理函數(shù)、沖刷緩沖,關(guān)閉流等操作,然后再終止進(jìn)程,而_exit函數(shù)會(huì)直接終止進(jìn)程,不會(huì)做任何收尾工作。
?
return、exit和_exit之間的聯(lián)系
執(zhí)行return num等同于執(zhí)行exit(num),因?yàn)檎{(diào)用main函數(shù)運(yùn)行結(jié)束后,會(huì)將main函數(shù)的返回值當(dāng)做exit的參數(shù)來(lái)調(diào)用exit函數(shù)。
使用exit函數(shù)退出進(jìn)程前,exit函數(shù)會(huì)先執(zhí)行用戶定義的清理函數(shù)、沖刷緩沖,關(guān)閉流等操作,然后再調(diào)用_exit函數(shù)終止進(jìn)程。
進(jìn)程異常退出
情況一:向進(jìn)程發(fā)生信號(hào)導(dǎo)致進(jìn)程異常退出。
例如,在進(jìn)程運(yùn)行過(guò)程中向進(jìn)程發(fā)生kill -9信號(hào)使得進(jìn)程異常退出,或是使用Ctrl+C使得進(jìn)程異常退出等。
情況二:代碼錯(cuò)誤導(dǎo)致進(jìn)程運(yùn)行時(shí)異常退出。
例如,代碼當(dāng)中存在野指針問(wèn)題使得進(jìn)程運(yùn)行時(shí)異常退出,或是出現(xiàn)除0的情況使得進(jìn)程運(yùn)行時(shí)異常退出等。
三、進(jìn)程等待
進(jìn)程等待的必要性
- 子進(jìn)程退出,父進(jìn)程如果不讀取子進(jìn)程的退出信息,子進(jìn)程就會(huì)變成僵尸進(jìn)程,進(jìn)而造成內(nèi)存泄漏。
- 進(jìn)程一旦變成僵尸進(jìn)程,那么就算是kill -9命令也無(wú)法將其殺死,因?yàn)檎l(shuí)也無(wú)法殺死一個(gè)已經(jīng)死去的進(jìn)程。
- 對(duì)于一個(gè)進(jìn)程來(lái)說(shuō),最關(guān)心自己的就是其父進(jìn)程,因?yàn)楦高M(jìn)程需要知道自己派給子進(jìn)程的任務(wù)完成的如何。
- 父進(jìn)程需要通過(guò)進(jìn)程等待的方式,回收子進(jìn)程資源,獲取子進(jìn)程的退出信息。
獲取子進(jìn)程status
下面進(jìn)程等待所使用的兩個(gè)函數(shù)wait和waitpid,都有一個(gè)status參數(shù),該參數(shù)是一個(gè)輸出型參數(shù),由操作系統(tǒng)進(jìn)行填充。
如果對(duì)status參數(shù)傳入NULL,表示不關(guān)心子進(jìn)程的退出狀態(tài)信息。否則,操作系統(tǒng)會(huì)通過(guò)該參數(shù),將子進(jìn)程的退出信息反饋給父進(jìn)程。
status是一個(gè)整型變量,但status不能簡(jiǎn)單的當(dāng)作整型來(lái)看待,status的不同比特位所代表的信息不同,具體細(xì)節(jié)如下(只研究status低16比特位):
在status的低16比特位當(dāng)中,高8位表示進(jìn)程的退出狀態(tài),即退出碼。進(jìn)程若是被信號(hào)所殺,則低7位表示終止信號(hào),而第8位比特位是core dump標(biāo)志。
我們通過(guò)一系列位操作,就可以根據(jù)status得到進(jìn)程的退出碼和退出信號(hào)。
exitCode?=?(status?>>?8)?&?0xFF;?//退出碼 exitSignal?=?status?&?0x7F;??????//退出信號(hào)
對(duì)于此,系統(tǒng)當(dāng)中提供了兩個(gè)宏來(lái)獲取退出碼和退出信號(hào)。
- WIFEXITED(status):用于查看進(jìn)程是否是正常退出,本質(zhì)是檢查是否收到信號(hào)。
- WEXITSTATUS(status):用于獲取進(jìn)程的退出碼。
exitNormal?=?WIFEXITED(status);??//是否正常退出 exitCode?=?WEXITSTATUS(status);??//獲取退出碼
需要注意的是,當(dāng)一個(gè)進(jìn)程非正常退出時(shí),說(shuō)明該進(jìn)程是被信號(hào)所殺,那么該進(jìn)程的退出碼也就沒(méi)有意義了。
進(jìn)程等待的方法
wait方法
函數(shù)原型:pid_t wait(int* status);
作用:等待任意子進(jìn)程。
返回值:等待成功返回被等待進(jìn)程的pid,等待失敗返回-1。
參數(shù):輸出型參數(shù),獲取子進(jìn)程的退出狀態(tài),不關(guān)心可設(shè)置為NULL。
例如,創(chuàng)建子進(jìn)程后,父進(jìn)程可使用wait函數(shù)一直等待子進(jìn)程,直到子進(jìn)程退出后讀取子進(jìn)程的退出信息。
#include? #include? #include? #include? #include? int?main() { ?pid_t?id?=?fork(); ?if(id?==?0){ ??//child ??int?count?=?10; ??while(count--){ ???printf("I?am?child...PID:%d,?PPID:%d ",?getpid(),?getppid()); ???sleep(1); ??} ??exit(0); ?} ?//father ?int?status?=?0; ?pid_t?ret?=?wait(&status); ?if(ret?>?0){ ??//wait?success ??printf("wait?child?success... "); ??if(WIFEXITED(status)){ ???//exit?normal ???printf("exit?code:%d ",?WEXITSTATUS(status)); ??} ?} ?sleep(3); ?return?0; }
我們可以使用以下監(jiān)控腳本對(duì)進(jìn)程進(jìn)行實(shí)時(shí)監(jiān)控:
[cl@VM-0-15-centos?procWait]$?while?:;?do?ps?axj?|?head?-1?&&?ps?axj?|?grep?proc?|?grep?-v?grep;echo?"######################";sleep?1;done
這時(shí)我們可以看到,當(dāng)子進(jìn)程退出后,父進(jìn)程讀取了子進(jìn)程的退出信息,子進(jìn)程也就不會(huì)變成僵尸進(jìn)程了。
waitpid方法
函數(shù)原型:pid_t waitpid(pid_t pid, int* status, int options);
作用:等待指定子進(jìn)程或任意子進(jìn)程。
返回值:
1、等待成功返回被等待進(jìn)程的pid。
2、如果設(shè)置了選項(xiàng)WNOHANG,而調(diào)用中waitpid發(fā)現(xiàn)沒(méi)有已退出的子進(jìn)程可收集,則返回0。
3、如果調(diào)用中出錯(cuò),則返回-1,這時(shí)errno會(huì)被設(shè)置成相應(yīng)的值以指示錯(cuò)誤所在。
參數(shù):
1、pid:待等待子進(jìn)程的pid,若設(shè)置為-1,則等待任意子進(jìn)程。
2、status:輸出型參數(shù),獲取子進(jìn)程的退出狀態(tài),不關(guān)心可設(shè)置為NULL。
3、options:當(dāng)設(shè)置為WNOHANG時(shí),若等待的子進(jìn)程沒(méi)有結(jié)束,則waitpid函數(shù)直接返回0,不予以等待。若正常結(jié)束,則返回該子進(jìn)程的pid。
例如,創(chuàng)建子進(jìn)程后,父進(jìn)程可使用waitpid函數(shù)一直等待子進(jìn)程(此時(shí)將waitpid的第三個(gè)參數(shù)設(shè)置為0),直到子進(jìn)程退出后讀取子進(jìn)程的退出信息。
#include? #include? #include? #include? #include? int?main() { ?pid_t?id?=?fork(); ?if?(id?==?0){ ??//child?????????? ??int?count?=?10; ??while?(count--){ ???printf("I?am?child...PID:%d,?PPID:%d ",?getpid(),?getppid()); ???sleep(1); ??} ??exit(0); ?} ?//father??????????? ?int?status?=?0; ?pid_t?ret?=?waitpid(id,?&status,?0); ?if?(ret?>=?0){ ??//wait?success???????????????????? ??printf("wait?child?success... "); ??if?(WIFEXITED(status)){ ???//exit?normal????????????????????????????????? ???printf("exit?code:%d ",?WEXITSTATUS(status)); ??} ??else{ ???//signal?killed?????????????????????????????? ???printf("killed?by?siganl?%d ",?status?&?0x7F); ??} ?} ?sleep(3); ?return?0; }
在父進(jìn)程運(yùn)行過(guò)程中,我們可以嘗試使用kill -9命令將子進(jìn)程殺死,這時(shí)父進(jìn)程也能等待子進(jìn)程成功。
注意: 被信號(hào)殺死而退出的進(jìn)程,其退出碼將沒(méi)有意義。
多進(jìn)程創(chuàng)建以及等待的代碼模型
上面演示的都是父進(jìn)程創(chuàng)建以及等待一個(gè)子進(jìn)程的例子,實(shí)際上我們還可以同時(shí)創(chuàng)建多個(gè)子進(jìn)程,然后讓父進(jìn)程依次等待子進(jìn)程退出,這叫做多進(jìn)程創(chuàng)建以及等待的代碼模型。
例如,以下代碼中同時(shí)創(chuàng)建了10個(gè)子進(jìn)程,同時(shí)將子進(jìn)程的pid放入到ids數(shù)組當(dāng)中,并將這10個(gè)子進(jìn)程退出時(shí)的退出碼設(shè)置為該子進(jìn)程pid在數(shù)組ids中的下標(biāo),之后父進(jìn)程再使用waitpid函數(shù)指定等待這10個(gè)子進(jìn)程。
#include? #include? #include? #include? #include? int?main() { ?pid_t?ids[10]; ?for?(int?i?=?0;?i?if?(id?==?0){ ???//child ???printf("child?process?created?successfully...PID:%d ",?getpid()); ???sleep(3); ???exit(i);?//將子進(jìn)程的退出碼設(shè)置為該子進(jìn)程PID在數(shù)組ids中的下標(biāo) ??} ??//father ??ids[i]?=?id; ?} ?for?(int?i?=?0;?i?if?(ret?>=?0){ ???//wait?child?success ???printf("wiat?child?success..PID:%d ",?ids[i]); ???if?(WIFEXITED(status)){ ????//exit?normal ????printf("exit?code:%d ",?WEXITSTATUS(status)); ???} ???else{ ????//signal?killed ????printf("killed?by?signal?%d ",?status?&?0x7F); ???} ??} ?} ?return?0; }
運(yùn)行代碼,這時(shí)我們便可以看到父進(jìn)程同時(shí)創(chuàng)建多個(gè)子進(jìn)程,當(dāng)子進(jìn)程退出后,父進(jìn)程再依次讀取這些子進(jìn)程的退出信息。
基于非阻塞接口的輪詢檢測(cè)方案
上述所給例子中,當(dāng)子進(jìn)程未退出時(shí),父進(jìn)程都在一直等待子進(jìn)程退出,在等待期間,父進(jìn)程不能做任何事情,這種等待叫做阻塞等待。
實(shí)際上我們可以讓父進(jìn)程不要一直等待子進(jìn)程退出,而是當(dāng)子進(jìn)程未退出時(shí)父進(jìn)程可以做一些自己的事情,當(dāng)子進(jìn)程退出時(shí)再讀取子進(jìn)程的退出信息,即非阻塞等待。
?
做法很簡(jiǎn)單,向waitpid函數(shù)的第三個(gè)參數(shù)potions傳入WNOHANG,這樣一來(lái),等待的子進(jìn)程若是沒(méi)有結(jié)束,那么waitpid函數(shù)將直接返回0,不予以等待。而等待的子進(jìn)程若是正常結(jié)束,則返回該子進(jìn)程的pid。
例如,父進(jìn)程可以隔一段時(shí)間調(diào)用一次waitpid函數(shù),若是等待的子進(jìn)程尚未退出,則父進(jìn)程可以先去做一些其他事,過(guò)一段時(shí)間再調(diào)用waitpid函數(shù)讀取子進(jìn)程的退出信息。
#include? #include? #include? #include? #include? int?main() { ?pid_t?id?=?fork(); ?if?(id?==?0){ ??//child ??int?count?=?3; ??while?(count--){ ???printf("child?do?something...PID:%d,?PPID:%d ",?getpid(),?getppid()); ???sleep(3); ??} ??exit(0); ?} ?//father ?while?(1){ ??int?status?=?0; ??pid_t?ret?=?waitpid(id,?&status,?WNOHANG); ??if?(ret?>?0){ ???printf("wait?child?success... "); ???printf("exit?code:%d ",?WEXITSTATUS(status)); ???break; ??} ??else?if?(ret?==?0){ ???printf("father?do?other?things... "); ???sleep(1); ??} ??else{ ???printf("waitpid?error... "); ???break; ??} ?} ?return?0; }
運(yùn)行結(jié)果就是,父進(jìn)程每隔一段時(shí)間就去查看子進(jìn)程是否退出,若未退出,則父進(jìn)程先去忙自己的事情,過(guò)一段時(shí)間再來(lái)查看,直到子進(jìn)程退出后讀取子進(jìn)程的退出信息。
四、進(jìn)程程序替換
替換原理
用fork創(chuàng)建子進(jìn)程后,子進(jìn)程執(zhí)行的是和父進(jìn)程相同的程序(但有可能執(zhí)行不同的代碼分支),若想讓子進(jìn)程執(zhí)行另一個(gè)程序,往往需要調(diào)用一種exec函數(shù)。
當(dāng)進(jìn)程調(diào)用一種exec函數(shù)時(shí),該進(jìn)程的用戶空間代碼和數(shù)據(jù)完全被新程序替換,并從新程序的啟動(dòng)例程開(kāi)始執(zhí)行。
?
當(dāng)進(jìn)行進(jìn)程程序替換時(shí),有沒(méi)有創(chuàng)建新的進(jìn)程?
進(jìn)程程序替換之后,該進(jìn)程對(duì)應(yīng)的PCB、進(jìn)程地址空間以及頁(yè)表等數(shù)據(jù)結(jié)構(gòu)都沒(méi)有發(fā)生改變,只是進(jìn)程在物理內(nèi)存當(dāng)中的數(shù)據(jù)和代碼發(fā)生了改變,所以并沒(méi)有創(chuàng)建新的進(jìn)程,而且進(jìn)程程序替換前后該進(jìn)程的pid并沒(méi)有改變。
?
子進(jìn)程進(jìn)行進(jìn)程程序替換后,會(huì)影響父進(jìn)程的代碼和數(shù)據(jù)嗎?
子進(jìn)程剛被創(chuàng)建時(shí),與父進(jìn)程共享代碼和數(shù)據(jù),但當(dāng)子進(jìn)程需要進(jìn)行進(jìn)程程序替換時(shí),也就意味著子進(jìn)程需要對(duì)其數(shù)據(jù)和代碼進(jìn)行寫(xiě)入操作,這時(shí)便需要將父子進(jìn)程共享的代碼和數(shù)據(jù)進(jìn)行寫(xiě)時(shí)拷貝,此后父子進(jìn)程的代碼和數(shù)據(jù)也就分離了,因此子進(jìn)程進(jìn)行程序替換后不會(huì)影響父進(jìn)程的代碼和數(shù)據(jù)。
替換函數(shù)
替換函數(shù)有六種以exec開(kāi)頭的函數(shù),它們統(tǒng)稱為exec函數(shù):
一、int execl(const char *path, const char *arg, …);
第一個(gè)參數(shù)是要執(zhí)行程序的路徑,第二個(gè)參數(shù)是可變參數(shù)列表,表示你要如何執(zhí)行這個(gè)程序,并以NULL結(jié)尾。
例如,要執(zhí)行的是ls程序。
execl("/usr/bin/ls",?"ls",?"-a",?"-i",?"-l",?NULL);
二、int execlp(const char *file, const char *arg, …);
第一個(gè)參數(shù)是要執(zhí)行程序的名字,第二個(gè)參數(shù)是可變參數(shù)列表,表示你要如何執(zhí)行這個(gè)程序,并以NULL結(jié)尾。
例如,要執(zhí)行的是ls程序。
execlp("ls",?"ls",?"-a",?"-i",?"-l",?NULL);
三、int execle(const char *path, const char *arg, …, char *const envp[]);
第一個(gè)參數(shù)是要執(zhí)行程序的路徑,第二個(gè)參數(shù)是可變參數(shù)列表,表示你要如何執(zhí)行這個(gè)程序,并以NULL結(jié)尾,第三個(gè)參數(shù)是你自己設(shè)置的環(huán)境變量。
例如,你設(shè)置了MYVAL環(huán)境變量,在mycmd程序內(nèi)部就可以使用該環(huán)境變量。
char*?myenvp[]?=?{?"MYVAL=2021",?NULL?}; execle("./mycmd",?"mycmd",?NULL,?myenvp);
四、int execv(const char *path, char *const argv[]);
第一個(gè)參數(shù)是要執(zhí)行程序的路徑,第二個(gè)參數(shù)是一個(gè)指針數(shù)組,數(shù)組當(dāng)中的內(nèi)容表示你要如何執(zhí)行這個(gè)程序,數(shù)組以NULL結(jié)尾。
例如,要執(zhí)行的是ls程序。
char*?myargv[]?=?{?"ls",?"-a",?"-i",?"-l",?NULL?}; execv("/usr/bin/ls",?myargv);
五、int execvp(const char *file, char *const argv[]);
第一個(gè)參數(shù)是要執(zhí)行程序的名字,第二個(gè)參數(shù)是一個(gè)指針數(shù)組,數(shù)組當(dāng)中的內(nèi)容表示你要如何執(zhí)行這個(gè)程序,數(shù)組以NULL結(jié)尾。
例如,要執(zhí)行的是ls程序。
char*?myargv[]?=?{?"ls",?"-a",?"-i",?"-l",?NULL?}; execvp("ls",?myargv);
六、int execve(const char *path, char *const argv[], char *const envp[]);
第一個(gè)參數(shù)是要執(zhí)行程序的路徑,第二個(gè)參數(shù)是一個(gè)指針數(shù)組,數(shù)組當(dāng)中的內(nèi)容表示你要如何執(zhí)行這個(gè)程序,數(shù)組以NULL結(jié)尾,第三個(gè)參數(shù)是你自己設(shè)置的環(huán)境變量。
例如,你設(shè)置了MYVAL環(huán)境變量,在mycmd程序內(nèi)部就可以使用該環(huán)境變量。
char*?myargv[]?=?{?"mycmd",?NULL?}; char*?myenvp[]?=?{?"MYVAL=2021",?NULL?}; execve("./mycmd",?myargv,?myenvp);
函數(shù)解釋
- 這些函數(shù)如果調(diào)用成功,則加載指定的程序并從啟動(dòng)代碼開(kāi)始執(zhí)行,不再返回。
- 如果調(diào)用出錯(cuò),則返回-1。
也就是說(shuō),exec系列函數(shù)只要返回了,就意味著調(diào)用失敗。
命名理解
這六個(gè)exec系列函數(shù)的函數(shù)名都以exec開(kāi)頭,其后綴的含義如下:
- l(list):表示參數(shù)采用列表的形式,一一列出。
- v(vector):表示參數(shù)采用數(shù)組的形式。
- p(path):表示能自動(dòng)搜索環(huán)境變量PATH,進(jìn)行程序查找。
- e(env):表示可以傳入自己設(shè)置的環(huán)境變量。
事實(shí)上,只有execve才是真正的系統(tǒng)調(diào)用,其它五個(gè)函數(shù)最終都是調(diào)用的execve,所以execve在man手冊(cè)的第2節(jié),而其它五個(gè)函數(shù)在man手冊(cè)的第3節(jié),也就是說(shuō)其他五個(gè)函數(shù)實(shí)際上是對(duì)系統(tǒng)調(diào)用execve進(jìn)行了封裝,以滿足不同用戶的不同調(diào)用場(chǎng)景的。
下圖為exec系列函數(shù)族之間的關(guān)系:
做一個(gè)簡(jiǎn)易的shell
shell也就是命令行解釋器,其運(yùn)行原理就是:當(dāng)有命令需要執(zhí)行時(shí),shell創(chuàng)建子進(jìn)程,讓子進(jìn)程執(zhí)行命令,而shell只需等待子進(jìn)程退出即可。
其實(shí)shell需要執(zhí)行的邏輯非常簡(jiǎn)單,其只需循環(huán)執(zhí)行以下步驟:
- 獲取命令行。
- 解析命令行。
- 創(chuàng)建子進(jìn)程。
- 替換子進(jìn)程。
- 等待子進(jìn)程退出。
其中,創(chuàng)建子進(jìn)程使用fork函數(shù),替換子進(jìn)程使用exec系列函數(shù),等待子進(jìn)程使用wait或者waitpid函數(shù)。
于是我們可以很容易實(shí)現(xiàn)一個(gè)簡(jiǎn)易的shell,代碼如下:
#include? #include? #include? #include? #include? #include? #include? #define?LEN?1024?//命令最大長(zhǎng)度 #define?NUM?32?//命令拆分后的最大個(gè)數(shù) int?main() { ?char?cmd[LEN];?//存儲(chǔ)命令 ?char*?myargv[NUM];?//存儲(chǔ)命令拆分后的結(jié)果 ?char?hostname[32];?//主機(jī)名 ?char?pwd[128];?//當(dāng)前目錄 ?while?(1){ ??//獲取命令提示信息 ??struct?passwd*?pass?=?getpwuid(getuid()); ??gethostname(hostname,?sizeof(hostname)-1); ??getcwd(pwd,?sizeof(pwd)-1); ??int?len?=?strlen(pwd); ??char*?p?=?pwd?+?len?-?1; ??while?(*p?!=?'/'){ ???p--; ??} ??p++; ??//打印命令提示信息 ??printf("[%s@%s?%s]$?",?pass->pw_name,?hostname,?p); ??//讀取命令 ??fgets(cmd,?LEN,?stdin); ??cmd[strlen(cmd)?-?1]?=?' 主站蜘蛛池模板: 一级欧美 | 91综合网 | jav成人av免费播放 | 视频在线亚洲 | 欧美日韩一区二区电影 | 一级片av | www免费视频 | 日韩羞羞 | 欧美一级在线观看 | 精品一区二区久久久久久久网站 | 人人干在线 | 久久久国产一区二区三区 | 伊人狼人影院 | 国产在线精品免费 | 91美女在线 | 成人在线电影在线观看 | 日韩在线免费视频 | 国产精品自产拍 | 91一区二区| 麻豆av电影网 | 在线观看国产视频 | 亚洲色图第一页 | 亚洲一级淫片 | 午夜久久久久久久久久一区二区 | 亚洲精品区 | 欧美1区2区| 中文字幕第三页 | 欧美日韩在线一区 | 亚洲一二三区在线观看 | 成人一区二区视频 | 国产大学生情侣呻吟视频 | 黄a在线观看 | 欧美日韩一区二区电影 | 久久九九色 | 91视频久久 | 成人精品一区二区三区 | 国产精品久久久久久久久久久久 | 美女久久久久 | 罗宾被扒开腿做同人网站 | 成人av观看 | 欧美性一区二区三区 |