kernel hacking. Linux的僵尸进程及其回收处理
UNIX家族的操作系统里面都用进程的概念,进程就是一个程序运行的实体(instance)。这个概念当年大学里面学《操作系统原理》的时候怎么也搞不懂(清华大学出版社出版,屠立德著)。
直到后面读研究生开始做课题,写代码才知道到底是怎么回事。
Linux延用了UNIX的设计思想,继续使用进程这个概念(后面的线程也是用进程来实现的,所以叫做轻量级进程,LWP)。进程在系统中有不同的状态,进程就是在各个状态之间来回切换,从而完成设计的功能。操作系统内部为每一个进程提供了一个进程描述符,这个结构庞大而且复杂,用来描述进程的信息,例如打开的文件,调度信息,父子进程关系等等,是操作系统管理进程的核心数据结构,Linux里面是struct task_struct。
系统里面的进程因为父子关系而形成一个树形结构。整个系统启动过程中第一个用户态的进程是Init进程,叫做1号进程,它是整个树形结构的根,所有进程都是它的子孙后代。Init是系统的管理进程,包含很多功能,其中一个功能就是“垃圾回收”。
在操作系统原理中会提到“僵尸进程”,什么是僵尸进程?就是一个进程在结束运行的时候它的主体已经结束,但是内核当中的进程描述符还没有被回收(它的“壳”还在,但是“灵魂”没有了)。为什么它的进程描述符没有被回收呢?
因为一个进程结束运行的时候,是需要通知它的父进程对其进程描述符进行回收(它自己都死了,没法回收自己)。而如果它的父进程忙于别的事情而不去主动回收该进程的描述符,就会导致系统出现“僵尸进程”。而这个通知和回收的过程是通过信号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;
}
运行结果为 (运行环境是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命令下面显示的状态是Z,而且不接受kill -9 pid去退出。
那么如果一个进程A的父进程P先于它退出,那么A在退出的时候谁来回收A的进程描述符呢?这个就是init进程的工作。当一个进程的父进程(生父)退出之后,这个父进程下面的子进程都成为init(养父)的子进程了。init进程会周期的调用wait()来回收其子进程的进程描述符。
所以,要想真正“清除”僵尸进程,需要杀掉(kill)它的父进程(生父,不是养父init进程)。
有没有别的办法来做这个事情呢?有,只要我们能写和插入kernel module就可以干这个事情。
下面这个代码是模拟内核当中父进程回收子进程资源的逻辑来完成对僵尸进程的“过继”和“清除”,而不需要杀死其父进程。简单来说就是把一个父进程活着的僵尸进程直接交给init进程使其对它进行回收。
代码参考前面。父进程启动把自己名字改为testaa,然后启动子进程,子进程改名为taowtest,并把自己变成僵尸进程。最后插入kernel module,其找到Zombie状态,并且名字是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);
。。。。。
}
}
}
模块加载之后的内核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的输出,已经找不到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系统中进程之间关系,进程回收,信号处理,以及init进程等等很多方面。而且它可以解决不杀死父进程的情况下回收清理大量僵尸进程的场景和需求。
后续会陆陆续续把一千多篇关于Linux,X86,VT-X,KVM,服务器,嵌入式系统等有关的东西整理出来。
欢迎转载,转载请标明出处。Thanks