1、前言
之前在看《unix環(huán)境高級(jí)編程》第八章進(jìn)程時(shí)候,提到孤兒進(jìn)程和僵尸進(jìn)程,一直對(duì)這兩個(gè)概念比較模糊。今天被人問到什么是孤兒進(jìn)程和僵尸進(jìn)程,會(huì)帶來什么問題,怎么解決,我只停留在概念上面,沒有深入,倍感慚愧。晚上回來google了一下,再次參考APUE,認(rèn)真總結(jié)一下,加深理解。
2、基本概念
我們知道在unix/linux中,正常情況下,子進(jìn)程是通過父進(jìn)程創(chuàng)建的,子進(jìn)程在創(chuàng)建新的進(jìn)程。子進(jìn)程的結(jié)束和父進(jìn)程的運(yùn)行是一個(gè)異步過程,即父進(jìn)程永遠(yuǎn)無法預(yù)測(cè)子進(jìn)程 到底什么時(shí)候結(jié)束。 當(dāng)一個(gè) 進(jìn)程完成它的工作終止之后,它的父進(jìn)程需要調(diào)用wait()或者waitpid()系統(tǒng)調(diào)用取得子進(jìn)程的終止?fàn)顟B(tài)。
孤兒進(jìn)程:一個(gè)父進(jìn)程退出,而它的一個(gè)或多個(gè)子進(jìn)程還在運(yùn)行,那么那些子進(jìn)程將成為孤兒進(jìn)程。孤兒進(jìn)程將被init進(jìn)程(進(jìn)程號(hào)為1)所收養(yǎng),并由init進(jìn)程對(duì)它們完成狀態(tài)收集工作。
僵尸進(jìn)程:一個(gè)進(jìn)程使用fork創(chuàng)建子進(jìn)程,如果子進(jìn)程退出,而父進(jìn)程并沒有調(diào)用wait或waitpid獲取子進(jìn)程的狀態(tài)信息,那么子進(jìn)程的進(jìn)程描述符仍然保存在系統(tǒng)中。這種進(jìn)程稱之為僵死進(jìn)程。
3、問題及危害
unix提供了一種機(jī)制可以保證只要父進(jìn)程想知道子進(jìn)程結(jié)束時(shí)的狀態(tài)信息, 就可以得到。這種機(jī)制就是: 在每個(gè)進(jìn)程退出的時(shí)候,內(nèi)核釋放該進(jìn)程所有的資源,包括打開的文件,占用的內(nèi)存等。 但是仍然為其保留一定的信息(包括進(jìn)程號(hào)the process ID,退出狀態(tài)the termination status of the process,運(yùn)行時(shí)間the amount of CPU time taken by the process等)。直到父進(jìn)程通過wait / waitpid來取時(shí)才釋放。 但這樣就導(dǎo)致了問題,如果進(jìn)程不調(diào)用wait / waitpid的話, 那么保留的那段信息就不會(huì)釋放,其進(jìn)程號(hào)就會(huì)一直被占用,但是系統(tǒng)所能使用的進(jìn)程號(hào)是有限的,如果大量的產(chǎn)生僵死進(jìn)程,將因?yàn)闆]有可用的進(jìn)程號(hào)而導(dǎo)致系統(tǒng)不能產(chǎn)生新的進(jìn)程. 此即為僵尸進(jìn)程的危害,應(yīng)當(dāng)避免。
孤兒進(jìn)程是沒有父進(jìn)程的進(jìn)程,孤兒進(jìn)程這個(gè)重任就落到了init進(jìn)程身上,init進(jìn)程就好像是一個(gè)民政局,專門負(fù)責(zé)處理孤兒進(jìn)程的善后工作。每當(dāng)出現(xiàn)一個(gè)孤兒進(jìn)程的時(shí)候,內(nèi)核就把孤 兒進(jìn)程的父進(jìn)程設(shè)置為init,而init進(jìn)程會(huì)循環(huán)地wait()它的已經(jīng)退出的子進(jìn)程。這樣,當(dāng)一個(gè)孤兒進(jìn)程凄涼地結(jié)束了其生命周期的時(shí)候,init進(jìn)程就會(huì)代表黨和政府出面處理它的一切善后工作。因此孤兒進(jìn)程并不會(huì)有什么危害。
任何一個(gè)子進(jìn)程(init除外)在exit()之后,并非馬上就消失掉,而是留下一個(gè)稱為僵尸進(jìn)程(Zombie)的數(shù)據(jù)結(jié)構(gòu),等待父進(jìn)程處理。這是每個(gè) 子進(jìn)程在結(jié)束時(shí)都要經(jīng)過的階段。如果子進(jìn)程在exit()之后,父進(jìn)程沒有來得及處理,這時(shí)用ps命令就能看到子進(jìn)程的狀態(tài)是“Z”。如果父進(jìn)程能及時(shí) 處理,可能用ps命令就來不及看到子進(jìn)程的僵尸狀態(tài),但這并不等于子進(jìn)程不經(jīng)過僵尸狀態(tài)。 如果父進(jìn)程在子進(jìn)程結(jié)束之前退出,則子進(jìn)程將由init接管。init將會(huì)以父進(jìn)程的身份對(duì)僵尸狀態(tài)的子進(jìn)程進(jìn)行處理。
僵尸進(jìn)程危害場(chǎng)景:
例如有個(gè)進(jìn)程,它定期的產(chǎn) 生一個(gè)子進(jìn)程,這個(gè)子進(jìn)程需要做的事情很少,做完它該做的事情之后就退出了,因此這個(gè)子進(jìn)程的生命周期很短,但是,父進(jìn)程只管生成新的子進(jìn)程,至于子進(jìn)程 退出之后的事情,則一概不聞不問,這樣,系統(tǒng)運(yùn)行上一段時(shí)間之后,系統(tǒng)中就會(huì)存在很多的僵死進(jìn)程,倘若用ps命令查看的話,就會(huì)看到很多狀態(tài)為Z的進(jìn)程。 嚴(yán)格地來說,僵死進(jìn)程并不是問題的根源,罪魁禍?zhǔn)资钱a(chǎn)生出大量僵死進(jìn)程的那個(gè)父進(jìn)程。因此,當(dāng)我們尋求如何消滅系統(tǒng)中大量的僵死進(jìn)程時(shí),答案就是把產(chǎn)生大 量僵死進(jìn)程的那個(gè)元兇槍斃掉(也就是通過kill發(fā)送SIGTERM或者SIGKILL信號(hào)啦)。槍斃了元兇進(jìn)程之后,它產(chǎn)生的僵死進(jìn)程就變成了孤兒進(jìn) 程,這些孤兒進(jìn)程會(huì)被init進(jìn)程接管,init進(jìn)程會(huì)wait()這些孤兒進(jìn)程,釋放它們占用的系統(tǒng)進(jìn)程表中的資源,這樣,這些已經(jīng)僵死的孤兒進(jìn)程 就能瞑目而去了。
3、孤兒進(jìn)程和僵尸進(jìn)程測(cè)試
孤兒進(jìn)程測(cè)試程序如下所示:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <errno.h> 4 #include <unistd.h> 5 6 int main() 7 { 8 pid_t pid; 9 //創(chuàng)建一個(gè)進(jìn)程 10 pid = fork(); 11 //創(chuàng)建失敗 12 if (pid < 0) 13 { 14 perror("fork error:"); 15 exit(1); 16 } 17 //子進(jìn)程 18 if (pid == 0) 19 { 20 printf("I am the child process.\n"); 21 //輸出進(jìn)程ID和父進(jìn)程ID 22 printf("pid: %d\tppid:%d\n",getpid(),getppid()); 23 printf("I will sleep five seconds.\n"); 24 //睡眠5s,保證父進(jìn)程先退出 25 sleep(5); 26 printf("pid: %d\tppid:%d\n",getpid(),getppid()); 27 printf("child process is exited.\n"); 28 } 29 //父進(jìn)程 30 else 31 { 32 printf("I am father process.\n"); 33 //父進(jìn)程睡眠1s,保證子進(jìn)程輸出進(jìn)程id 34 sleep(1); 35 printf("father process is exited.\n"); 36 } 37 return 0; 38 }
測(cè)試結(jié)果如下:

