BusyBox init启动脚本与系统加速 - 第2节

init 进程启动流程:Linux 内核启动后第一个用户态进程

内核启动完,最后一步就是调用 /sbin/init。这时候,系统从内核态切换到用户态。说白了,内核把接力棒交给了 init 进程。init 的 PID 永远是 1,它是所有用户进程的祖先。

我刚开始做嵌入式时,以为 init 就是个花架子。直到有一次,产品在产线测试时频繁死机,查了两天才发现是 init 配置不当,导致关键服务没启动起来。嗯,从那以后我再也不敢小看这个 PID 1 了。

内核怎么找到 init 的?

内核在挂载根文件系统后,会按顺序尝试执行这几个路径:

  1. /sbin/init — 标准路径,大部分嵌入式系统用这个
  2. /etc/init — 备选
  3. /bin/init — 再备选
  4. /bin/sh — 最后的保底方案

如果全失败了,内核会 panic。你想想看,连个 shell 都起不来,这系统基本就废了。

我的习惯:在构建根文件系统时,我会在 /sbin/init 之外,再软链接一个 /bin/init 指向 busybox。万一主路径被误删,系统还能救回来。

Busybox init 的启动顺序

Busybox 的 init 比 SysV init 简单得多。它不跑 rc 脚本,不管理运行级别(至少不按传统方式)。它的核心逻辑就三步:

  • 读取配置文件 — 解析 /etc/inittab
  • 执行系统初始化 — 跑 sysinit 动作
  • 进入等待循环 — 处理 respawn、askfirst、once 等动作

下面这张图把整个流程串起来了:

Busybox init 启动流程 Busybox init 启动流程 内核启动完成 执行 /sbin/init 读取 /etc/inittab 执行 ::sysinit 动作 进入等待循环 处理 respawn / askfirst / once 重新读取 inittab 注意:Busybox init 不会像 SysV init 那样切换运行级别,它只按 inittab 配置执行动作 所有子进程退出后,init 会重新读取 inittab 并重启已退出的 respawn 进程

/etc/inittab 配置文件详解

inittab 是 Busybox init 的灵魂。没有它,init 就不知道要干什么。它的格式很简单,每行一个条目,用冒号分隔四个字段:

id:runlevels:action:process

我见过不少新手把 inittab 写成 shell 脚本,结果系统起不来。记住,它不是脚本,是配置表。

四个字段的含义

字段 含义 说明
id 唯一标识符 1-4 个字符,用于控制 tty 或作为进程标识。Busybox 中通常用 tty 名称,如 ttyS0
runlevels 运行级别 Busybox 忽略此字段,但为了兼容性可以留空或填任意值
action 动作类型 决定何时以及如何执行 process 字段指定的命令
process 要执行的命令 可以是可执行文件路径或 shell 命令
我曾经踩过的坑:id 字段不能重复。如果两个条目用了相同的 id,init 只会执行最后一个。我有一次在调试串口登录时,配了两个 ttyS0 条目,结果第二个覆盖了第一个,导致 getty 没起来。查了整整一下午。

八种 action 类型

这是 inittab 最核心的部分。不同的 action 决定了进程的启动时机和行为。我按使用频率排了个序:

action 触发时机 进程行为 典型用途
sysinit init 启动后立即执行 执行一次,等待完成 挂载文件系统、设置主机名、初始化网络
respawn 进程退出后自动重启 无限重启 getty 登录 shell、关键守护进程
askfirst 类似 respawn,但先打印提示 等待用户按 Enter 后才启动 串口控制台、调试终端
once 启动时执行一次 不等待完成,不重启 启动非关键服务、清理临时文件
wait 启动时执行一次 等待进程退出后才继续 初始化脚本、依赖后续步骤的任务
ctrlaltdel 收到 Ctrl+Alt+Del 信号 执行一次 重启或关机命令
shutdown 系统关机时 执行一次 卸载文件系统、关闭服务
restart init 收到 SIGHUP 信号 执行一次,通常用于重启 init 重新加载配置

一个典型的 inittab 实例

下面是我在项目中常用的模板。它覆盖了从系统初始化到用户登录的完整流程:

