今天分享一个我遇到的 Linux 调试案例,问题的原因非常出人意料。
背景是这样的:车端域控里有一个自启进程,负责采集设备的温度、存储等健康状态,并上报给本地监控系统。它由启动脚本在后台拉起:
telemetry-agent -f etc/telemetry-agent/profile-a.json \ > /tmp/telemetry-agent.log &问题表现为:「偶尔起不来」。最麻烦的是,当起不来的时候,在系统里找不到它存在过的痕迹:没有 log,没有 core dump,跟个幽灵似的。
起初我怀疑这进程压根就没被拉起来,但问题是偶发的,也只能猜测。
最近终于有一台设备可以稳定复现这个现象,于是开始排查。
第一步,先确认这个进程到底有没有被拉起来:在启动脚本里加了一行 pid 打印。结果:pid 打出来了。这说明进程「确实被拉起来了」,只是起来之后很快就没了。
接着怀疑是不是后台启动没加 nohup,脚本退出时被 shell 带走了?但是我把 nohup、setsid、disown 三件套挨个试了下,都没用。
既然这样,我就来个大的,我在脚本末尾加了一行 sleep 1000,硬控它 1000 秒,不许退出。
这次,进程活下来了。
调到这里,我认为有两种可能:
telemetry-agent 有启动竞态第一个比较自然,因为 sleep 1000 的目的就是让它不退出,但是第二点还需要进一步验证。
于是,我把 sleep 1000 改成了 sleep 1。
神奇,这样做进程也能活。甚至说,我随便加一条打印,也行。如果是时序问题的话,那这要求还真不高。

这个时候其实已经有个能用的 workaround 了。
但我不想用 sleep 1 去解决一个「进程莫名消失」的问题,这算什么,玄学?更好的做法也有,那就是把这个进程交给 systemd 单独托管,但那等于绕开问题,不是解决问题。
常言道,事出反常必有妖,我要把根因抓出来。
我找 AI 帮忙分析,梳理出了完整的启动调用链:
systemd └─ platform-bootstrap.service └─ platform-bootstrap.sh └─ startup.sh └─ telemetry-agent &原来这个任务是被 systemd 管理的,它的关键配置如下:
[Service]Type=simpleExecStart=/usr/local/bin/platform-bootstrap.shRemainAfterExit=yesKillMode=control-groupAI 给出了第一个猜想:startup.sh 结束时,systemd 按 cgroup 生命周期清理了后台进程。
但我当时就提出了质疑:「如果进程是脚本结束时被杀的,那加延时也没用啊。」 而且 KillMode=control-group 的含义也不是主脚本退出就杀子进程。它的意思是:systemd 停止或清理这个 service 时,会向该 service cgroup 内的全部进程发信号。
我还注意到另一个关键配置 RemainAfterExit=yes,这表示,主脚本成功时,RemainAfterExit=yes 会让任务保持 active,只有失败才会进入 failed 并触发 cgroup 清理。
所以,可能是主脚本失败了?沿着这个思路继续查。AI 发现上层脚本是用 bash -x 执行的,日志会被 systemd 收集进这个 service 的 journal。于是,AI 把启动日志翻了出来:
/opt/platform/startup.sh: line 142: /var/log/platform/diagnostics/466.txt: No such file or directorysystemd: platform-bootstrap.service: Main process exited, code=exited, status=1/FAILUREsystemd: platform-bootstrap.service: Failed with result 'exit-code'.还真是这样,真相终于浮出水面了。
startup.sh 最后一行是:
collect-hw-status > /var/log/platform/diagnostics/${boot_id}.txt但在这个设备上,这个目录不存在,所以重定向失败了。
这本来也没什么,但坏就坏在它是脚本的最后一条命令,导致 Bash 以非零退出码结束了。
这个错误码导致 platform-bootstrap.service 进入了 failed 状态,最终触发了 systemd 的清理动作,而挂在里面的进程就当了炮灰。

真是城门失火,殃及池鱼。
那为什么 sleep 1 能「修好」,不是因为它给了进程启动时间,只是因为这个脚本没有显式 exit,所以退出码默认取决于最后一条命令:重定向虽然失败了,但 sleep 1 成功了,所以最终退出码变成 0,service 不再 failed,进程自然就留下来了。
为了确认这不是某个嵌入式平台的特殊现象,我还在 x86 上做了个最小复现:
systemd-run --property=Type=simple \ --property=RemainAfterExit=yes \ --property=KillMode=control-group \ /bin/bash -c 'sleep 300 & exit 1'结果 ActiveState=failed,后台 sleep 没活下来。但把 exit 1 换成 exit 0,进程就存活了。
现在回头再看 nohup 为什么没用:nohup 对付的是 SIGHUP,但这个脚本是由 systemd 非交互运行,本来就没有终端断开带来的 SIGHUP。而 setsid 是用于脱离会话的,disown 则是让 shell 不再追踪 job,但它们都不能把 telemetry-agent 从 platform-bootstrap.service 的 cgroup 里移出去。当 systemd 清理这组进程时,它还是会一起被清掉。
最后总结一下这次的收获:
在 systemd 启动的脚本里,后台进程有没有活下来,不只取决于它怎么启动,还取决于启动它的 service 最后以什么状态收场。
另外,虽然问题原因找到了,但把需要常驻的进程放到脚本里启动并不合适,应该做成 service 交给 systemd 单独托管。
推荐:解压打转块小游戏