在开发机上,机器人程序通常由人在终端里启动;到设备上,它需要开机自启、崩溃后恢复、日志可追溯。用 systemd 托管是常见选择,但“写一个 unit 文件”和“定义清楚故障边界”是两件事。
这篇文章关注三个容易被忽略的决策:重启策略描述了哪些失败、进程组决定了停止信号发给谁、以及退出码最终如何被系统解释。它们共同决定了一个节点崩溃时,系统是局部恢复还是整体失控。
重启策略要匹配失败类型
Restart=always 听起来最稳妥,实则会掩盖配置错误。如果节点因为参数非法而每次启动都立刻退出,always 会把它变成一个持续重启的循环,日志被反复刷屏,真正原因反而更难发现。
更常见的选择是 Restart=on-failure,它只在非正常退出时重启。配合 StartLimitIntervalSec 与 StartLimitBurst,可以限制短时间内重启次数,避免无限循环:
1 | [Service] |
这几个值的含义需要和节点行为对齐。如果节点在配置错误时以 0 退出,on-failure 就不会重启,问题会被静默接受;如果节点在所有错误路径上都以非零退出,那么一个暂时的设备占用会触发重启。退出码应当是节点对外契约的一部分,而不是随手写 return -1。
进程组决定停止时谁会收到信号
ROS 2 节点经常以 launch 或脚本形式启动子进程。默认情况下,停止服务时 systemd 会终止整个控制组,这在大多数场景下是正确的。但如果启动脚本自己管理子进程,KillMode 的选择就会影响资源释放。
1 | systemctl show my-robot.service -p KillMode -p TimeoutStopSec |
TimeoutStopSec 之后 systemd 会升级为强杀。对于持有 SocketCAN 句柄、串口或 CAN 设备的进程,强杀意味着没有机会执行安全停止。因此清理时间必须留够,而安全动作本身最好由设备侧看门狗独立兜底,而不是只依赖进程优雅退出。
依赖顺序不等于依赖就绪
After= 和 Wants= 描述的是启动顺序和依赖关系,不表示被依赖的服务已经可以工作。这一点和 launch 的延时问题同源:顺序被表达出来了,条件没有。
如果确实需要等待条件,常见做法是在 unit 里使用健康检查命令配合 ExecStartPre,或者让上层节点自己重试连接,而不是依赖一个固定延时。重试要有限次并且有明确失败出口,否则又会退化成无限等待。
权限与实时能力应显式声明
实时任务需要的能力不应该通过“用 root 跑”来解决。systemd 提供了更窄的授权方式:
1 | [Service] |
这些声明也需要被验证。写进 unit 不代表实际生效,容器、cgroup 版本和内核配置都可能改变结果。验证方式是运行后回读进程的真实限制,而不是假设配置已经生效。
日志和退出码是最终证据
1 | journalctl -u my-robot.service -n 50 --no-pager |
判断一次故障时,至少要能回答:进程以什么退出码结束、是被信号终止还是主动退出、重启了几次、最后一次重启前发生了什么。如果这些信息只能靠翻应用日志,说明单元配置还没有把运行状态完整暴露出来。
参考资料
证据边界
本文讨论 systemd 的托管语义与配置选项,不包含任何具体设备上的实测恢复时间。重启策略、超时和权限配置都需要在目标系统上验证实际行为;“服务能自启”也不等于“故障能恢复”,安全相关的停止动作应另有独立机制。