1. 程式人生 > >linux下殭屍程序(Defunct程序)的產生與避免

linux下殭屍程序(Defunct程序)的產生與避免

在測試基於 DirectFB+Gstreamer 的視訊聯播系統的一個 Demo 的時候,其中大量使用 system 呼叫的語句,例如在 menu 程式碼中的 system("./play") ,而且多次執行,這種情況下,在 ps -ef 列表中出現了大量的 defunct 程序,對程式的執行時有害的。按說system的原始碼中應該已經包含了wait,但也不能排除開發板上這個版本的system中可能沒有wait,總之,開發板上在呼叫system後新增wait之後,defunct程序不復存在了。

下面談談 defunct 程序,中文翻譯叫殭屍程序。下文整理於網路以及APUE一書。

 

一、什麼是殭屍程序

 

在UNIX 系統中,一個程序結束了,但是他的父程序沒有等待(呼叫wait / waitpid)他,那麼他將變成一個殭屍程序。當用ps命令觀察程序的執行狀態時,看到這些程序的狀態列為defunct。殭屍程序是一個早已死亡的程序,但在程序表(processs table)中仍佔了一個位置(slot)。

 

但是如果該程序的父程序已經先結束了,那麼該程序就不會變成殭屍程序。因為每個程序結束的時候,系統都會掃描當前系統中所執行的所有程序,看看有沒有哪個程序是剛剛結束的這個程序的子程序,如果是的話,就由Init程序來接管他,成為他的父程序,從而保證每個程序都會有一個父程序。而Init程序會自動wait其子程序,因此被Init接管的所有程序都不會變成殭屍程序。

 

二、UNIX下程序的運作方式

 

每個Unix程序在程序表裡都有一個進入點(entry),核心程序執行該程序時使用到的一切資訊都儲存在進入點。當用 ps 命令察看系統中的程序資訊時,看到的就是程序表中的相關資料。當以fork()系統呼叫建立一個新的程序後,核心程序就會在程序表中給這個新程序分配一個進入點,然後將相關資訊儲存在該進入點所對應的程序表內。這些資訊中有一項是其父程序的識別碼。

 

子程序的結束和父程序的執行是一個非同步過程,即父程序永遠無法預測子程序到底什麼時候結束。那麼會不會因為父程序太忙來不及 wait 子程序,或者說不知道子程序什麼時候結束,而丟失子程序結束時的狀態資訊呢?

 

不會。因為UNIX提供了一種機制可以保證,只要父程序想知道子程序結束時的狀態資訊,就可以得到。這種機制就是:當子程序走完了自己的生命週期後,它會執行exit()系統呼叫,核心釋放該程序所有的資源,包括開啟的檔案,佔用的記憶體等。但是仍然為其保留一定的資訊(包括程序號the process ID,退出碼exit code,退出狀態the terminationstatus of the process,執行時間the amount of CPU time taken by the process等),這些資料會一直保留到系統將它傳遞給它的父程序為止,直到父程序通過wait / waitpid來取時才釋放。

 

也就是說,當一個程序死亡時,它並不是完全的消失了。程序終止,它不再執行,但是還有一些殘留的資料等待父程序收回。當父程序 fork() 一個子程序後,它必須用 wait() (或者 waitpid())等待子程序退出。正是這個 wait() 動作來讓子程序的殘留資料消失。

 

三、殭屍程序的危害

 

如果父程序不呼叫wait / waitpid的話,那麼保留的那段資訊就不會釋放,其程序號就會一直被佔用,但是系統的程序表容量是有限的,所能使用的程序號也是有限的,如果大量的產生殭屍程序,將因為沒有可用的程序號而導致系統不能產生新的程序。


所以,defunct程序不僅佔用系統的記憶體資源,影響系統的效能,而且如果其數目太多,還會導致系統癱瘓。而且,由於排程程式無法選中Defunct 程序,所以不能用kill命令刪除Defunct 程序,惟一的方法只有重啟系統。

 

四、殭屍程序的產生

 

如果子程序死亡時父程序沒有 wait(),通常用 ps 可以看到它被顯示為“<defunct>”,這樣就產生了殭屍程序。它將永遠保持這樣直到父程序 wait()。

 

由此可見,defunct程序的出現時間是在子程序終止後,但是父程序尚未讀取這些資料之前。利用這一點我們可以用下面的程式建立一個defunct 程序:

 

C程式碼  收藏程式碼

  1. #include <stdio.h>  
  2.   
  3. #include<sys/types.h>  
  4.   
  5. main()  
  6. {  
  7.   
  8.     if(!fork())  
  9.     {  
  10.   
  11.         printf(“child pid=%d\n”, getpid());  
  12.   
  13.         exit(0);  
  14.   
  15.     }  
  16.   
  17.     sleep(20);  
  18.   
  19.     printf(“parent pid=%d \n”, getpid());  
  20.   
  21.     exit(0);  
  22.   
  23. }  

 

