摘要:孤兒進程與殭屍進程是操作系統中常見的兩個概念,對於我們學習操作系統十分重要,第一部分就兩者基本概念展開敘述,通過生動的比喻加深我們的理解,第二部分主要討論兩者的危害性,在第三部分筆者通過C/C++代碼分析產生的原因,最後提出了兩種解決方案。本文通俗易懂,風格幽默,令我受益匪淺,故分享給大家! —— 轉者
(一)基本概念
我們知道在Unix/Linux中,正常情況下,子進程是通過父進程創建的,子進程再創建新的進程。子進程的結束和父進程的運行是一個異步過程,即父進程永遠無法預測子進程到底什麼時候結束。當一個進程完成它的工作終止之後,它的父進程需要調用wait()或者waitpid()系統調用取得子進程的終止狀態。
-
孤兒進程
一個父進程退出,而它的一個或多個子進程還在運行,那麼那些子進程將成爲孤兒進程。孤兒進程將被init進程(進程號爲1)所收養,並由init進程對它們完成狀態收集工作。 -
殭屍進程
一個進程使用fork創建子進程,如果子進程退出,而父進程並沒有調用wait或waitpid獲取子進程的狀態信息,那麼子進程的進程描述符仍然保存在系統中。這種進程稱之爲殭屍進程。
(二)問題及危害
(下面一段描述很長,不過轉者建議大家讀完,很有意思,原作者筆風很幽默)
Unix提供了一種機制可以保證只要父進程想知道子進程結束時的狀態信息,就可以得到。
這種機制就是: 在每個進程退出的時候,內核釋放該進程所有的資源,包括打開的文件,佔用的內存等。 但是仍然爲其保留一定的信息,包括進程號(the process ID),退出狀態(the termination status of the process),運行時間(the amount of CPU time taken by the process)等。直到父進程通過wait () / waitpid()來取時才釋放。
但這樣就導致了問題,如果進程不調用wait() / waitpid()的話, 那麼保留的那段信息就不會釋放,其進程號就會一直被佔用,但是系統所能使用的進程號是有限的,如果大量的產生殭屍進程,將因爲沒有可用的進程號而導致系統不能產生新的進程。此即爲殭屍進程的危害,應當避免。
孤兒進程是沒有父進程的進程,孤兒進程這個重任就落到了init進程身上,init進程就好像是一個民政局,專門負責處理孤兒進程的善後工作。每當出現一個孤兒進程的時候,內核就把孤兒進程的父進程設置爲init,而init進程會循環地wait()結束的子進程。這樣,當一個孤兒進程淒涼地結束了其生命週期的時候,init進程就會代表黨和政府出面處理它的一切善後工作。因此孤兒進程並不會有什麼危害。
(這段話轉者笑哭,真生動~init 收養孤兒進程並且調用 wait() 防止其變成殭屍進程,所以孤兒進程不會產生危害)
任何一個子進程(init除外)在exit()之後,並非馬上就消失掉,而是留下一個稱爲殭屍進程(Zombie)的數據結構,等待父進程處理。這是每個子進程在結束時都要經過的階段。
如果子進程在exit()之後,父進程沒有來得及處理,這時用ps命令就能看到子進程的狀態是“Z”。如果父進程能及時處理,可能用ps命令就來不及看到子進程的殭屍狀態,但這並不等於子進程不經過殭屍狀態。如果父進程在子進程結束之前退出,則子進程將由init接管,init將會以父進程的身份對殭屍狀態的子進程進行處理。
假如有個進程,它定期的產生一個子進程,這個子進程需要做的事情很少,做完它該做的事情之後就退出了,因此這個子進程的生命週期很短,但是,父進程只管生成新的子進程,至於子進程退出之後的事情,則一概不聞不問。這樣,系統運行上一段時間之後,系統中就會存在很多的殭屍進程,倘若用ps命令查看的話,就會看到很多狀態爲Z的進程。
嚴格地來說,殭屍進程並不是問題的根源,罪魁禍首是產生出大量殭屍進程的那個父進程。因此,當我們尋求如何消滅系統中大量的殭屍進程時,答案就是把產生大量殭屍進程的那個元兇槍斃掉(也就是通過kill發送SIGTERM或者SIGKILL信號啦)。槍斃了元兇進程之後,它產生的殭屍進程就變成了孤兒進程,這些孤兒進程會被init進程接管,init進程會wait()這些孤兒進程,釋放它們佔用的系統進程表中的資源,這樣,這些已經殭屍的孤兒進程就能瞑目而去了。(這段描述真的殘暴~)
(三)孤兒進程和殭屍進程測試
- 孤兒進程測試程序
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
int main(){
pid_t pid;
pid = fork();
if (pid < 0){
perror("fork error:");
exit(1);
}
//子進程
if (pid == 0){
printf("I am the child process.\n");
//輸出進程ID和父進程ID
printf("pid: %d\tppid:%d\n",getpid(),getppid());
printf("I will sleep five seconds.\n");
//睡眠5s,保證父進程先退出
sleep(5);
printf("pid: %d\tppid:%d\n",getpid(),getppid());
printf("child process is exited.\n");
} else { //父進程
printf("I am father process.\n");
//父進程睡眠1s,保證子進程輸出進程id
sleep(1);
printf("father process is exited.\n");
}
return 0;
}
測試結果如下:
- 殭屍進程測試程序
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
int main(){
pid_t pid;
pid = fork();
if (pid < 0){
perror("fork error:");
exit(1);
}else if (pid == 0){
printf("I am child process.I am exiting.\n");
exit(0);
}
printf("I am father process.I will sleep two seconds\n");
//等待子進程先退出
sleep(2);
//輸出進程信息,此時子進程先退出,但是父進程還沒有調用wait
system("ps -o pid,ppid,state,tty,command");
printf("father process is exiting.\n");
return 0;
}
測試結果如下所示:
- 殭屍進程測試2:父進程循環創建子進程,子進程退出,造成多個殭屍進程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
int main(){
pid_t pid;
//循環創建子進程
while(1){
pid = fork();
if (pid < 0){
perror("fork error:");
exit(1);
}else if (pid == 0){
printf("I am a child process.\nI am exiting.\n");
//子進程退出,成爲殭屍進程
exit(0);
}else{
//父進程
//休眠20s繼續循環創建子進程
sleep(20);
continue;
}
}
return 0;
}
程序測試結果如下所示:
(四)殭屍進程解決辦法
- 通過信號機制
子進程退出時向父進程發送SIGCHILD信號,父進程處理SIGCHILD信號。在信號處理函數中調用wait進行處理殭屍進程。測試程序如下所示:
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <signal.h>
static void sig_child(int signo);
int main(){
pid_t pid;
//創建捕捉子進程退出信號
signal(SIGCHLD,sig_child);
pid = fork();
if (pid < 0){
perror("fork error:");
exit(1);
}else if (pid == 0){
printf("I am child process,pid id %d.I am exiting.\n",getpid());
exit(0);
}
printf("I am father process.I will sleep two seconds\n");
//等待子進程先退出
sleep(2);
//輸出進程信息
system("ps -o pid,ppid,state,tty,command");
printf("father process is exiting.\n");
return 0;
}
static void sig_child(int signo){
pid_t pid;
int stat;
//處理殭屍進程
while ((pid = waitpid(-1, &stat, WNOHANG)) >0)
printf("child %d terminated.\n", pid);
}
測試結果如下所示:
- fork兩次
《Unix 環境高級編程》8.6節說的非常詳細。原理是將子進程成爲孤兒進程,從而其的父進程變爲init進程,通過init進程可以處理殭屍進程。測試程序如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
int main(){
pid_t pid;
//創建第一個子進程
pid = fork();
if (pid < 0){
perror("fork error:");
exit(1);
}else if (pid == 0){
//第一個子進程
//子進程再創建子進程
printf("I am the first child process.pid:%d\tppid:%d\n",getpid(),getppid());
pid = fork();
if (pid < 0){
perror("fork error:");
exit(1);
}else if (pid > 0){
//第一個子進程退出
printf("first procee is exited.\n");
exit(0);
}
//第二個子進程
//睡眠3s保證第一個子進程退出,這樣第二個子進程的父親就是init進程裏
sleep(3);
printf("I am the second child process.pid: %d\tppid:%d\n",getpid(),getppid());
exit(0);
}
//父進程處理第一個子進程退出,
if (waitpid(pid, NULL, 0) != pid){
perror("waitepid error:");
exit(1);
}
exit(0);
return 0;
}
測試結果如下圖所示:
參考資料
《unix環境高級編程》第八章
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
轉載結束,歡迎留言討論。本文轉載自孤兒進程與殭屍進程[總結]。