
你运行ps aux,看到一行奇怪的输出:[python] <defunct>。进程名后面跟着<defunct>,状态列是Z。这是一个僵尸进程。它不占CPU,不占内存,但占着进程表里的一个位置。如果僵尸进程太多,系统可能无法创建新进程。
今天把僵尸进程的产生原因和清理方法讲清楚。
先看一个数据
僵尸进程本身不消耗CPU和内存,但会占用进程ID(PID)资源。每个系统能创建的进程数量是有限的(默认PID最大值通常为32768)。如果僵尸进程数量达到上万级别,系统可能无法启动新的服务或应用,从而导致可用性中断。
僵尸进程是怎么产生的
在Linux系统中,每个进程退出时都会经历一个状态转换。父进程通过wait()或waitpid()系统调用来获取子进程的退出状态。如果父进程没有调用这些函数来“收尸”,子进程就会进入僵尸状态。
一个简单的产生僵尸进程的C程序示例:
c
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程直接退出
printf("Child process exiting\n");
} else {
// 父进程故意不调用wait()
printf("Parent process sleeping\n");
sleep(60);
}
return 0;
}
这个程序创建了一个子进程后,父进程不调用wait()回收子进程,子进程就会变成僵尸进程。
僵尸进程的完整生命周期
进程退出时的典型流程:
- 进程通过
exit()系统调用退出 - 内核释放进程的大部分资源(内存、文件描述符等)
- 进程状态变为
Z(僵尸),进程描述符保留在进程表中,等待父进程调用wait()获取退出状态 - 父进程调用
wait(),内核释放最后的进程描述符,僵尸进程消失 - 如果父进程没有调用
wait(),僵尸进程会一直保留在进程表中
内核必须保留僵尸进程的进程描述符,因为父进程可能需要读取子进程的退出码(0表示成功,非0表示错误)。父进程读取后,僵尸进程才会被彻底清理。
如何识别僵尸进程
用ps命令查看:
bash
ps aux | grep Z ps -ef | grep defunct
状态为Z的进程就是僵尸进程。ps aux输出中STAT列显示Z,COMMAND列显示<defunct>。
查看僵尸进程数量:
bash
ps aux | grep -c Z
如何清理僵尸进程
僵尸进程已经死了,你无法用kill杀死它。kill -9对僵尸进程无效,它已经在等父进程回收了。
清理僵尸进程的唯一方法:杀死它的父进程。
找到父进程:
bash
ps -o ppid= -p 僵尸PID
或使用pstree查看进程树:
bash
pstree -p 父PID
杀死父进程:
bash
kill -15 父PID # 如果不行,用 -9 kill -9 父PID
父进程被杀死后,僵尸进程会变成孤儿进程,被init进程(PID 1)收养。init进程会定期调用wait()回收孤儿进程,僵尸进程就会被清理掉。
如果父进程是PID 1(init/systemd):
理论上PID 1不会被杀死,这种情况比较棘手。通常意味着系统级服务有bug。最常见的解决方案是重启整个服务器。但这应该是最后的手段。
确认僵尸进程是否已清理:
bash
ps aux | grep 僵尸PID # 如果查不到,说明已经被回收了
僵尸进程堆积的常见原因
1. 父进程代码有bug
父进程创建子进程后,没有调用wait()或waitpid()来回收子进程。这是最常见的原因。
2. 父进程被挂起或阻塞
父进程卡在某个操作中(如死循环、等待I/O),无法执行到wait()调用。父进程还活着,但永远无法“收尸”。
3. 父进程先于子进程退出
如果父进程在子进程之前退出,子进程变成孤儿进程,由init进程收养。init进程会回收,通常不会产生僵尸。僵尸进程产生的条件只能是父进程仍在运行但未回收子进程。
如何预防僵尸进程
1. 信号处理
父进程注册SIGCHLD信号处理函数,在信号处理函数中调用wait():
c
signal(SIGCHLD, sigchld_handler);
2. 使用waitpid(-1, NULL, WNOHANG)
不阻塞父进程,周期性地检查并回收子进程。
3. 双重fork技巧
让子进程再创建一个孙进程,然后子进程立即退出,孙进程由init进程收养。这样init进程会负责回收,父进程不用担心子进程变成僵尸。
真实案例
某台服务器运行了一个Python脚本,每隔几分钟创建一个子进程去处理任务。脚本没有正确回收子进程,运行三天后产生了800多个僵尸进程。虽然不消耗CPU,但进程表快满了,新的定时任务无法启动。kill无法杀死僵尸,最终通过pstree找到父进程并重启了Python脚本,所有僵尸进程被init进程回收。
最后一句
僵尸进程清理的本质是让父进程执行wait()。如果父进程不正常,就杀死父进程,让init进程接管。kill -9杀不掉僵尸,这是很多人容易走错的弯路。如果你的服务器上僵尸进程数量持续增长,说明父进程代码有bug,需要从源头修复。杀死父进程只是治标。找到那段不调用wait()的代码,才是治本。




