Linux系統殭屍進程詳解

  • 2021 年 11 月 14 日
  • 筆記

大安好,我是良許。

本文我們將來討論一下什麼是殭屍進程,殭屍進程是怎麼產生的,如何殺死一個殭屍進程。

Linux中的進程是什麼?

講到進程,我們要先了解一下另一個概念:程式

程式說白了就是躺在電腦硬碟上的一個文件而已(如同硬碟女神一樣),在被 CPU 執行之前,它啥也做不了。

當程式被執行之後,它運行的實例就稱為進程 。一個程式可以對應多個進程。

進程是系統的工作單元。系統由多個進程組成,其中有的是作業系統進程(執行系統程式碼),其他的是用戶進程(執行用戶程式碼)。所有這些進程都會並發執行,例如通過在單 CPU 上採用多路復用來實現。

你可以使用 ps 命令查看 Linux 系統中的所有進程 。

$ ps -ax
        PID TTY         STAT   TIME COMMAND
        1 ?     Ss      0:01 /usr/lib/systemd/systemd rhgb --switched-root --sys
        2 ?     S       0:00 [kthreadd]
        3 ?     I<      0:00 [rcu_gp]
        4 ?     I<      0:00 [rcu_par_gp]

當一個進程調用 fork 函數生成另一個進程,原進程就稱為父進程,新生成的進程則稱為子進程。

Linux 系統中這樣父子進程非常多,我們可以使用 pstree 命令查看系統上的進程「譜系」。

$ pstree -psn
systemd(1)─┬─systemd-journal(952)
        ├─systemd-udevd(963)
        ├─systemd-oomd(1137)
        ├─systemd-resolve(1138)
        ├─systemd-userdbd(1139)─┬─systemd-userwor(12707)
        │                     ├─systemd-userwor(12714)
        │                     └─systemd-userwor(12715)
        ├─auditd(1140)───{auditd}(1141)
        ├─dbus-broker-lau(1164)───dbus-broker(1165)
        ├─avahi-daemon(1166)───avahi-daemon(1196)
        ├─bluetoothd(1167)

每個進程在系統中都被分配了一個編號。在這所有的進程中,有個非常特殊的進程,它的 ID 號是 1 。它是系統在引導過程中執行的第一個進程,PID 1 之後的每個後續進程都是它的後代。

什麼是殭屍進程?

前面提到過,在 Linux 環境中,我們是通過 fork 函數來創建子進程的。創建完畢之後,父子進程獨立運行,父進程無法預知子進程什麼時候結束。

通常情況下,子進程退出後,父進程會使用 waitwaitpid 函數進行回收子進程的資源,並獲得子進程的終止狀態。

但是,如果父進程先於子進程結束,則子進程成為孤兒進程。孤兒進程將被 init 進程(進程號為1)領養,並由 init 進程對孤兒進程完成狀態收集工作。

而如果子進程先於父進程退出,同時父進程太忙了,無瑕回收子進程的資源,子進程殘留資源(PCB)存放於內核中,變成殭屍(Zombie)進程,如下圖所示:

殭屍進程是怎麼產生的?

前面已經介紹了殭屍進程產生的原理,下面我們通過程式碼來模擬殭屍進程的產生。

#include  
#include  
#include  
#include  

int main(void)  
{  
    pid_t pid;  
    pid = fork();  
    if (pid == 0) {  
            printf("I am child, my parent= %d, going to sleep 3s\n", getppid());  
            sleep(3);  
            printf("-------------child die--------------\n");  
    } else if (pid > 0) {  
            printf("I am parent, pid = %d, myson = %d, going to sleep 5s\n", getpid(), pid);  
            sleep(5);  
            system("ps -o pid,ppid,state,tty,command");  
    } else {  
        perror("fork");  
        return 1;  
    }  

    return 0;  
}  

在這個程式里,父進程創建子進程之後,就休眠 5 秒鐘。而子進程只休眠 3 秒鐘就退出,在它退出之後,父進程還未蘇醒,因此沒人給子進程「收屍」,所以它就變成了殭屍進程。

如何殺死殭屍進程

對於普通進程,我們可以通過使用 kill 命令來殺死它們。kill 命令它還有幾個兄弟,比如 pkillkillall ,雖然它們名稱里都帶 kill 這樣殺氣騰騰的字眼,但它們實際上是被設計為向一個或多個進程發送訊號。

在未指定的情況下,這幾個命令默認發送的是 SIGTERM 訊號。

普通進程可以被 kill ,但殭屍進程是不行的。為什麼?因為殭屍進程本身就已經「死」過一次了!如果還可以再「死」,那「殭屍」這個名號就沒多大意義了。

殭屍進程其實已經就是退出的進程,因此無法再利用kill命令殺死殭屍進程。殭屍進程的罪魁禍首是父進程沒有回收它的資源,那我們可以想辦法它其它進程去回收殭屍進程的資源,這個進程就是 init 進程。

因此,我們可以直接殺死父進程,init 進程就會很善良地把那些殭屍進程領養過來,併合理的回收它們的資源,那些殭屍進程就得到了妥善的處理了。

例如,如果 PID 5878 是一個殭屍進程,它的父進程是 PID 4809,那麼要殺死殭屍進程 (5878),您可以結束父進程 (4809):

$ sudo kill -9 4809  #4809 is the parent, not the zombie

殺死父進程時要非常小心,如果一個進程的父進程就是 PID 1 ,並且你還殺死了它,那麼系統將直接重啟!

這將是一個更可怕的故事!