# /etc/inittab — Busybox init 配置文件
# 格式: id:runlevels:action:process

# 系统初始化 — 挂载文件系统、设置环境
::sysinit:/etc/init.d/rcS

# 串口控制台 — 按 Enter 才启动,适合调试
ttyS0::askfirst:/sbin/getty -L ttyS0 115200 vt100

# 虚拟终端 — 自动重启登录进程
tty1::respawn:/sbin/getty 38400 tty1

# 非关键服务 — 启动一次,挂了不管
::once:/usr/sbin/lighttpd -f /etc/lighttpd.conf

# 关机时执行 — 安全卸载
::shutdown:/sbin/swapoff -a
::shutdown:/bin/umount -a -r

# 重启时执行 — 重新挂载
::restart:/sbin/init

关键点:

  • sysinit 条目没有 id,用空字符串表示。Busybox 允许这样。
  • askfirst 和 respawn 的区别:前者会打印 "Please press Enter to activate this console.",然后等待用户按键。后者直接启动,不等待。
  • shutdown 条目按顺序执行,所以先关 swap 再卸载文件系统。

sysinit 的执行细节

sysinit 是系统初始化的入口。它通常指向一个 shell 脚本,比如 /etc/init.d/rcS。这个脚本里会做:

  • 挂载 /proc、/sys、/dev、/tmp
  • 设置主机名
  • 配置网络接口
  • 启动 udev 或 mdev 管理设备节点
  • 加载内核模块

我个人习惯在 rcS 脚本开头加一行 echo "=== rcS started ===" > /dev/console。这样如果系统卡在初始化阶段,至少能看到脚本有没有被执行。

避坑指南:sysinit 脚本里不要用 exit。如果脚本提前退出,init 会认为 sysinit 执行完毕,然后继续处理后续条目。我曾经在 rcS 里写了个 exit 1 用于错误处理,结果后面的 respawn 条目全跑了,系统状态一团糟。

respawn 与 askfirst 的选择

这两个 action 最容易混淆。我简单总结一下:

  • respawn:进程退出后立即重启。适合必须一直运行的服务,比如 getty、看门狗守护进程。
  • askfirst:进程退出后重启,但启动前先打印提示并等待用户按键。适合串口控制台,防止系统启动时自动弹出登录提示,干扰其他输出。

你想想看,如果串口上配了 respawn,系统启动时就会直接弹出 login 提示。但如果你在调试内核启动日志,这些提示会刷掉你的调试信息。用 askfirst 就能避免这个问题。

shutdown 和 restart 的注意事项

shutdown 条目在系统关机或重启时执行。Busybox init 会按 inittab 中 shutdown 条目的顺序依次执行。这里有个细节:shutdown 条目执行时,init 不会等待它们完成。所以如果你需要确保某个操作完成,最好在脚本里自己加同步机制。

restart 条目在 init 收到 SIGHUP 信号时触发。通常用来重新加载 inittab。我一般会在 restart 条目里写 /sbin/init,这样相当于重启 init 进程本身。

我曾经犯过的错:在 shutdown 条目里执行 killall -9 杀掉所有进程。结果 init 自己也被杀了,系统直接挂死。正确的做法是让 init 来管理进程生命周期,不要自己乱杀。

inittab 的调试技巧

inittab 配置错了,系统可能起不来。这时候怎么办?我分享几个经验:

  1. 内核参数加 init=/bin/sh:绕过 init,直接进 shell。然后手动执行 /sbin/init -i 查看 inittab 解析结果。
  2. 检查语法:Busybox init 对 inittab 的解析很严格。每行不能有多余空格,字段之间只能用冒号分隔。
  3. 用 strace 跟踪:如果系统能部分启动,用 strace -p 1 看 init 在干什么。
  4. 日志输出:在 sysinit 脚本里加 set -x,把执行过程输出到串口或日志文件。

嗯,这些技巧帮我省了不少时间。特别是第一条,简直是嵌入式调试的救命稻草。

总结一下

inittab 是 Busybox init 的配置核心。它用简单的四字段格式,定义了系统从启动到关机的完整行为。理解这八种 action 的差异,你就能精确控制每个进程的启动时机和生命周期。