ps aux 看到 STAT 列有个 Z?僵尸进程杀不死、删不掉,还占用 PID 资源。本文从进程生命周期讲起,彻底搞懂僵尸进程的产生原理,以及生产环境的排查和处理方案。# 日常巡检,发现异常进程$ ps aux | grep 'Z'USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMANDroot 1234 0.0 0.0 0 0 ? Z Aug05 0:00 [python] <defunct># 或者$ topTasks: 245 total, 1 running, 234 sleeping, 0 stopped, 10 zombie# ↑ 10个僵尸进程!# 尝试杀死,无效!$ sudo kill -9 1234$ ps aux | grep 1234root 1234 0.0 0.0 0 0 ? Z Aug05 0:00 [python] <defunct># 还在!Z 状态没变
僵尸进程的危害:
占用 PID 资源(系统 PID 有上限,耗尽后无法创建新进程)
占用进程表条目
不占用 CPU 和内存(但父进程如果大量产生,会拖垮系统)
进程的一生:┌─────────┐ fork() ┌─────────┐ exec() ┌─────────┐│ 父进程 │ ─────────→ │ 子进程 │ ─────────→ │ 运行中 ││ (Parent)│ │ (Child) │ │ (Running)│└─────────┘ └─────────┘ └────┬────┘↑ ││ │ exit()│ ↓│ ┌─────────┐│ │ 僵尸状态 ││ │ (Zombie) ││ │ 等待父进程 ││ │ 收尸(wait) ││ └────┬────┘│ │└──────────────── wait() / waitpid() ←──────────┘↓┌─────────┐│ 彻底销毁 ││ (Removed)│└─────────┘
关键规则:
子进程退出时,内核保留其 PID、退出状态等信息
父进程必须调用 wait() 或 waitpid() 读取这些信息
父进程读取后,内核才释放子进程的进程表条目
父进程不读取 → 子进程变成僵尸进程(Zombie)
// ❌ 错误示例:父进程不等待子进程#include<unistd.h>#include<sys/wait.h>intmain(){pid_t pid = fork();if (pid == 0) {// 子进程printf("Child exiting\n");_exit(0); // 子进程退出}// 父进程sleep(60); // 睡觉,不调用 wait()// 子进程变成僵尸!return 0;}
// ❌ 错误示例:SIGCHLD 信号被忽略,但未 reap 子进程#include<signal.h>voidhandler(int sig){// 空处理,不调用 waitpid()printf("Got SIGCHLD\n");}intmain(){signal(SIGCHLD, handler); // 收到子进程退出信号,但不处理while (1) {fork(); // 不断创建子进程sleep(1);}}
场景:1. 父进程创建子进程后崩溃2. 子进程还在运行3. 子进程结束后,init/systemd 成为新父进程4. init 定期调用 wait(),正常回收→ 这种情况通常不会产生僵尸进程(init 会处理)
wait | |
# 方法1:ps 查看 Z 状态ps aux | awk '$8 ~ /^Z/ {print $0}'# 方法2:top 看 Tasks 行top -bn1 | grep zombie# 方法3:专门查看ps -eo pid,ppid,stat,cmd | grep '^ *[^ ]* *[^ ]* *Z'# 方法4:统计僵尸进程数量ps aux | grep -c '^.*Z'
# 查看僵尸进程的 PPID(父进程ID)ps -eo pid,ppid,stat,cmd | grep 'Z'# 输出示例:# PID PPID STAT CMD# 1234 5678 Z [python] <defunct># ↑ 父进程是 5678# 查看父进程ps -f -p 5678UID PID PPID C STIME TTY TIME CMDroot 5678 1 0 Aug05 ? 00:00:10 /usr/bin/python /opt/app/main.py
关键:找到 PPID,处理父进程才是根治!
# 找到僵尸进程的父进程ps -eo pid,ppid,stat,cmd | grep 'Z'# 假设父进程 PID 是 5678# 杀死父进程sudo kill -9 5678# 父进程死后,僵尸子进程被 init/systemd 接管# init 会调用 wait() 回收,僵尸消失
风险:父进程是主服务,杀死后服务中断。
# 向父进程发送 SIGCHLD,触发其信号处理sudo kill -CHLD 5678# 如果父进程的信号处理函数里有 wait(),僵尸会被回收# 如果父进程没处理 SIGCHLD,无效
# 优雅重启(如果服务支持)sudo systemctl restart myapp# 或sudo docker restart container_name# 重启后,旧进程退出,僵尸自然消失
如果父进程是自己开发的程序,修复代码:
// ✅ 正确示例:父进程正确回收子进程#include<sys/wait.h>#include<signal.h>voidreap_children(int sig){// 循环 waitpid,处理多个同时退出的子进程while (waitpid(-1, NULL, WNOHANG) > 0);}intmain(){// 设置 SIGCHLD 处理函数struct sigaction sa;sa.sa_handler = reap_children;sigemptyset(&sa.sa_mask);sa.sa_flags = SA_RESTART;sigaction(SIGCHLD, &sa, NULL);// 创建子进程...pid_t pid = fork();if (pid == 0) {// 子进程工作execl("/bin/ls", "ls", NULL);_exit(1);}// 父进程继续工作,子进程退出时自动触发 reap_childrenwhile (1) {sleep(1);}return 0;}
# ✅ Python 正确示例import osimport signaldef reap_children(signum, frame):"""回收所有已退出的子进程"""while True:try:pid, status = os.waitpid(-1, os.WNOHANG)if pid == 0:breakprint(f"Reaped child {pid} with status {status}")except ChildProcessError:break# 注册 SIGCHLD 处理函数signal.signal(signal.SIGCHLD, reap_children)# 创建子进程pid = os.fork()if pid == 0:# 子进程os._exit(0)# 父进程继续
Docker 容器中的坑:
# ❌ 错误:直接运行应用作为 PID 1CMD ["python", "app.py"]# 如果 app.py 创建子进程,Python 不会自动回收# ✅ 正确:使用 dumb-init 或 tini 作为 PID 1# dumb-init 会正确处理 SIGCHLD,回收孤儿进程FROM python:3.11-slimRUN apt-get update && apt-get install -y dumb-initENTRYPOINT ["dumb-init", "--"]CMD ["python", "app.py"]# 或 tini(更现代)FROM python:3.11-slimRUN apt-get update && apt-get install -y tiniENTRYPOINT ["/usr/bin/tini", "--"]CMD ["python", "app.py"]
# /etc/systemd/system/myapp.service[Service]Type=simpleExecStart=/opt/app/myappRestart=always# systemd 作为 PID 1,会自动回收孤儿进程
# Python:使用 subprocess 时确保回收import subprocess# ✅ 正确:使用 with 语句,自动等待with subprocess.Popen(["ls", "-l"], stdout=subprocess.PIPE) as proc:output = proc.stdout.read()# with 结束时自动调用 wait()# ✅ 正确:显式调用 wait()proc = subprocess.Popen(["ls", "-l"])# ... 其他操作proc.wait() # 必须调用!
# 监控脚本:检查僵尸进程数量#!/bin/bashZOMBIE_COUNT=$(ps aux | awk '$8 ~ /^Z/ {count++} END {print count+0}')if [ "$ZOMBIE_COUNT" -gt 10 ]; thenecho "ALERT: $ZOMBIE_COUNT zombie processes detected!"# 发送告警(钉钉/企业微信/邮件)curl -X POST "https://oapi.dingtalk.com/robot/send?access_token=xxx" \-H "Content-Type: application/json" \-d "{\"msgtype\":\"text\",\"text\":{\"content\":\"僵尸进程告警:当前 $ZOMBIE_COUNT 个\"}}"fi
| 僵尸进程(Zombie) | |||
| 孤儿进程(Orphan) |
孤儿进程:父进程死了 → 子进程被 init(PID 1)收养 → init 调用 wait() → 正常回收僵尸进程:父进程活着 → 子进程死了 → 父进程不调用 wait() → 变成僵尸
□ 僵尸进程产生原因:父进程未调用 wait()/waitpid()□ 排查:ps aux 找 STAT=Z,记录 PPID□ 临时处理:kill 父进程(简单粗暴)□ 根治:修复父进程代码,正确处理 SIGCHLD□ 预防1:容器用 dumb-init/tini 作为 PID 1□ 预防2:代码中 fork 后确保 wait□ 预防3:subprocess.Popen 后调用 wait()□ 监控:定期检查僵尸进程数量,超阈值告警
僵尸进程杀不死,因为已经死了;要治僵尸,找它爹——让父进程调用 wait() 收尸,或者直接把爹杀了让 init 接管。