一、现象:为什么 ps 里突然出现 <defunct>

很多运维同学写过这样的 shell 一行流:

ffmpeg -i a.mp4 -c copy output.mp4 &
# 或者
nohup ffmpeg -i a.mp4 -c copy output.mp4 >/dev/null 2>&1 &

第二天巡检却发现:

USER PID PPID ... COMMAND
work 1234 1 0 Z  [ffmpeg] <defunct>

这就是“僵尸进程”(Zombie)。它只占一个 PID,不耗 CPU/内存,但如果批量任务不断产生,PID 耗尽=系统拒绝 fork=新任务全失败。理解成因才能根治。

二、僵尸进程的本质

Linux 里进程终止时,内核会保留一个“最小尸体”——仅含退出码、资源使用等统计信息,等待父进程 wait()/waitpid() 来“收尸”。
若父进程 永远不收,尸体就一直躺尸,状态标记为 Z

三、两种常见写法风险对比
写法 父进程是谁? 是否会 wait 僵尸风险
cmd & 当前 Shell 交互式 Shell 会装作业控制,脚本 Shell 默认不等 脚本退出后高
nohup cmd & 当前 Shell 同上,仅忽略 SIGHUP,不解决 wait 问题 同上

一句话:&nohup 都只解决“终端挂断”,不解决“收尸”

四、典型翻车场景
  1. CI/CD 或 Crontab 调用脚本,脚本里 for 循环启动 100 个 ffmpeg & 后直接退出。
  2. 父 Shell 异常被杀(SSH 断、OOM),PPID 变成 1,但 systemd 也不会自动 wait 非 service 子进程。
    结果:批量僵尸。
五、生产级 4 种安全方案
  1. 显式 wait(最简单)
for f in *.mp4; do
    ffmpeg -i "$f" -c copy "out/${f%.mp4}.mp4" &
done
wait          # 阻塞直到所有后台任务完成
  1. 进程替换+waitpid(bash ≥4.3)
#!/usr/bin/env bash
for f in *.mp4; do
    (ffmpeg -i "$f" -c copy "out/${f%.mp4}.mp4" &)
    pid=$!
    echo "$pid" >>/tmp/ffmpeg.pids
done
while read -r pid; do
    wait "$pid"
done </tmp/ffmpeg.pids
  1. systemd-run(推荐,自带日志与自重启)
systemd-run --user --scope -p MemoryMax=2G \
            -p CPUQuota=50% \
            --collect \
            ffmpeg -i a.mp4 -c copy output.mp4

--collect 保证任务结束后立即回收,僵尸概率 0;还能用 journalctl -u run-r*.scope 看日志。

  1. 超时+清理兜底
timeout 300s ffmpeg -i a.mp4 -c copy output.mp4 &
PID=$!
(sleep 310 && kill -9 $PID 2>/dev/null) &
wait $PID

防止转码卡死,同时保证 wait。

六、一键巡检脚本
ps -eo pid,ppid,stat,comm | awk '$3~/^Z/ {print}'

输出不为空即存在僵尸;配合 pstree -p 可快速定位“不负责”的父进程。

七、结论
  • 僵尸进程不是“病毒”,而是 父进程写法缺陷
  • &/nohup 只能让任务忽略挂端信号,不能替代 wait()
  • 脚本场景务必 wait;长期服务请交给 systemdsupervisor;批量转码用 作业调度器(SLURM、Airflow) 更佳。
    做好“收尸”,生产环境才能夜夜安睡。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