Linux丨进程僵尸态的实战成因

603 阅读5分钟

众所周知,Linux里的进程都由父进程fork而来。当它们死亡时,父进程有责任替它们“收尸”,否则这些死去的进程会进入“僵尸态”(Zombie),并一直占用进程表的条目。

通常而言,父进程会通过wait()waitpid()去读取子进程的退出状态,并释放子进程在进程表中占用的条目。但这句话对于调试缺乏指导意义,因为大多数情况waitpid()就在那里,可是问题的难点在于“为什么waitpid()没被调用”或者“为什么waitpid()无法回收子进程”。

下面介绍实战中会导致进程僵尸态的两个原因,它们对于调试问题更有价值。

第一种原因是:子进程的主线程已经退出,但仍有一个线程卡在D态无法退出。

当我们想要杀死一个进程时,通常会给它发送SIGKILL。但不论我们采用kill(将SIGKILL发送给进程)还是tgkill(将SIGKILL发送给某个线程),最终的结果都是杀死整个进程(线程组)。因为SIGKILL是致命信号,发送端在内核的complete_signal函数中会遍历目标进程的所有线程,往它们的pending signal中塞入SIGKILL并唤醒它们。

发送SIGKILL给进程

从上层的视角来看,信号似乎一发送就被对端线程处理掉。但真实的情况并非如此。信号会等在那里,等待对端线程从内核态返回到用户态来检查它。而唤醒也并非意味着线程立刻恢复运行,多数时候只是将它加入到某个CPU的run queue,然后等待调度根据vruntime来临幸它。因此虽然上图中三个线程都被唤醒来处理信号,但它们处理的先后顺序实际上是没法保证的。

主线程最后退出

主线程先退出,且有一个线程卡在D态

每个线程在退出时都会将自身的状态置为Zombie,但只有最后一个退出的线程才会给父进程发送SIGCHLD通知它来回收。因为只有这个时候,所有的线程才都退出了。但是如果进程中有一个线程卡在D态(TASK_UNINTERRUPTIBLE),那么情况就发生了变化。D态的线程无法被唤醒,自然也就无法处理SIGKILL。而其他退出的线程会发现自己并非最后一个存活的线程,因此也都不会给父进程发送SIGCHLD。这么一来,主线程进入Zombie状态,父进程也收不到SIGCHLD。由于进程的状态取自主线程,因此这时用ps看到的进程状态便是Z。这种情况下,用ps -T -p能看到进程下有两个线程,一个是Z态的主线程,另一个就是D态的卡死线程。这时,我们就得针对性地查一下为什么会有线程卡在D态,这往往是问题的根因。

第二种原因是:父进程采用signalfd的方式来处理SIGCHLD信号,但自身却卡在其他的epoll事件处理函数中。

默认情况下,信号都采用signal handler的方式来处理,但系统中也有进程会采用signalfd的方式,譬如init进程对SIGCHLD的处理。相较于signal handler那种打断执行流的方式,signalfd会将信号转换为可读事件,在合适的时机通过read()来获取和处理,譬如结合epoll变成事件驱动的处理方式。另外,传统的signal handler用起来虽然简单,但也有诸多短板。譬如handler中只能使用异步安全的函数,这一点很多人并不知道。无脑地往信号处理函数中塞入各种逻辑,最终很可能引发死锁和奇怪的内存问题。相比之下,signalfd是一种同步的处理方式,因此可以有更大的自由度。

signalfd的处理方式

我们以init举例,它将signalfd挂载到epoll上,当子进程退出时,SIGCHLD会作为一个事件加入到epoll的事件队列中。但是epoll的处理是串行的,它不像signal handler任何线程都可以处理,如果上一个事件卡住,那么后续加入的事件就只能等待。因此,这种情况下子进程其实已经做完所有的事情,SIGCHLD也发送出来了,但是父进程由于采用了signalfd,一旦前面的事件阻塞住,那么SIGCHLD就无法得到处理,自然也就没法回收子进程了。这时,我们就得针对性地查一下为什么前面的事件会阻塞,这往往是问题的根因。

后记

最近在学习Kernel的过程中频繁使用ChatGPT o1和DeepSeek r1的模型来帮助理解,不得不惊叹AI的进化速度,再想想自己早些年对AI的轻浮言论,着实羞愧。这些天的使用体验让我有了一个观点:日后技术博客、书籍和课程的价值将会逐步衰减。原因有两个,一是不论博客、书籍还是课程,通过它们来获取知识都是单向且线性的。它们并不了解我们对哪块掌握的深,对哪块掌握的浅,而只是静静地躺在那里,等待我们自己去搜寻对自身有价值的部分。而更好的学习方式则是互动,或者说是问答式的,颇有点孔子当年授课的感觉。当AI能够以更宏大的知识储备和相对精准的回答应对我们的提问时,我们便可以在一问一答的博弈中快速领会知识点。之前,碰到自己不熟悉的知识我还经常请教相关领域的牛人,如今这也不用了,因为绝大多数的内行其实回答的还不如AI,而且人问久了还会厌烦。第二个原因是AI已经慢慢渗透到生产环节,会有越来越多的技术不需要去弄懂,而是交给AI来解决。

好在我今天写的这篇文章,AI的回答还差点意思,不然我估计也没有动力再写下去了。时代的洪流,比我预想的来得快了许多。