當上述程式以後臺的方式執行時,第17行強迫程式睡眠20秒,讓使用者有時間輸入ps -e指令,觀察程序的狀態,我們看到程序表中出現了defunct程序。當父程序執行終止後,再用ps -e命令觀察時,我們會發現defunct程序也隨之消失。這是因為父程序終止後,init 程序會接管父程序留下的這些“孤兒程序”(orphan process),而這些“孤兒程序”執行完後,它在程序表中的進入點將被刪除。如果一個程式設計上有缺陷,就可能導致某個程序的父程序一直處於睡眠狀態或是陷入死迴圈,父程序沒有wait子程序,也沒有終止以使Init接管,該子程序執行結束後就變成了defunct程序,這個defunct 程序可能會一直留在系統中直到系統重新啟動。

 

 

在看一個產生殭屍程序的例子。

子程序要執行的程式test_prog

 

 

C程式碼  收藏程式碼

  1. //test.c  
  2. #include <stdio.h>  
  3. int main()  
  4. {  
  5.         int i = 0;  
  6.         for (i = 0 ; i < 10; i++)  
  7.         {  
  8.                 printf ("child time %d\n", i+1);  
  9.                 sleep (1);  
  10.         }  
  11.         return 0;  
  12. }  

 

 

 

父程序father的程式碼father.c

 

 

C程式碼  收藏程式碼

  1. #include <stdio.h>  
  2. #include <unistd.h>  
  3. #include <sys/types.h>  
  4. #include <sys/wait.h>  
  5. int main()  
  6. {  
  7.         int pid = fork ();  
  8.         if (pid == 0)  
  9.         {  
  10.                 system ("./test_prog");  
  11.                 _exit (0);  
  12.         }else  
  13.         {  
  14.                 int i = 0;  
  15.                 /* 
  16.                                 int status = 0; 
  17.                 while (!waitpid(pid, &status, WNOHANG)) 
  18.                 { 
  19.                         printf ("father waiting%d\n", ++i); 
  20.                         sleep (1); 
  21.                 }*/  
  22.                 while (1)  
  23.                 {  
  24.                         printf ("father waiting over%d\n", ++i);  
  25.                         sleep (1);  
  26.                 }  
  27.                 return 0;  
  28.         }  
  29.   
  30. }  

 

 

 

執行./father,當子程序退出後,由於父程序沒有對它的退出進行關注,會出現殭屍程序

 

 

C程式碼  收藏程式碼

  1. 20786 pts/0    00:00:00 father  
  2. 20787 pts/0    00:00:00 father <defunct>  

 

    總結:子程序成為 defunct 直到父程序 wait(),除非父程序忽略了 SIGCLD 。更進一步,父程序沒有 wait() 就消亡(仍假設父程序沒有忽略 SIGCLD )的子程序(活動的或者 defunct)成為 init 的子程序,init 著手處理它們。

 

五、如何避免殭屍程序

 

1、父程序通過wait和waitpid等函式等待子程序結束,這會導致父程序掛起。

 

在上個例子中,如果我們略作修改,在第8行sleep()系統呼叫前執行wait()或waitpid()系統呼叫,則子程序在終止後會立即把它在程序表中的資料返回給父程序,此時系統會立即刪除該進入點。在這種情形下就不會產生defunct程序。


2. 如果父程序很忙,那麼可以用signal函式為SIGCHLD安裝handler。在子程序結束後,父程序會收到該訊號,可以在handler中呼叫wait回收。

 

3. 如果父程序不關心子程序什麼時候結束,那麼可以用signal(SIGCLD, SIG_IGN)或signal(SIGCHLD, SIG_IGN)通知核心,自己對子程序的結束不感興趣,那麼子程序結束後,核心會回收,並不再給父程序傳送訊號 


4. fork兩次,父程序fork一個子程序,然後繼續工作,子程序fork一個孫程序後退出,那麼孫程序被init接管,孫程序結束後,init會回收。不過子程序的回收還要自己做。 下面就是Stevens給的採用兩次folk避免殭屍程序的示例:

 

 

C程式碼  收藏程式碼

  1. #include "apue.h"  
  2. #include <sys/wait.h>  
  3.   
  4. int  
  5. main(void)  
  6. ...{  
  7.      pid_t    pid;  
  8.   
  9.     if ((pid = fork()) < 0) ...{  
  10.          err_sys("fork error");  
  11.      } else if (pid == 0) ...{     /**//* first child */  
  12.         if ((pid = fork()) < 0)  
  13.              err_sys("fork error");  
  14.         else if (pid > 0)  
  15.              exit(0);    /**//* parent from second fork == first child */  
  16.         /**//* 
  17.           * We're the second child; our parent becomes init as soon 
  18.           * as our real parent calls exit() in the statement above. 
  19.           * Here's where we'd continue executing, knowing that when 
  20.           * we're done, init will reap our status. 
  21.          */  
  22.          sleep(2);  
  23.          printf("second child, parent pid = %d ", getppid());  
  24.          exit(0);  
  25.      }  
  26.       
  27.     if (waitpid(pid, NULL, 0) != pid)  /**//* wait for first child */  
  28.          err_sys("waitpid error");  
  29.   
  30.     /**//* 
  31.       * We're the parent (the original process); we continue executing, 
  32.       * knowing that we're not the parent of the second child. 
  33.      */  
  34.      exit(0);  
  35. }