4. init脚本核心概念
在定制 init 脚本的工程实践中,真正决定系统能否“快速且正确”启动的,并不是脚本写得多短,而是对五个核心概念的把握:运行级别、服务依赖、启动顺序、并行启动和守护进程管理。它们共同构成 init 脚本的调度骨架。理解这五者之间的关系,才能避免“启动快了但服务不可用”或“服务都起来了但耗时过长”的两难局面。
一、运行级别:系统状态的离散切面
运行级别(Runlevel)是 init 对系统整体状态的一种离散划分。传统 SysV init 用 0~6 表示不同状态,例如 0 关机、1 单用户、3 多用户文本、5 图形界面、6 重启。定制 init 脚本时,运行级别本质上是“一组服务集合的标签”:进入某个级别,就意味着启动该级别对应的服务集合,并停止不属于该级别的服务。
在嵌入式或精简系统中,运行级别常被简化为两到三个,例如 boot(基础运行)、normal(完整业务)、recovery(恢复模式)。关键点在于:运行级别必须与目录或配置表绑定,init 才能知道“进入该级别要做什么”。
# 典型的运行级别目录约定
/etc/rc.d/rc0.d/ # 关机
/etc/rc.d/rc1.d/ # 单用户
/etc/rc.d/rc3.d/ # 多用户文本
/etc/rc.d/rc5.d/ # 图形界面
# 目录内脚本命名规则:Sxx 启动,Kxx 停止
S10network # 第10个启动
S20syslog # 第20个启动
K90network # 停止时第90个执行
二、服务依赖与启动顺序:谁必须先于谁
服务依赖描述的是“服务之间的前置条件”。例如网络服务依赖网卡驱动,业务服务依赖网络和日志服务。启动顺序则是依赖关系在时间轴上的线性化结果。依赖是逻辑关系,顺序是执行安排,两者不能混为一谈。
如果只按脚本编号排序,很容易出现“编号小的服务依赖编号大的服务”这种矛盾。因此定制 init 脚本时,通常需要显式声明依赖,再由 init 计算拓扑顺序。常见做法是:每个服务声明 REQUIRES、AFTER、BEFORE 三类元数据。
- REQUIRES:强依赖,被依赖服务失败则本服务不应启动。
- AFTER:顺序依赖,仅要求在被依赖服务之后启动,不要求其成功。
- BEFORE:反向顺序约束,用于提前占位。
三、并行启动:用依赖换时间
串行启动的总耗时等于各服务启动时间之和,而并行启动的总耗时取决于关键路径。并行启动的核心思想是:没有依赖关系的服务可以同时启动,从而压缩整体启动时间。但并行会带来三个新问题:日志交错、资源竞争、依赖判定复杂。
因此并行启动通常配合“就绪通知”机制使用。服务启动后不是简单 fork 就返回,而是通过 socket、FIFO 或 sd_notify 通知 init “我已就绪”。init 收到通知后才释放依赖它的后继服务。
# 并行启动的依赖释放示意
启动 A、B(无依赖)───┬──> A 就绪 ──> 启动 C(依赖 A)
└──> B 就绪 ──> 启动 D(依赖 B)
C、D 均就绪 ──> 启动 E(依赖 C、D)
四、守护进程管理:启动之后还要管住
守护进程管理的目标是:服务不仅能启动,还能被监控、重启和优雅停止。定制 init 脚本常见的管理动作包括:
- 启动:fork 子进程并脱离终端,写入 PID 文件。
- 状态检查:通过 PID 文件或进程名判断是否存活。
- 重启:进程异常退出后按策略拉起,避免无限快速重启。
- 停止:先发 SIGTERM,超时后发 SIGKILL,并清理 PID 文件。
需要特别注意的是“双 fork”与 PID 文件的一致性。如果守护进程自己 daemon 化,父进程退出,init 拿到的 PID 可能已经失效。稳妥做法是由 init 负责后台化,或要求服务在前台运行并由 init 托管。
五、五者关系总览
下图把运行级别、服务依赖、启动顺序、并行启动和守护进程管理放在同一张关系图中:运行级别决定服务集合,依赖决定约束,顺序与并行决定调度策略,守护进程管理决定启动后的生命周期。
六、案例场景:一次“启动快但业务失败”的复盘
某设备定制 init 脚本后,启动时间从 18 秒降到 9 秒,但业务服务偶发连接数据库失败。排查发现:数据库服务与业务服务被并行启动,而业务服务只声明了 AFTER=network,没有声明 REQUIRES=database。并行调度器认为两者无依赖,于是同时拉起。数据库尚未完成监听,业务服务已经发起连接。
修复方式不是简单改回串行,而是补充依赖声明,并让数据库服务在就绪后通过通知机制释放业务服务。这样既保留了并行收益,又保证了正确性。
| 概念 | 解决的问题 | 典型配置/机制 | 常见误区 |
|---|---|---|---|
| 运行级别 | 系统状态切换 | rcX.d 目录、S/K 编号 | 级别与目录未绑定 |
| 服务依赖 | 前置条件约束 | REQUIRES / AFTER / BEFORE | 把顺序当依赖 |
| 启动顺序 | 时间轴线性化 | 拓扑排序、编号排序 | 编号与依赖冲突 |
| 并行启动 | 压缩启动时间 | 就绪通知、依赖释放 | 无依赖判定过粗 |
| 守护进程管理 | 启动后存活与恢复 | PID 文件、SIGTERM/SIGKILL | PID 文件与进程不一致 |
掌握这五个概念后,init 脚本就不再是一堆零散的启动命令,而是一个有状态、有约束、有调度、有生命周期的启动系统。后续无论是裁剪服务、调整顺序,还是引入并行,都可以在这套框架下有的放矢。