Linux 系統中僵屍進程


Linux 系統中僵屍進程和現實中僵屍(雖然我也沒見過)類似,雖然已經死了,但是由於沒人給它們收屍,還能四處走動。僵屍進程指的是那些雖然已經終止的進程,但仍然保留一些信息,等待其父進程為其收屍。配圖源自 Flickr 
2596483147_58d6bae3b1_z

僵屍進程如何產生的?

如果一個進程在其終止的時候,自己就回收所有分配給它的資源,系統就不會產生所謂的僵屍進程了。那么我們說一個進程終止之后,還保留哪些信息?為什么終止之后還需要保留這些信息呢?

一個進程終止的方法很多,進程終止后有些信息對於父進程和內核還是很有用的,例如進程的ID號、進程的退出狀態、進程運行的CPU時間等。因此進程在終止時,回收所有內核分配給它的內存、關閉它打開的所有文件等等,但是還會保留以上極少的信息,以供父進程使用。父進程可以使用 wait/waitpid 等系統調用來為子進程收拾,做一些收尾工作。

因此,一個僵屍進程產生的過程是:父進程調用fork創建子進程后,子進程運行直至其終止,它立即從內存中移除,但進程描述符仍然保留在內存中(進程描述符占有極少的內存空間)。子進程的狀態變成EXIT_ZOMBIE,並且向父進程發送SIGCHLD 信號,父進程此時應該調用 wait() 系統調用來獲取子進程的退出狀態以及其它的信息。在 wait 調用之后,僵屍進程就完全從內存中移除。因此一個僵屍存在於其終止到父進程調用 wait 等函數這個時間的間隙,一般很快就消失,但如果編程不合理,父進程從不調用 wait 等系統調用來收集僵屍進程,那么這些進程會一直存在內存中。

在 Linux 下,我們可以使用 ps 等命令查看系統中僵屍進程,僵屍進程的狀態標記為‘Z’:
screenshot from 2013-10-17 22 36 17

產生一個僵屍進程

根據上面的描述,我們很容易去寫一個程序來產生僵屍進程,如下代碼:

#include <stdio.h>
#include <sys/types.h>

int main()
{
    //fork a child process
    pid_t pid = fork();

    if (pid > 0)   //parent process
    {
        printf("in parent process, sleep for one miniute...zZ...\n");
        sleep(60);
        printf("after sleeping, and exit!\n");
    }
    else if (pid == 0)  
    {
        //child process exit, and to be a zombie process
        printf("in child process, and exit!\n");
        exit(0);
    }

    return 0;
}

父進程並沒有寫 wait 等系統調用函數,因此在子進程退出之后變成僵屍進程,父進程並沒有為其去收屍。我們使用下面命令編譯運行該進程,然后查看系統中進程狀態:

guohailin@guohailin:~/Documents$ gcc zombie.c -o zombie
guohailin@guohailin:~/Documents$ ./zombie 
in parent process, sleep for one miniute...zZ...
in child process, and exit!

# 打開另一個終端:
guohailin@guohailin:~$ ps aux | grep -w 'Z'
1000      2211  1.2  0.0      0     0 ?        Z    13:24   6:53 [chromium-browse] <defunct>
1000      4400  0.0  0.0      0     0 ?        Z    10月16   0:00 [fcitx] <defunct>
1000     10871  0.0  0.0      0     0 pts/4    Z+   22:32   0:00 [zombie] <defunct>

從上面可以看出,系統中多了一個僵屍進程。但如果等父進程睡眠醒來退出之后,我們再次查看系統進程信息,發現剛才的僵屍進程不見了。

guohailin@guohailin:~/Documents$ ./zombie 
in parent process, sleep for one miniute...zZ...
in child process, and exit!
after sleeping, and exit!
guohailin@guohailin:~/Documents$ ps aux | grep -w 'Z'
1000      2211  1.2  0.0      0     0 ?        Z    13:24   6:53 [chromium-browse] <defunct>
1000      4400  0.0  0.0      0     0 ?        Z    10月16   0:00 [fcitx] <defunct>

這是為什么呢?父進程到死都也沒有為其子進程收屍呀,怎么父進程退出之后,那個僵屍進程就消失了呢?難道父進程在退出時會為子進程收拾嗎?其實不然....真正的原因是:父進程死掉之后,其所有子進程過繼給 init 進程,init 進程成為該僵屍進程的新進程,init 進程會周期性地去調用 wait 系統調用來清除它的僵屍孩子。因此,你會發現上面例子中父進程死掉之后,僵屍進程也跟着消失,其實是 init 進程為其收屍的!

怎樣避免僵屍進程的產生

不能使用 kill 后接 SIGKILL 信號這樣的命令像殺死普通進程一樣殺死僵屍進程,因為僵屍進程是已經死掉的進程,它不能再接收任何信號。事實上,如果系統中僵屍進程並不多的話,我們也無需去消除它們,少數的僵屍進程並不會對系統的性能有什么影響。

那么在編程時,如果能避免系統中大量產生僵屍進程呢?根據上面描述的,子進程在終止時會向父進程發 SIGCHLD 信號,Linux 默認是忽略該信號的,我們可以顯示安裝該信號,在信號處理函數中調用 wait 等函數來為其收屍,這樣就能避免僵屍進程長期存在於系統中了。示例代碼如下:

#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>

sig_atomic_t child_exit_status;

void clean_up_child_process(int signal_num)
{
    /* clean up child process */
    int status;
    wait (&status);

    /* store its exit status in a global variable */
    child_exit_status = status;
}

int main()
{
    /* handle SIGCHLD by calling clean_up_child_process  */
    struct sigaction sigchild_action;
    memset(&sigchild_action, 0, sizeof(sigchild_action));
    sigchild_action.sa_handler = &clean_up_child_process;
    sigaction(SIGCHLD, &sigchild_action, NULL);

    /* fork a child, and let the child process dies before parent */
    pid_t c_pid;
    c_pid = fork();
    if (c_pid > 0)
    {
        printf("in parent process, and sleep for on mininute...zZ...\n");
        sleep(60);
    }
    else if(c_pid == 0)
    {
        printf("in child process, and exit now\n");
        exit(0);
    }
    else
    {
        printf("fork failed!\n");
    }

    return 0;
}   

參考資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM