Linux殭屍程式處置

大雄45發表於2020-10-22
導讀 一般情況下,程式呼叫exit(包括_exit和_Exit,它們的區別這裡不做解釋),它的絕大多數記憶體和相關的資源已經被核心釋放掉,但是在程式表中這個程式項(entry)還保留著(程式ID,退出狀態,佔用的資源等等)
一、什麼是僵死程式?

一般情況下,程式呼叫exit(包括_exit和_Exit,它們的區別這裡不做解釋),它的絕大多數記憶體和相關的資源已經被核心釋放掉,但是在程式表中這個程式項(entry)還保留著(程式ID,退出狀態,佔用的資源等等),你可能會問,為什麼這麼麻煩,直接釋放完資源不就行了嗎?這是因為有時它的父程式想了解它的退出狀態。在子程式退出但還未被其父程式“收屍”之前,該子程式就是僵死程式,或者殭屍程式。如果父程式先於子程式去世,那麼子程式將被init程式收養,這個時候init就是這個子程式的父程式。

所以一旦出現父程式長期執行,而又沒有顯示呼叫wait或者waitpid,同時也沒有處理SIGCHLD訊號,這個時候init程式就沒有辦法來替子程式收屍,這個時候,子程式就真的成了“殭屍”了。

二、僵死程式與孤兒程式的區別?

回答這個問題很簡單,就是爸爸(父程式)和兒子(子程式)誰先死的問題!

如果當兒子還在世的時候,爸爸去世了,那麼兒子就成孤兒了,這個時候兒子就會被init收養,換句話說,init程式充當了兒子的爸爸,所以等到兒子去世的時候,就由init程式來為其收屍。

如果當爸爸還活著的時候,兒子死了,這個時候如果爸爸不給兒子收屍,那麼兒子就會變成殭屍程式。

三、僵死程式的危害?
  1. 僵死程式的PID還佔據著,意味著海量的子程式會佔據滿程式表項,會使後來的程式無法fork.
  1. 僵死程式的核心棧無法被釋放掉(1K 或者 2K大小),為啥會留著它的核心棧,因為在棧的最低端,有著thread_info結構,它包含著 struct_task 結構,這裡麵包含著一些退出資訊。
四、避免僵死程式的方法

網上搜了下,總結有三種方方法:

  1. ① 程式中顯示的呼叫signal(SIGCHLD, SIG_IGN)來忽略SIGCHLD訊號,這樣子程式結束後,由核心來wai和釋放資源
  1. ② fork兩次,第一次fork的子程式在fork完成後直接退出,這樣第二次fork得到的子程式就沒有爸爸了,它會自動被老祖宗init收養,init會負責釋放它的資源,這樣就不會有“殭屍”產生了
  1. ③ 對子程式進行wait,釋放它們的資源,但是父程式一般沒工夫在那裡守著,等著子程式的退出,所以,一般使用訊號的方式來處理,在收到SIGCHLD訊號的時候,在訊號處理函式中呼叫wait操作來釋放他們的資源。
五、對每個避免僵死程式方法的解析與總結

首先我們讓我們來看一個生成殭屍程式的程式zombie.c如下:

#include    
#include   #include     int main(int argc, const char *argv[])   
{      int i;   
    pid_t pid;        for (i = 0; i < 10; i++) {   
        if ((pid = fork()) == 0)    /* child */   
            _exit(0);   
    }      sleep(10);   
      exit(EXIT_SUCCESS);  }

執行程式,在10s睡眠期間使用ps檢視程式,你會發現有10個標記為“defunct”的殭屍程式:

Linux殭屍程式處置Linux殭屍程式處置

接下來看第一種方法,程式avoid_zombie1.c如下:

#include    
#include   #include   #include   #include     int main(int argc, const char *argv[])   
{      pid_t pid;        if (SIG_ERR == signal(SIGCHLD, SIG_IGN)) {   
        perror("signal error");   
        _exit(EXIT_FAILURE);      }        while (1) {   
        if ((pid = fork()) == 0)    /* child */   
            _exit(0);   
    }        exit(EXIT_SUCCESS);  }

