kernel hacking. Linux的僵尸進程及其回收處理

UNIX家族的操作系統(tǒng)里面都用進程的概念,進程就是一個程序運行的實體(instance)。這個概念當年大學里面學《操作系統(tǒng)原理》的時候怎么也搞不懂(清華大學出版社出版,屠立德著)。

直到后面讀研究生開始做課題,寫代碼才知道到底是怎么回事。

Linux延用了UNIX的設(shè)計思想,繼續(xù)使用進程這個概念(后面的線程也是用進程來實現(xiàn)的,所以叫做輕量級進程,LWP)。進程在系統(tǒng)中有不同的狀態(tài),進程就是在各個狀態(tài)之間來回切換,從而完成設(shè)計的功能。操作系統(tǒng)內(nèi)部為每一個進程提供了一個進程描述符,這個結(jié)構(gòu)龐大而且復雜,用來描述進程的信息,例如打開的文件,調(diào)度信息,父子進程關(guān)系等等,是操作系統(tǒng)管理進程的核心數(shù)據(jù)結(jié)構(gòu),Linux里面是struct task_struct。

系統(tǒng)里面的進程因為父子關(guān)系而形成一個樹形結(jié)構(gòu)。整個系統(tǒng)啟動過程中第一個用戶態(tài)的進程是Init進程,叫做1號進程,它是整個樹形結(jié)構(gòu)的根,所有進程都是它的子孫后代。Init是系統(tǒng)的管理進程,包含很多功能,其中一個功能就是“垃圾回收”。

在操作系統(tǒng)原理中會提到“僵尸進程”,什么是僵尸進程?就是一個進程在結(jié)束運行的時候它的主體已經(jīng)結(jié)束,但是內(nèi)核當中的進程描述符還沒有被回收(它的“殼”還在,但是“靈魂”沒有了)。為什么它的進程描述符沒有被回收呢?

因為一個進程結(jié)束運行的時候,是需要通知它的父進程對其進程描述符進行回收(它自己都死了,沒法回收自己)。而如果它的父進程忙于別的事情而不去主動回收該進程的描述符,就會導致系統(tǒng)出現(xiàn)“僵尸進程”。而這個通知和回收的過程是通過信號SIGCHLD和wait來完成的。具體可以參考Linux進程編程方面的文章或者書籍。

下面是一個例子。

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <sys/types.h>

#include <string.h>

int main(int argc, char **argv)

{

pid_t pid, ppid = 0;

printf("Argv[0] = %s, length = %ld\n", argv[0], strlen(argv[0]));

strncpy(argv[0], "testaa", strlen(argv[0]));

pid = fork();

if (pid > 0) {

printf("Parent: pid = %d, ppid = %d\n", getpid(), getppid());

while(1) {

sleep(1);

}

} else if (pid == 0) {

strncpy(argv[0], "taowtest", strlen(argv[0]));

printf("Child: pid = %d, ppid = %d\n", getpid(), getppid());

exit(123);

}

return 0;

}

運行結(jié)果為 (運行環(huán)境是Ubuntu 18.04 X86_64, VMware的虛擬機)

Parent: pid = 561, ppid = 31574

Child: pid = 562, ppid = 561

/test/kermod# ps ax | grep -e 'testaa\|taow'

561 pts/0 S 0:00 testaa

562 pts/0 Z 0:00 [taowtest] <defunct>

“僵尸進程”在ps命令下面顯示的狀態(tài)是Z,而且不接受kill -9 pid去退出。

那么如果一個進程A的父進程P先于它退出,那么A在退出的時候誰來回收A的進程描述符呢?這個就是init進程的工作。當一個進程的父進程(生父)退出之后,這個父進程下面的子進程都成為init(養(yǎng)父)的子進程了。init進程會周期的調(diào)用wait()來回收其子進程的進程描述符。

所以,要想真正“清除”僵尸進程,需要殺掉(kill)它的父進程(生父,不是養(yǎng)父init進程)。

有沒有別的辦法來做這個事情呢?有,只要我們能寫和插入kernel module就可以干這個事情。

下面這個代碼是模擬內(nèi)核當中父進程回收子進程資源的邏輯來完成對僵尸進程的“過繼”和“清除”,而不需要殺死其父進程。簡單來說就是把一個父進程活著的僵尸進程直接交給init進程使其對它進行回收。

代碼參考前面。父進程啟動把自己名字改為testaa,然后啟動子進程,子進程改名為taowtest,并把自己變成僵尸進程。最后插入kernel module,其找到Zombie狀態(tài),并且名字是taowtest的進程之后對其進行處理,把它交給init進程,并通知init進程對其進行回收。

下面是找到名為taowtest的僵尸進程的參考代碼。

for_each_process( task ) {

? ? if ((task->pid == 0) || (task->pid == 1)) {

? ? ? ? pr_info("PID %d, comm = %s\n", task->pid, task->comm);

? ? ? ? p = task;

? ? ? ? continue;

? ? }

? ? if (strstr(task->comm, "taowtest")) {

? ? ? ? pr_info("Got : %d, %s, ppid=%d, exit_state = %d\n",

? ? ? ? ? ? ? ? task->pid, task->comm, task->parent->pid, task->exit_state);

? ? ? ? if (task->exit_state == EXIT_ZOMBIE) {

? ? ? ? ? ? 。。。。。

? ? ? ? ? ? pr_info("Reaped Zombie process %d\n", task->pid);

? ? ? ? ? ? 。。。。。

? ? ? ? }

? ? }

}

模塊加載之后的內(nèi)核log顯示如下,

[250532.186292] LOADING MODULE

[250532.186294] PID 1, comm = systemd

[250532.186380] Got : 561, taowtest, ppid=31574, exit_state = 0

[250532.186382] Got : 562, taowtest, ppid=561, exit_state = 32

[250532.187181] Reaped Zombie process 562

此時,再看ps -ax的輸出,已經(jīng)找不到taowtest了,而此時testaa仍然還在。

/test/kermod# ps ax | grep 'testaa\|taow'

561 pts/0 S 0:00 testaa

577 pts/0 S+ 0:00 grep --color=auto testaa\|taow

/test/kermod#

以上是一種回收僵尸進程的方法,還有另一種方法有空再分析吧。

總之,這是個有趣的實驗,有助于搞清楚Linux系統(tǒng)中進程之間關(guān)系,進程回收,信號處理,以及init進程等等很多方面。而且它可以解決不殺死父進程的情況下回收清理大量僵尸進程的場景和需求。

后續(xù)會陸陸續(xù)續(xù)把一千多篇關(guān)于Linux,X86,VT-X,KVM,服務(wù)器,嵌入式系統(tǒng)等有關(guān)的東西整理出來。

歡迎轉(zhuǎn)載,轉(zhuǎn)載請標明出處。Thanks

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容