溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Linux中如何處理僵尸進程

發(fā)布時間:2022-02-18 09:48:01 來源:億速云 閱讀:150 作者:小新 欄目:開發(fā)技術

這篇文章將為大家詳細講解有關Linux中如何處理僵尸進程,小編覺得挺實用的,因此分享給大家做個參考,希望大家閱讀完這篇文章后可以有所收獲。

僵尸進程(Zombie process)通俗來說指那些雖然已經(jīng)終止的進程,但仍然保留一些信息,等待其父進程為其收尸。也就是說父進程沒有結束,但是子進程結束了,父進程沒死,沒辦法給子進程收尸,真的是只有父進程死了才能收尸。

Linux中如何處理僵尸進程

一、什么是僵死進程?

一般情況下,程序調用exit(包括exit和Exit,它們的區(qū)別這里不做解釋),它的絕大多數(shù)內存和相關的資源已經(jīng)被內核釋放掉,但是在進程表中這個進程項(entry)還保留著(進程ID,退出狀態(tài),占用的資源等等),你可能會問,為什么這么麻煩,直接釋放完資源不就行了嗎?這是因為有時它的父進程想了解它的退出狀態(tài)。在子進程退出但還未被其父進程“收尸”之前,該子進程就是僵死進程,或者僵尸進程。如果父進程先于子進程去世,那么子進程將被init進程收養(yǎng),這個時候init就是這個子進程的父進程。

所以一旦出現(xiàn)父進程長期運行,而又沒有顯示調用wait或者waitpid,同時也沒有處理SIGCHLD信號,這個時候init進程就沒有辦法來替子進程收尸,這個時候,子進程就真的成了“僵尸”了。

二、僵死進程與孤兒進程的區(qū)別?

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

如果當兒子還在世的時候,爸爸去世了,那么兒子就成孤兒了,這個時候兒子就會被init收養(yǎng),換句話說,init進程充當了兒子的爸爸,所以等到兒子去世的時候,就由init進程來為其收尸。

如果當爸爸還活著的時候,兒子死了,這個時候如果爸爸不給兒子收尸,那么兒子就會變成僵尸進程。

三、僵死進程的危害?

僵死進程的PID還占據(jù)著,意味著海量的子進程會占據(jù)滿進程表項,會使后來的進程無法fork.

僵死進程的內核棧無法被釋放掉(1K 或者 2K大?。瑸樯稌糁膬群藯?,因為在棧的最低端,有著thread_info結構,它包含著 struct_task 結構,這里面包含著一些退出信息。

四、避免僵死進程的方法

網(wǎng)上搜了下,總結有三種方方法:

① 程序中顯示的調用signal(SIGCHLD, SIG_IGN)來忽略SIGCHLD信號,這樣子進程結束后,由內核來wai和釋放資源

② fork兩次,第一次fork的子進程在fork完成后直接退出,這樣第二次fork得到的子進程就沒有爸爸了,它會自動被老祖宗init收養(yǎng),init會負責釋放它的資源,這樣就不會有“僵尸”產(chǎn)生了

③ 對子進程進行wait,釋放它們的資源,但是父進程一般沒工夫在那里守著,等著子進程的退出,所以,一般使用信號的方式來處理,在收到SIGCHLD信號的時候,在信號處理函數(shù)中調用wait操作來釋放他們的資源。

五、對每個避免僵死進程方法的解析與總結

首先我們讓我們來看一個生成僵尸進程的程序zombie.c如下:

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

運行程序,在10s睡眠期間使用ps查看進程,你會發(fā)現(xiàn)有10個標記為“defunct”的僵尸進程:

Linux中如何處理僵尸進程
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命令的確沒有發(fā)現(xiàn)僵尸進程的存在。

在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.

意思是說盡管系統(tǒng)對信號SIGCHLD的默認處理就是“ignore”,但是顯示的設置成SIG_IGN的處理方式在在這里會表現(xiàn)不同的處理方式(即子進程結束后,資源由系統(tǒng)自動收回,所以不會產(chǎn)生僵尸進程),這是信號SIGCHLD與其他信號的不同之處。

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

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

第二種方法,即通過兩次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);  }

