Linux 进程有五个基本状态:运行(R)、可中断睡眠(S)、不可中断睡眠(D)、停止(T)、僵死(Z)。
这里面,D 和 Z 是最让人头疼的。
D 状态,全称 TASK_UNINTERRUPTIBLE。进程在等 I/O 完成,信号打不进去。你 kill -9 都杀不死它。
很多初学者不理解:为什么 Linux 不干脆让进程被打断?因为 I/O 操作一旦开始,必须等硬件返回结果。你在读硬盘的一个扇区,中断过来也没用——硬件还没读完,你没法让硬盘停下来去干别的。
D 进程对系统性能最直接的影响是 load average。Linux 的 load average 计算包含了处于 uninterruptible sleep 状态的进程。所以有时候你看到 load 很高但 CPU 利用率很低——基本就是被 D 进程卡住了。系统负载高不是因为 CPU 不够,而是因为大量进程在等 I/O 完成,CPU 反而闲着呢。
Z 状态,全称 TASK_DEAD。子进程跑完了,内核还在进程表里保留它的条目。因为父进程没来回收。
一个 Z 进程只占一个 PID,不占内存。多了就不一样了。Linux 默认 PID 上限 32768,你把 PID 池占满,系统就再也 fork 不出新进程了。线上事故我见过不止一次。
搞懂 D 状态得看内核源码。
进程发起 I/O 请求时,内核调用 set_current_state(TASK_UNINTERRUPTIBLE),扔进等待队列,调用 schedule() 让出 CPU。等 I/O 完成,中断处理程序唤醒它,状态切回 TASK_RUNNING。
为什么不能被打断?I/O 操作涉及磁盘缓存、文件系统元数据、驱动缓冲区。信号中断会导致数据不一致。你正在写磁盘数据,一个 SIGKILL 过来,盘上数据写到一半,文件系统就坏了。
Z 状态的形成更像一个交接失误。子进程调 exit() 退出,内核给它发 SIGCHLD 信号通知父进程。父进程得调 wait() 或 waitpid() 来收尸。父进程没调,或父进程自己卡死了,子进程永远处于 Z 状态。
有一种特殊情况:父进程挂了,孤儿进程被 init(PID 1)自动收养,init 会定期调 wait() 来回收。所以 zombie 的父进程如果是 1,等一会儿就会被清掉,不用太担心。
先说最常用的:
ps -eo pid,stat,comm,wchan --sort=-%cpu | grep -E 'D|Z'
wchan 是关键字段,告诉你进程在内核里等什么。看到 sync_write、__lock_page、wait_on_page_bit,基本就是 I/O 问题。
cat /proc/<pid>/stack
直接告诉你阻塞在内核哪个函数,精确到行号。
ps -e -o stat | grep -c Dps -e -o stat | grep -c Z
统计当前 D 和 Z 进程的数量。
dmesg --time-first | grep -i -A5 -B5 'error\|fail\|hung_task'
hung_task 是内核检测机制。某个任务超过 120 秒(默认值)没被调度,内核会打印告警到 dmesg。
strace -f -e trace=process -o trace.log -p <pid>
-f 跟踪子线程,-e trace=process 只关注进程相关的系统调用,减少输出噪音。
症状:系统 load 高,响应慢,top 里一堆 D 进程
先查是不是 I/O 问题:
iostat -x 1 5
看两个指标:%util 接近 100% 说明磁盘满负荷,await 超过几十毫秒说明延迟高。
%util 这一项容易让人误判。这不是 CPU 使用率,是磁盘忙于处理请求的时间占比。100% 意味着磁盘一直在干活,一秒都没闲着。但这是磁盘性能的瓶颈信号,不是 I/O 挂死。真正的挂死信号是请求完成后 rd_ios 和 wr_ios 不再增长——请求发出了,但永远回不来了。
然后定位是哪个设备:
lsblkcat /proc/<pid>/io
再看进程在等什么:
cat /proc/<pid>/wchancat /proc/<pid>/stackperf record -g -p <pid> && perf report
症状:Z 进程越来越多
先找到父进程:
ps -eo pid,ppid,stat,cmd | grep Z
看 PPID 列。
然后看父进程状态。如果父进程也在 Z 状态,往上追,直到找到活着的祖先。
尝试发送 SIGCHLD 信号:
kill -s SIGCHLD <parent_pid>
前提是父进程正确处理了信号。
如果爹死活不收尸,kill 掉父进程:
kill -9 <parent_pid>
init 会自动收养并回收。但这有风险——父进程可能是你的核心服务。先确认再动手。
D 进程分两种:
- 临时性的,等几十毫秒就恢复。
- 永久性的,硬件挂了或者 NFS 断了。
临时性的不用管,I/O 完成自然恢复。
永久性的,问题出在底层,不是进程本身。
排查路径:
- 查硬件。
smartctl -a /dev/sdX 看磁盘健康,lspci -vvv 看设备状态。 - 查内核。
journalctl -k -b 看有没有 I/O 错误。 - 查挂载。NFS 的话用
mount -t nfs4 检查,nfsstat 看统计。
处理方案:
- 磁盘坏了——换盘,恢复数据
- 驱动缺陷——升级内核或驱动
- 真没救了——重启。
echo b > /proc/sysrq-trigger 直接重启
有个争议:D 进程能不能杀?
技术派说不能杀,数据可能坏。实战派说你线上 D 进程挂一天了,不重启业务就废了。
我个人的看法:先诊断,确认是什么 I/O 堵塞。NFS 挂了或磁盘坏了,重启也解决不了。驱动 bug,升级内核才治本。kill 不是选项。
千万别干的事:
千万别对 D 进程执行 kill -9。它不吃信号不说,强制杀 I/O 操作可能导致数据损坏。也别直接 reboot -f 不看磁盘缓存,先 sync 一下。
有些场景可以试试 sysrq 的魔法键。比如 echo w > /proc/sysrq-trigger,内核会把所有处于 uninterruptible 状态的进程堆栈打印到 dmesg——不用 gdb,不用 strace,一次性看到全局。然后 echo t > /proc/sysrq-trigger 可以看到所有任务的状态。
临时方案:
kill -s SIGCHLD <parent_pid># 如果不行kill -9 <parent_pid>
代码层面:
Linux 下正确的回收姿势:
signal(SIGCHLD, SIG_IGN);// 或者signal(SIGCHLD, sigchld_handler);voidsigchld_handler(int sig){int status;pid_t pid;while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {// 回收完成 }}
更专业的做法,用 sigaction 替代 signal:
structsigactionsa;sa.sa_handler = sigchld_handler;sigemptyset(&sa.sa_mask);sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;sigaction(SIGCHLD, &sa, NULL);
守护进程的经典做法:
double-fork:
pid_t pid = fork();if (pid == 0) { setsid(); pid = fork();if (pid == 0) {// 孙进程,真正的业务进程 } else {exit(0); }} else { wait(NULL);}
逻辑很简单:孙进程的父进程是子进程。子进程调 wait 后立刻退出,孙进程被 init 收养。init 会帮你回收,一劳永逸。
systemd 场景:
[Service]Type=forkingPIDFile=/var/run/app.pidRestart=on-failureKillMode=process
KillMode=process 确保 systemd 只杀主进程不杀子进程。子进程多的话改用 KillMode=mixed。
还有个争议:一个僵尸进程要不要管?
单个僵尸不占资源。但你发现僵尸越来越多,说明代码或系统有问题。
PID 池耗尽不是危言耸听——32768 个 PID,每秒 fork 一个,9 小时用光。之前看到一个例子,有个监控平台的后台进程 fork 子进程但不 wait,每天泄漏几十个 PID,两周后全站宕机。
怎么判断 PID 是否接近上限?
cat /proc/sys/kernel/pid_max# 输出当前允许的最大PIDcat /proc/sys/kernel/threads-max# 输出当前允许的最大线程数
如果系统无法 fork 新进程,/var/log/messages 或 dmesg 里会看到类似这样的错误:fork: Cannot allocate memory。但这个提示具有迷惑性,你第一反应去看内存——内存还够,其实是 PID 用完了。
I/O 调度器的选择:
- SSD 用
none 或 noop,不需要重排请求,延迟越低越好 - 机械盘用
deadline 或 mq-deadline,保证每个请求有截止时间
# 查看当前调度器cat /sys/block/sda/queue/scheduler# 修改echo deadline > /sys/block/sda/queue/scheduler
内核参数调整:
# 减少脏页比例,加快I/O完成sysctl vm.dirty_ratio=10sysctl vm.dirty_background_ratio=3# hung_task检测超时,给D进程更多缓冲sysctl kernel.hung_task_timeout_secs=300# 增大PID上限,给处理僵尸争取时间sysctl kernel.pid_max=65535
这几个参数写进 /etc/sysctl.conf 让它持久化,重启不丢。
文件系统调优:
# 关闭访问时间更新,减少元数据I/Omount -o remount,noatime,nodiratime /mount/point
资源限制:
# /etc/security/limits.conf* soft nproc 65535* hard nproc 65535
最简单的健康检查脚本:
#!/bin/bashD_COUNT=$(ps -e -o stat | grep -c D)Z_COUNT=$(ps -e -o stat | grep -c Z)if [ $Z_COUNT -gt 10 ]; thenecho"检测到 $Z_COUNT 个僵尸进程" | mail -s "Zombie Alert" admin@example.comfi
容器场景:
Kubernetes 配置 livenessProbe:
livenessProbe: exec: command: - sh - -c - "ps -eo stat | grep -q 'D' && exit 1 || exit 0" initialDelaySeconds: 30 periodSeconds: 15
preStop 钩子确保优雅退出:
lifecycle: preStop: exec: command: ["sh", "-c", "kill -SIGTERM 1 && sleep 10"]
| |
|---|
ps -eo pid,stat,comm,wchan | |
cat /proc/<pid>/stack | |
cat /proc/<pid>/wchan | |
iostat -x 1 | |
dmesg \| grep hung_task | |
kill -s SIGCHLD <ppid> | |
strace -p <pid> | |
perf record -g -p <pid> | |
ps -e -o stat \| grep -c Z | |