僵尸進(jìn)程測(cè)試程序如下所示:
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <errno.h> 4 #include <stdlib.h> 5 6 int main() 7 { 8 pid_t pid; 9 pid = fork(); 10 if (pid < 0) 11 { 12 perror("fork error:"); 13 exit(1); 14 } 15 else if (pid == 0) 16 { 17 printf("I am child process.I am exiting.\n"); 18 exit(0); 19 } 20 printf("I am father process.I will sleep two seconds\n"); 21 //等待子進(jìn)程先退出 22 sleep(2); 23 //輸出進(jìn)程信息 24 system("ps -o pid,ppid,state,tty,command"); 25 printf("father process is exiting.\n"); 26 return 0; 27 }
測(cè)試結(jié)果如下所示:

僵尸進(jìn)程測(cè)試2:父進(jìn)程循環(huán)創(chuàng)建子進(jìn)程,子進(jìn)程退出,造成多個(gè)僵尸進(jìn)程,程序如下所示:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <errno.h> 5 6 int main() 7 { 8 pid_t pid; 9 //循環(huán)創(chuàng)建子進(jìn)程 10 while(1) 11 { 12 pid = fork(); 13 if (pid < 0) 14 { 15 perror("fork error:"); 16 exit(1); 17 } 18 else if (pid == 0) 19 { 20 printf("I am a child process.\nI am exiting.\n"); 21 //子進(jìn)程退出,成為僵尸進(jìn)程 22 exit(0); 23 } 24 else 25 { 26 //父進(jìn)程休眠20s繼續(xù)創(chuàng)建子進(jìn)程 27 sleep(20); 28 continue; 29 } 30 } 31 return 0; 32 }
程序測(cè)試結(jié)果如下所示:

4、僵尸進(jìn)程解決辦法
(1)通過信號(hào)機(jī)制
子進(jìn)程退出時(shí)向父進(jìn)程發(fā)送SIGCHILD信號(hào),父進(jìn)程處理SIGCHILD信號(hào)。在信號(hào)處理函數(shù)中調(diào)用wait進(jìn)行處理僵尸進(jìn)程。測(cè)試程序如下所示:
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <errno.h> 4 #include <stdlib.h> 5 #include <signal.h> 6 7 static void sig_child(int signo); 8 9 int main() 10 { 11 pid_t pid; 12 //創(chuàng)建捕捉子進(jìn)程退出信號(hào) 13 signal(SIGCHLD,sig_child); 14 pid = fork(); 15 if (pid < 0) 16 { 17 perror("fork error:"); 18 exit(1); 19 } 20 else if (pid == 0) 21 { 22 printf("I am child process,pid id %d.I am exiting.\n",getpid()); 23 exit(0); 24 } 25 printf("I am father process.I will sleep two seconds\n"); 26 //等待子進(jìn)程先退出 27 sleep(2); 28 //輸出進(jìn)程信息 29 system("ps -o pid,ppid,state,tty,command"); 30 printf("father process is exiting.\n"); 31 return 0; 32 } 33 34 static void sig_child(int signo) 35 { 36 pid_t pid; 37 int stat; 38 //處理僵尸進(jìn)程 39 while ((pid = waitpid(-1, &stat, WNOHANG)) >0) 40 printf("child %d terminated.\n", pid); 41 }
測(cè)試結(jié)果如下所示:

(2)fork兩次
《Unix 環(huán)境高級(jí)編程》8.6節(jié)說的非常詳細(xì)。原理是將子進(jìn)程成為孤兒進(jìn)程,從而其的父進(jìn)程變?yōu)閕nit進(jìn)程,通過init進(jìn)程可以處理僵尸進(jìn)程。測(cè)試程序如下所示:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <errno.h> 5 6 int main() 7 { 8 pid_t pid; 9 //創(chuàng)建第一個(gè)子進(jìn)程 10 pid = fork(); 11 if (pid < 0) 12 { 13 perror("fork error:"); 14 exit(1); 15 } 16 //第一個(gè)子進(jìn)程 17 else if (pid == 0) 18 { 19 //子進(jìn)程再創(chuàng)建子進(jìn)程 20 printf("I am the first child process.pid:%d\tppid:%d\n",getpid(),getppid()); 21 pid = fork(); 22 if (pid < 0) 23 { 24 perror("fork error:"); 25 exit(1); 26 } 27 //第一個(gè)子進(jìn)程退出 28 else if (pid >0) 29 { 30 printf("first procee is exited.\n"); 31 exit(0); 32 } 33 //第二個(gè)子進(jìn)程 34 //睡眠3s保證第一個(gè)子進(jìn)程退出,這樣第二個(gè)子進(jìn)程的父親就是init進(jìn)程里 35 sleep(3); 36 printf("I am the second child process.pid: %d\tppid:%d\n",getpid(),getppid()); 37 exit(0); 38 } 39 //父進(jìn)程處理第一個(gè)子進(jìn)程退出 40 if (waitpid(pid, NULL, 0) != pid) 41 { 42 perror("waitepid error:"); 43 exit(1); 44 } 45 exit(0); 46 return 0; 47 }
測(cè)試結(jié)果如下圖所示:

5、參考資料
《unix環(huán)境高級(jí)編程》第八章
http://www.rosoo.net/a/201109/15071.html
http://blog.chinaunix.net/uid-1829236-id-3166986.html
http://forkhope.diandian.com/post/2012-10-01/40040574200
http://blog.csdn.net/metasearch/article/details/2498853
http://blog.csdn.net/yuwenliang/article/details/6770750