這的確是個有效的辦法,但是我想這個方法不適宜網(wǎng)絡并發(fā)服務器中,應為fork的效率是不高的。

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

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

#include    #include   #include   #include    #include   #include   #include   #include   #include       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. 當某個信號的信號處理函數(shù)被調用時,該信號會被操作系統(tǒng)阻塞(默認sa_flags不設置SA_NODEFER標志)。

2.當某個信號的信號處理函數(shù)被調用時,該信號阻塞時,該信號又多次發(fā)生,那么操作系統(tǒng)并不將它們排隊,而是只保留第一次的,后續(xù)的被拋棄。

\3. wait系列函數(shù)與信號SIGCHLD是沒有任何關系的,即wait系列函數(shù)并不是信號SIGCHLD驅動的。

這個時候,肯定有人有疑問了,既然會丟棄信號,那怎么保證可以收回所有的僵尸進程呢?

關于這個問題,我們可以這樣來理解,當子進程結束時,不管有沒有產(chǎn)生SIGCHLD信號,或者子進程產(chǎn)生了SIGCHLD信號,而不管父進程有沒有收到SIGCHLD信號,這都與子進程已經(jīng)終止這個事實無關,就是說,子進程終止與信號其實沒有任何關系,只是操作系統(tǒng)在子進程終止時會發(fā)送信號SIGCHLD給父進程,告之其子進程終止的消息,這樣的話,父進程就可以做相應的操作了。而wait系列函數(shù)的目的就是收回子進程終止時殘留在進程列表中的信息,所以任何時候調用while ((pid = waitpid(-1, &exit_status, WNOHANG)) > 0)都可以收回所有的僵尸進程信息(可以參考下面的程序)。但是這里為什么放在信號處理函數(shù)中處理了,這樣做的原因是:子進程什么時候結束是個異步事件,而信號機制就是用來處理異步事件的,所以當子進程結束時,可以迅速的收回其殘余信息,這樣系統(tǒng)中就不會積累大量的僵尸進程了。

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

了解了以上知識點,就能理解為什么while ((pid = waitpid(-1, &exit_status, WNOHANG)) > 0)能夠回收所有的僵尸進程了。

我們可以在上面的信號處理函數(shù)中加入相應的打印信息:

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;  }

打印的結果你會發(fā)現(xiàn),當num1遞增1的時候,即每調用一次信號處理函數(shù),num2一般會遞增很多,即while循環(huán)了很多次,所以盡管有的SIGCHLD信號被丟棄了,但是我們不用擔心子進程的殘余信息會收不回來。退出while循環(huán)時,證明此時系統(tǒng)中已經(jīng)沒有僵尸進程了,所以退出信號處理函數(shù)后,阻塞的唯一SIGCHLD信號會再次觸發(fā)該信號處理函數(shù),這樣我們就不用擔心了。我們不防做個最壞的打算,即之前的信號全部被丟棄了,只有最后一次的SIGCHLD信號被捕獲,從而觸發(fā)了信號處理函數(shù),這樣我們也不用擔心,因為while循環(huán)會一次性收回全部的僵尸進程信息,只是這次循環(huán)的次數(shù)要多得多罷了,當然這只是假設,一般系統(tǒng)不會出現(xiàn)這樣的情況(可以參考本文最后一個程序事例)。

為了證明wait系統(tǒng)函數(shù)與信號SIGCHLD沒有任何關系,我們可以做個簡單的實驗,代碼如下:

#include    #include    #include    #include   #include    int main(int argc, char *argv[])  {      int i;      pid_t pid;        for (i = 0; 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僵尸進程處置Linux僵尸進程處置

可以看到第一次sleep時系統(tǒng)中積累了5個僵尸進程,第二次sleep時,那5個僵尸進程都被收回了。這個也明顯的看到了使用信號處理函數(shù)的優(yōu)勢,即可以保證系統(tǒng)不會積累大量的僵尸進程,它可以迅速的清理掉系統(tǒng)中的僵尸進程。

關于“Linux中如何處理僵尸進程”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,使各位可以學到更多知識,如果覺得文章不錯,請把它分享出去讓更多的人看到。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內容。

AI