程式執行期間透過ps 的確沒有發現殭屍程式的存在。

在man文件中有這段話:

Note that even though the default disposition of SIGCHLD is "ignore", explicitly setting the disposition to SIG_IGN results in different treatment of zombie process children.

意思是說盡管系統對訊號SIGCHLD的預設處理就是“ignore”,但是顯示的設定成SIG_IGN的處理方式在在這裡會表現不同的處理方式(即子程式結束後,資源由系統自動收回,所以不會產生殭屍程式),這是訊號SIGCHLD與其他訊號的不同之處。

在man文件中同樣有這樣一段話:

The original POSIX standard left the behavior of setting SIGCHLD to SIG_IGN unspecified. 看來這個方法不是每個平臺都使用,尤其在一些老的系統中,相容性不是很好,所以如果你在寫一個可移植的程式的話,不推薦使用這個方法。

第二種方法,即透過兩次fork來避免殭屍程式,我們來看一個例子avoid_zombie2.c:

#include    
#include   #include   #include   #include     int main(int argc, const char *argv[])   
{      pid_t pid;        while (1) {   
        if ((pid = fork()) == 0) {  /* child */   
            if ((pid = fork()) > 0)   
                _exit(0);   
            sleep(1);   
            printf("grandchild, parent id = %ld\n",   
                            (long)getppid());              _exit(0);   
        }          if (waitpid(-1, NULL, 0) != pid) {   
            perror("waitpid error");   
            _exit(EXIT_FAILURE);          }      }        exit(EXIT_SUCCESS);  }

這的確是個有效的辦法,但是我想這個方法不適宜網路併發伺服器中,應為fork的效率是不高的。

最後來看第三種方法, 也是最通用的方法

先看我們的測試程式avoid_zombie3.c

#include    
#include   #include   #include    
#include   #include   #include   #include <sys/wait.h>  #include <sys/types.h>      void avoid_zombies_handler(int signo)   
{      pid_t pid;      int exit_status;   
    int saved_errno = errno;   
      while ((pid = waitpid(-1, &exit_status, WNOHANG)) > 0) {   
        /* do nothing */   
    }        errno = saved_errno;  }    int main(int argc, char *argv[])   
{      pid_t pid;      int status;   
    struct sigaction child_act;    
      memset(&child_act, 0, sizeof(struct sigaction));   
    child_act.sa_handler = avoid_zombies_handler;      child_act.sa_flags = SA_RESTART | SA_NOCLDSTOP;       sigemptyset(&child_act.sa_mask);      if (sigaction(SIGCHLD, &child_act, NULL) == -1) {   
        perror("sigaction error");   
        _exit(EXIT_FAILURE);      }        while (1) {   
        if ((pid = fork()) == 0) {  /* child process */   
            _exit(0);   
        } else if (pid > 0) {        /* parent process */   
        }      }            _exit(EXIT_SUCCESS);  }

首先需要知道三點:

  1. 1. 當某個訊號的訊號處理函式被呼叫時,該訊號會被作業系統阻塞(預設sa_flags不設定SA_NODEFER標誌)。
  1. 2.當某個訊號的訊號處理函式被呼叫時,該訊號阻塞時,該訊號又多次發生,那麼作業系統並不將它們排隊,而是隻保留第一次的,後續的被拋棄。
  1. 3. wait系列函式與訊號SIGCHLD是沒有任何關係的,即wait系列函式並不是訊號SIGCHLD驅動的。

這個時候,肯定有人有疑問了,既然會丟棄訊號,那怎麼保證可以收回所有的殭屍程式呢?

