Linux 系統(tǒng)僵尸進(jìn)程詳解
大安好,我是良許。
本文我們將來討論一下什么是僵尸進(jìn)程,僵尸進(jìn)程是怎么產(chǎn)生的,如何殺死一個(gè)僵尸進(jìn)程。
Linux中的進(jìn)程是什么?
講到進(jìn)程,我們要先了解一下另一個(gè)概念:程序。
程序說白了就是躺在電腦硬盤上的一個(gè)文件而已(如同硬盤女神一樣),在被 CPU 執(zhí)行之前,它啥也做不了。
當(dāng)程序被執(zhí)行之后,它運(yùn)行的實(shí)例就稱為進(jìn)程 。一個(gè)程序可以對(duì)應(yīng)多個(gè)進(jìn)程。
進(jìn)程是系統(tǒng)的工作單元。系統(tǒng)由多個(gè)進(jìn)程組成,其中有的是操作系統(tǒng)進(jìn)程(執(zhí)行系統(tǒng)代碼),其他的是用戶進(jìn)程(執(zhí)行用戶代碼)。所有這些進(jìn)程都會(huì)并發(fā)執(zhí)行,例如通過在單 CPU 上采用多路復(fù)用來實(shí)現(xiàn)。
你可以使用 ps 命令查看 Linux 系統(tǒng)中的所有進(jìn)程 。
- $ 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]
當(dāng)一個(gè)進(jìn)程調(diào)用 fork 函數(shù)生成另一個(gè)進(jìn)程,原進(jìn)程就稱為父進(jìn)程,新生成的進(jìn)程則稱為子進(jìn)程。
Linux 系統(tǒng)中這樣父子進(jìn)程非常多,我們可以使用 pstree 命令查看系統(tǒng)上的進(jìn)程「譜系」。
- $ 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)
每個(gè)進(jìn)程在系統(tǒng)中都被分配了一個(gè)編號(hào)。在這所有的進(jìn)程中,有個(gè)非常特殊的進(jìn)程,它的 ID 號(hào)是 1 。它是系統(tǒng)在引導(dǎo)過程中執(zhí)行的第一個(gè)進(jìn)程,PID 1 之后的每個(gè)后續(xù)進(jìn)程都是它的后代。
什么是僵尸進(jìn)程?
前面提到過,在 Linux 環(huán)境中,我們是通過 fork 函數(shù)來創(chuàng)建子進(jìn)程的。創(chuàng)建完畢之后,父子進(jìn)程獨(dú)立運(yùn)行,父進(jìn)程無法預(yù)知子進(jìn)程什么時(shí)候結(jié)束。
通常情況下,子進(jìn)程退出后,父進(jìn)程會(huì)使用 wait 或 waitpid 函數(shù)進(jìn)行回收子進(jìn)程的資源,并獲得子進(jìn)程的終止?fàn)顟B(tài)。
但是,如果父進(jìn)程先于子進(jìn)程結(jié)束,則子進(jìn)程成為孤兒進(jìn)程。孤兒進(jìn)程將被 init 進(jìn)程(進(jìn)程號(hào)為1)領(lǐng)養(yǎng),并由 init 進(jìn)程對(duì)孤兒進(jìn)程完成狀態(tài)收集工作。
而如果子進(jìn)程先于父進(jìn)程退出,同時(shí)父進(jìn)程太忙了,無瑕回收子進(jìn)程的資源,子進(jìn)程殘留資源(PCB)存放于內(nèi)核中,變成僵尸(Zombie)進(jìn)程,如下圖所示:
僵尸進(jìn)程是怎么產(chǎn)生的?
前面已經(jīng)介紹了僵尸進(jìn)程產(chǎn)生的原理,下面我們通過代碼來模擬僵尸進(jìn)程的產(chǎn)生。
- #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;
- }
在這個(gè)程序里,父進(jìn)程創(chuàng)建子進(jìn)程之后,就休眠 5 秒鐘。而子進(jìn)程只休眠 3 秒鐘就退出,在它退出之后,父進(jìn)程還未蘇醒,因此沒人給子進(jìn)程「收尸」,所以它就變成了僵尸進(jìn)程。
如何殺死僵尸進(jìn)程
對(duì)于普通進(jìn)程,我們可以通過使用 kill 命令來殺死它們。kill 命令它還有幾個(gè)兄弟,比如 pkill 和 killall ,雖然它們名稱里都帶 kill 這樣殺氣騰騰的字眼,但它們實(shí)際上是被設(shè)計(jì)為向一個(gè)或多個(gè)進(jìn)程發(fā)送信號(hào)。
在未指定的情況下,這幾個(gè)命令默認(rèn)發(fā)送的是 SIGTERM 信號(hào)。
普通進(jìn)程可以被 kill ,但僵尸進(jìn)程是不行的。為什么?因?yàn)榻┦M(jìn)程本身就已經(jīng)「死」過一次了!如果還可以再「死」,那「僵尸」這個(gè)名號(hào)就沒多大意義了。
僵尸進(jìn)程其實(shí)已經(jīng)就是退出的進(jìn)程,因此無法再利用kill命令殺死僵尸進(jìn)程。僵尸進(jìn)程的罪魁禍?zhǔn)资歉高M(jìn)程沒有回收它的資源,那我們可以想辦法它其它進(jìn)程去回收僵尸進(jìn)程的資源,這個(gè)進(jìn)程就是 init 進(jìn)程。
因此,我們可以直接殺死父進(jìn)程,init 進(jìn)程就會(huì)很善良地把那些僵尸進(jìn)程領(lǐng)養(yǎng)過來,并合理的回收它們的資源,那些僵尸進(jìn)程就得到了妥善的處理了。
例如,如果 PID 5878 是一個(gè)僵尸進(jìn)程,它的父進(jìn)程是 PID 4809,那么要?dú)⑺澜┦M(jìn)程 (5878),您可以結(jié)束父進(jìn)程 (4809):
- $ sudo kill -9 4809 #4809 is the parent, not the zombie
殺死父進(jìn)程時(shí)要非常小心,如果一個(gè)進(jìn)程的父進(jìn)程就是 PID 1 ,并且你還殺死了它,那么系統(tǒng)將直接重啟!
這將是一個(gè)更可怕的故事!