BusyBoxinit.d脚本:不是“凑合能用”,而是“必须精准控制”的启动契约
你有没有遇到过这样的现场?
工业网关上电后,应用进程反复崩溃,日志里只有一行connect: Network is unreachable;
车载终端 OTA 升级后,DBus 总线没起来,整个 HMI 黑屏,但/etc/init.d/S50dbus文件明明存在;
IoT 设备在 -30℃ 低温启动失败,调试发现S10network比S01logging晚执行了整整三秒——而日志模块恰恰是网络配置的前置依赖。
这不是玄学,也不是硬件问题。
这是 BusyBox 的init.d启动机制被当成“类 SysV 兼容层”来用,却忽略了它本质是一套基于字符串排序、无状态、零容错的启动契约系统。
它的设计哲学非常朴素:不猜、不等、不依赖、不恢复。
你给它一个S01,它就信你真需要第一个跑;你给它一个s10(小写 s),它就当没看见;你漏掉x权限,它连打开文件的动作都不会做——不是报错,是彻底忽略。
下面,我们抛开所有“类比 Systemd”“兼容 LSB”的模糊表述,直接从init_main()的调用栈出发,讲清楚:BusyBox 的init.d到底在做什么、为什么只能这么写、以及写错一个字符会引发什么连锁反应。
它到底怎么启动?——从init_main()到S99app的真实路径
BusyBox 的init不是解释器,它是个“脚本分发器”。它的核心逻辑只有三步:
- 定位入口:先找
/etc/init.d/rcS,有则执行并退出该阶段;没有,则进入/etc/init.d/扫描模式; - 严格筛选:遍历目录,只认
S+两个 ASCII 数字字符(S00到S99)开头的可执行文件; - 字典序执行:对匹配到的文件名调用
strcmp()排序,然后fork()+exec()逐个拉起——不做任何依赖检查、不捕获返回值、不重试、不超时。
这就意味着:
✅S01logging和S99app是它唯一关心的“顺序信号”;
❌# Required-Start: $network这类 LSB 注释,对它而言和注释里的// TODO一样无意义;
⚠️S9network看似比S10network小,但在strcmp("S9", "S10")中,'9' > '1',所以S9会排在S10之后执行——这是一个真实的、可复现的竞态源头。
我们来看一段更贴近实际调试场景的源码逻辑(BusyBox v1.36.1,init.c):
// init_main() 中关键分支 if (ENABLE_INIT_SCRIPTS) { // 若未执行 rcS,则遍历 /etc/init.d/ if (access("/etc/init.d/rcS", X_OK) != 0) { run_actions(ACTION_BOOT); // → 最终调用 run_script("/etc/init.d/", "S", NULL) } }而run_script()的筛选逻辑,比文档写的更“冷酷”:
// libbb/run_applet.c if (entry->d_name[0] == 'S' && isdigit(entry->d_name[1]) && isdigit(entry->d_name[2]) && (suffix == NULL || endswith(entry->d_name, suffix))) { // 只有完全满足这四条,才进入 exec 流程 }注意:它不检查第4位是否为字母或点号,也