一个目录缺失让整机业务"假死":开机竞态排查与三层自愈的init设计
Contents
嵌入式产品最冤的一种坏:所有零件都是好的,软件也都在,就是开机那两秒的顺序不对,整机看起来像砖。这次的现象是烧完新固件"没有显示",最后定位到一个 tmpfs 目录偶发缺失,顺手把 init 脚本改成了三层自愈。这篇一半是排查实录,一半是设计。
一、现象
- 烧录新固件上电,面板能连上下位机,但歌名、状态全是占位符——面板活、主机"在",但什么都不说;
- 手动把
/etc/init.d/下几个服务 restart 一遍,全部恢复正常; - 用户视角是"烧录失败没有显示",真相是主机业务栈全灭。
“手动重启就好、开机必坏”——这个组合几乎直接剧透了答案:开机时序问题。
二、先踩 busybox ps 的坑
串口上去 ps 想看业务进程——空的!第一反应是业务二进制没进固件。翻 rootfs 镜像,二进制好端端都在。三轮误判之后才恍然:这版 busybox 的 ps 只显示挂在 ttyS0 上的进程,daemon 一个都看不见。
教训:查进程一律 /proc 直读——
|
|
这台设备的精简 busybox 连 head/tail 都没有,读文件得 dd | hexdump。工具不全的板子上,/proc 是唯一不会骗你的接口。
三、真相链:五张多米诺
/run/media/userdata不是块设备挂载,是 prepare-runtime(S15)在/run这个 tmpfs 里mkdir出来的目录;- 开机偶发:业务服务(S94 之后)起来的时候,这个目录还没就绪;
- MPD 启动第一件事是打开
/run/media/userdata/mpd.pid.db——打不开,秒崩; - procd 配的是
respawn 3600 5 5:重试 5 次、每次隔 5 秒,然后弃管(procd 不再拉起这个服务); - MPD 死透 → 上层的业务服务或连坐、或等锁,整栈全灭,且永不自愈。
每一环单看都"合理",合起来就是:一个目录晚到两秒,整机假死一辈子。
四、设计:三层自愈
设计目标一句话:服务可以晚启动,不允许死透。
第 1 层:启动前等待——把"崩溃"变"晚启动"
|
|
三个细节:
- 用
-w(可写)不用-e(存在):只读 rootfs 上如果有同名假目录,-e会放行然后照样崩;-w把这种假目录也拦住。 - 120 秒超时后放行:等待不能变成永远卡死启动,超时照常启动,让 respawn 层兜底。每层都要有出口。
- 每 10 秒打一行心跳到 stderr(logread 可见),现场定位一眼看出"卡在等谁"。
第 2 层:无限重生——把"5 次弃管"变"永远再试"
|
|
procd respawn 三个参数是 threshold timeout retry:3600 秒内崩溃视为异常重启、隔 5 秒再拉起、retry=0 表示无限次。OpenWrt 模板里常写 5——对桌面软件合理(防止疯狂刷日志),对无头嵌入式产品,“放弃服务"没有任何好处:反正没有人在现场敲命令重启它。
监控类服务(player-service-monitor)不做等待直接上——缺席检测本身就是它的职责,它不该有前置依赖。
第 3 层:依赖降级不连坐 + 末尾再断言
prepare-runtime 原来是串行 setup,private 分区(持久化增强件)挂载失败会中断整个引导。改成:
|
|
原则:基础件与增强件分级,增强件失败绝不连坐基础件。末尾那句看似多余的 mkdir -p 是防"中途被二次挂载遮蔽"的再断言——init 脚本里的幂等语句永远不嫌多。
五、复盘
| 坏味道 | 改法 |
|---|---|
| 启动即崩 | 启动前等待(带超时出口) |
| 重试 5 次弃管 | respawn retry=0 无限重生 |
| 依赖串行连坐 | 分级 + 降级警告 |
| 单点创建无兜底 | 末尾幂等再断言 |
还有一条隐藏收获:wait_userdata 的心跳日志成了最便宜的探针——以后任何"这次开机有没有发生等待"的疑问,logread | grep waiting 一行定案。
最后:修完的固件上板,开机两分钟内四个业务服务全部自起,/proc 枚举齐活——一个 init 脚本的改动,让这类"假死"从产品上永久退场。
Author 软件开发大郭
LastMod 2026-09-26