基本概念:
我們知道在unix/linux中,正常情況下,子進程是通過父進程創建的,子進程再創建新的進程。子進程的結束和父進程的運行是一個異步過程,即父進程永遠無法預測子進程 到底什麼時候結束。 當一個 進程完成它的工作終止之後,它的父進程需要調用wait()或者waitpid()系統調用取得子進程的終止狀態。
孤兒進程:一個父進程退出,而它的一個或多個子進程還在運行,那麼那些子進程將成爲孤兒進程。孤兒進程將被init進程(進程號爲1)所收養,並由init進程對它們完成狀態收集工作。
殭屍進程:一個進程使用fork創建子進程,如果子進程退出,而父進程並沒有調用wait或waitpid獲取子進程的狀態信息,那麼子進程的進程描述符仍然保存在系統中。這種進程稱之爲僵死進程。
重要概念:
一個進程在調用exit命令結束自己生命的時候,其實它並沒有真正的被銷燬,而是留下一個稱爲殭屍進程(Zombie)的數據結構。系統調用exit的作用是使進程退出,但也僅僅限於將一個正常的進程變成一個殭屍進程,並不能將其完全銷燬。
在Linux進程的狀態中,殭屍進程是非常特殊的一種,它已經放棄了幾乎所有的空間,沒有任何可執行代碼,也不能被調度,僅僅在進程的列表中保留一個位置,記載該進程的退出狀態等信息供其他進程收集。除此之外,殭屍進程不再佔有任何內存空間。
它需要他的父進程來爲他收屍,如果他的父進程沒有安裝SIGCHLD信息處理函數調用wait或waitpid等待子進程的結束,又沒有顯示忽略該信息,那麼它就一直保持殭屍狀態,如果這時候父進程結束了,那麼init進程自動會接手這個子進程,爲它收屍,它還是能被清除的。但是如果父進程是一個循環,不會結束,那麼子進程交一直保持殭屍狀態,這就是爲什麼系統中有時會有很多殭屍進程。
問題及危害:
unix提供了一種機制可以保證只要父進程想知道子進程結束時的狀態信息, 就可以得到。這種機制就是: 在每個進程退出的時候,內核釋放該進程所有的資源,包括打開的文件,佔用的內存等。 但是仍然爲其保留一定的信息(包括進程號the process ID,退出狀態the termination status of the process,運行時間the amount of CPU time taken by the process等)。直到父進程通過wait / waitpid來取時才釋放。 但這樣就導致了問題,如果進程不調用wait / waitpid的話, 那麼保留的那段信息就不會釋放,其進程號就會一直被佔用,但是系統所能使用的進程號是有限的,如果大量的產生僵死進程,將因爲沒有可用的進程號而導致系統不能產生新的進程. 此即爲殭屍進程的危害,應當避免。
殭屍進程危害場景:
例如有個進程,它定期的產 生一個子進程,這個子進程需要做的事情很少,做完它該做的事情之後就退出了,因此這個子進程的生命週期很短,但是,父進程只管生成新的子進程,至於子進程 退出之後的事情,則一概不聞不問,這樣,系統運行上一段時間之後,系統中就會存在很多的僵死進程,倘若用ps命令查看的話,就會看到很多狀態爲Z的進程。 嚴格地來說,僵死進程並不是問題的根源,罪魁禍首是產生出大量僵死進程的那個父進程。因此,當我們尋求如何消滅系統中大量的僵死進程時,答案就是把產生大 量僵死進程的那個元兇槍斃掉(也就是通過kill發送SIGTERM或者SIGKILL信號啦)。槍斃了元兇進程之後,它產生的僵死進程就變成了孤兒進 程,這些孤兒進程會被init進程接管,init進程會wait()這些孤兒進程,釋放它們佔用的系統進程表中的資源,這樣,這些已經僵死的孤兒進程 就能瞑目而去了。
四種殭屍進程避免方式:
1.wait和waitpid函數
2.signal安裝處理函數(交給內核處理)
3.signal忽略SIGCHLD信號(交給內核處理)
4.fork兩次
書231頁
wait和waitpid:
1.
wait系統調用在Linux函數庫中的原型是:
#include <sys/types.h> /* 提供類型pid_t的定義 */
#include <sys/wait.h>
pid_t wait(int *status)
進程一旦調用了wait,就立即阻塞自己,由wait自動分析是否當前進程的某個子進程已經退出,如果讓它找到了這樣一個已經變成殭屍的子進程,wait就會收集這個子進程的信息,並把它徹底銷燬後返回;如果沒有找到這樣一個子進程,wait就會一直阻塞在這裏,直到有一個出現爲止。(wait第一個結束的進程)
參數status用來保存被收集進程退出時的一些狀態,它是一個指向int類型的指針。但如果我們對這個子進程是如何死掉的毫不在意,只想把這個殭屍進程消滅掉,(事實上絕大多數情況下,我們都會這樣想),我們就可以設定這個參數爲NULL,就象下面這樣:pid = wait(NULL);
如果成功,wait會返回被收集的子進程的進程ID,如果調用進程沒有子進程,調用就會失敗,此時wait返回-1,同時errno被置爲ECHILD。
2.
waitpid系統調用在Linux函數庫中的原型是:
#include <sys/types.h> /* 提供類型pid_t的定義 */
#include <sys/wait.h>
pid_t waitpid(pid_t pid,int *status,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中只支持0(阻塞),WNOHANG(非阻塞)和WUNTRACED兩個選項,這是兩個常數,可以用”|”運算符把它們連接起來使用。
比如:
ret = waitpid(-1,NULL,WNOHANG | WUNTRACED);
使用了WNOHANG參數調用waitpid,即使沒有子進程退出,它也會立即返回,不會像wait那樣永遠等下去。
而WUNTRACED參數,由於涉及到一些跟蹤調試方面的知識,加之極少用到,這裏就不多費筆墨了,有興趣的讀者可以自行查閱相關材料。
看到這裏,聰明的讀者可能已經看出端倪了:wait不就是經過包裝的waitpid嗎?沒錯,察看<內核源碼目錄>/include/unistd.h文件349-352行就會發現以下
例子:
/******************************************
waitpid函數控制阻塞非阻塞wait模式
******************************************/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
/***********************
錯誤處理函數
***********************/
void die(const char *msg)
{
perror(msg);
exit(1);
}
/**************************************
孫進程執行函數
***************************************/
void child2_do()
{
printf("In child2: execute 'date'\n");
sleep(5);
//打印系統時間
if (execlp("date", "date", NULL) < 0)
{
perror("child2 execlp");
}
}
/**************************************
子進程執行函數
**************************************/
void child1_do(pid_t child2, char *argv)
{
pid_t pw;
do { /**************************
控制waitpid模式,等孫進程
***************************/
if (*argv == '1')
{
pw = waitpid(child2, NULL, 0);
}
else
{
pw = waitpid(child2, NULL, WNOHANG);
}
if (pw == 0)
{
printf("In child1 process:\nThe child2 process has not exited!\n");
sleep(1);
}
}while (pw == 0);
if (pw == child2)
{
printf("Get child2 %d.\n", pw);
sleep(5);
if (execlp("pwd", "pwd", NULL) < 0)
{
perror("child1 execlp");
}
}
else
{
printf("error occured!\n");
}
}
void father_do(pid_t child1, char *argv)
{
pid_t pw;
do {
if (*argv == '1') {
pw = waitpid(child1, NULL, 0);
}
else {
pw = waitpid(child1, NULL, WNOHANG);
}
if (pw == 0) {
printf("In father process:\nThe child1 process has not exited.\n");
sleep(1);
}
}while (pw == 0);
if (pw == child1) {
printf("Get child1 %d.\n", pw);
if (execlp("ls", "ls", "-l", NULL) < 0) {
perror("father execlp");
}
}
else {
printf("error occured!\n");
}
}
int main(int argc, char *argv[])
{
pid_t child1, child2;
/*
可變參數1和2控制,waitpid阻塞和非阻塞模式
*/
if (argc < 3) {
printf("Usage: waitpid [0 1] [0 1]\n");
exit(1);
}
child1 = fork();
if (child1 < 0) {
die("child1 fork");
}
else if (child1 == 0) {
child2 = fork();
if (child2 < 0) {
die("child2 fork");
}
else if (child2 == 0) {
child2_do();
}
else {
child1_do(child2, argv[1]);
}
}
else {
father_do(child1, argv[2]);
}
return 0;
}