關於這個問題,我們可以這樣來理解,當子程式結束時,不管有沒有產生SIGCHLD訊號,或者子程式產生了SIGCHLD訊號,而不管父程式有沒有收到SIGCHLD訊號,這都與子程式已經終止這個事實無關,就是說,子程式終止與訊號其實沒有任何關係,只是作業系統在子程式終止時會傳送訊號SIGCHLD給父程式,告之其子程式終止的訊息,這樣的話,父程式就可以做相應的操作了。而wait系列函式的目的就是收回子程式終止時殘留在程式列表中的資訊,所以任何時候呼叫while ((pid = waitpid(-1, &exit_status, WNOHANG)) > 0)都可以收回所有的殭屍程式資訊(可以參考下面的程式)。但是這裡為什麼放在訊號處理函式中處理了,這樣做的原因是:子程式什麼時候結束是個非同步事件,而訊號機制就是用來處理非同步事件的,所以當子程式結束時,可以迅速的收回其殘餘資訊,這樣系統中就不會積累大量的殭屍程式了。

也可以這樣來理解:系統把所有的殭屍程式串在一起形成一個殭屍程式連結串列,而while ((pid = waitpid(-1, &exit_status, WNOHANG)) > 0)就是來清空這個連結串列的,直到waitpid()返回0,表明已經沒有殭屍程式了,或者返回-1,表明出錯(當錯誤碼errno為ECHILD的時候同樣表明已經不存在殭屍程式了)。

瞭解了以上知識點,就能理解為什麼while ((pid = waitpid(-1, &exit_status, WNOHANG)) > 0)能夠回收所有的殭屍程式了。

我們可以在上面的訊號處理函式中加入相應的列印資訊:

static int num1 = 0   
static int num2 = 0;   
void avoid_zombies_handler(int signo)   
{      pid_t pid;   
    int exit_status;   
    int saved_errno = errno;   
      printf("num1 = %d\n", ++num1);   
    while ((pid = waitpid(-1, &exit_status, WNOHANG)) > 0) {   
        printf("num2 = %d\n", ++num2);   
    }        errno = saved_errno;  }

列印的結果你會發現,當num1遞增1的時候,即每呼叫一次訊號處理函式,num2一般會遞增很多,即while迴圈了很多次,所以儘管有的SIGCHLD訊號被丟棄了,但是我們不用擔心子程式的殘餘資訊會收不回來。退出while迴圈時,證明此時系統中已經沒有殭屍程式了,所以退出訊號處理函式後,阻塞的唯一SIGCHLD訊號會再次觸發該訊號處理函式,這樣我們就不用擔心了。我們不防做個最壞的打算,即之前的訊號全部被丟棄了,只有最後一次的SIGCHLD訊號被捕獲,從而觸發了訊號處理函式,這樣我們也不用擔心,因為while迴圈會一次性收回全部的殭屍程式資訊,只是這次迴圈的次數要多得多罷了,當然這只是假設,一般系統不會出現這樣的情況(可以參考本文最後一個程式事例)。

為了證明wait系統函式與訊號SIGCHLD沒有任何關係,我們可以做個簡單的實驗,程式碼如下:

#include    
#include    
#include    
#include <sys/wait.h>   
#include <sys/types.h>   
  int main(int argc, char *argv[])  {      int i;      pid_t pid;        for (i = 0; i < 5; i++) { if ((pid = fork()) == 0) /* child */ _exit(0); } sleep(10); while (waitpid(-1, NULL, WNOHANG) > 0) {   
        /* do nothing */      }      sleep(10);        _exit(EXIT_SUCCESS);  }

以下是列印結果:

Linux殭屍程式處置Linux殭屍程式處置

可以看到第一次sleep時系統中積累了5個殭屍程式,第二次sleep時,那5個殭屍程式都被收回了。這個也明顯的看到了使用訊號處理函式的優勢,即可以保證系統不會積累大量的殭屍程式,它可以迅速的清理掉系統中的殭屍程式。

原文來自:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69955379/viewspace-2728680/,如需轉載,請註明出處,否則將追究法律責任。

相關文章