有个脚本要跑一个会开窗口的桌面程序。我按常规做法启动:
bash
nohup ./cli.cmd publish-article ... >/dev/null 2>&1 &
disown
echo "launched"
命令立刻返回,PID 打印出来了。过一分钟去看输出文件------文件压根没生成。
同一条命令,换个方式跑就好了。这篇记录中间的差别在哪,以及我确证了什么、没确证什么。
先排除一个常见误判
nohup 和 disown 各自只解决一个很窄的问题:
nohup让进程忽略SIGHUP(终端挂断信号)。它的名字就写着自己能干什么,不多干。disown把作业从当前 shell 的作业表里移除,这样 shell 退出时不会给它发信号。
两者都不 阻止 SIGTERM。而清理动作发的通常是 SIGTERM。所以「加了 nohup 和 disown 就该活着」这个预期本身是错的------它们防的是挂断,不是终止。
我观察到的现象
同一条命令,两种跑法,结果稳定可复现:
| 跑法 | 父进程是否等待 | 结果 |
|---|---|---|
nohup ... & + disown,命令立即返回 |
否 | 输出文件从未生成 |
| 放进带轮询循环的后台任务,父进程等到结束 | 是 | 正常跑完,输出完整 |
差别只有一条:父进程是不是活着等到了子进程结束。
补充几个观察到的细节:
- 被杀时没有任何错误输出,表现为「命令成功返回了,但什么都没发生」。
- 目标程序是会开浏览器窗口的 GUI 程序。纯命令行的子进程在同环境下没有这个问题。
- 单篇任务稳定耗时约 40 秒,父进程只要撑过这 40 秒就没事。
我没有确证的部分
坦白说,具体是谁发的 SIGTERM、按什么粒度清理,我没有确证。几种可能性都解释得通:
- 进程组(process group)被整体回收
- 作业对象(Windows Job Object)在父进程退出时关闭
- 无控制台会话的 GUI 进程被会话管理器清理
能确证的只有行为和解法,机制层面以上都是推测。 如果你的环境和我不一样,先复现再说,别直接照搬结论。
有效的三件事
一、让父进程活到子进程结束
这是最可靠的一条。不要用「启动后就返回」的写法,改成启动后同步等待:
bash
nohup ./cli.cmd publish-article ... >/dev/null 2>&1 &
disown
for i in $(seq 1 40); do
sleep 5
[ -f "$OUT" ] && grep -q "执行结束" "$OUT" && break
done
父进程在这里耗着,子进程就活得下来。代价只是父进程多占一会儿。
二、输出必须落到文件,不能靠管道
管道会随着进程一起消失,文件不会。把 stdout/stderr 全部重定向到文件,然后轮询文件内容判断进度,而不是等进程退出。
这一步同时解决了另一个问题:即使中途被杀,已经写进文件的部分输出还在,能从中断的位置看出跑到哪一步了。
三、用哨兵文件区分「没启动」和「启动后被杀」
这两种失败的排查方向完全不同,但表现很像(都是没有产物)。
bash
OUT=/tmp/job.log
: > "$OUT" # 任务开始前清空
echo "STARTED $(date +%s)" >> "$OUT"
nohup ./cli.cmd ... >> "$OUT" 2>&1 &
# ...轮询...
判据:
OUT完全为空 → 子进程启动即死,或者根本没起来。查命令路径、环境变量。OUT有STARTED但没有完成标记 → 起来了,中途被杀。查父进程生命周期。OUT有完成标记 → 正常。
我的第一次失败属于第一种,正因为有文件可查,才没把它误判成「程序内部报错」。
顺带:退出码 0 不等于任务完成
这套流程里还有一个独立的坑。有一次命令退出码是 0,看输出里的状态字段也是 true,但产物文件里根本没有完成标记。
原因是:子进程把状态写进日志后,主进程才做收尾;退出码反映的是主进程的收尾结果,不代表业务真的走完了。
所以判据要用产物里的完成标记,不要用退出码,也不要只信状态字段。三个都对上才算数。
一句话
nohup 是免死金牌的印象,来自「终端关掉进程别死」这个原始场景。在会被主动清理的运行环境里,真正决定子进程生死的是父进程还在不在。让父进程等着,比折腾信号处理简单得多。