4. rcS启动脚本分析:/etc/init.d/rcS脚本的作用,rcS的执行时机,rcS的基本结构

好,咱们进入第四讲。这一章我打算把 /etc/init.d/rcS 这个脚本彻底讲透。很多做嵌入式开发的朋友,可能天天跟它打交道,但真要问它什么时候被调用、里面该写什么、不该写什么,未必能说清楚。我当年刚入行时也踩过坑,有一次把网络初始化写在了文件系统挂载之前,结果系统启动到一半就卡死了……嗯,从那以后我就养成了仔细读 rcS 的习惯。

4.1 rcS 到底是什么?

rcS 的全称是 Run Command Script,它是 Busybox init 进程启动后执行的第一个用户态脚本。说白了,它就是整个用户空间的“总调度员”。

它的核心职责就三条:

  • 初始化基础环境:挂载虚拟文件系统、设置主机名、配置网络
  • 启动核心服务:比如 syslogd、telnetd、dropbear(SSH)
  • 执行用户自定义逻辑:加载驱动、启动应用程序

关键认知:rcS 不是内核的一部分,它是 init 进程 fork 出来的一个 shell 子进程。这意味着 rcS 里如果出现死循环或者阻塞调用,整个系统启动就会卡住。我见过不少产品因为 rcS 里放了一个 while true 的等待脚本,结果系统永远进不了 shell。

4.2 rcS 的执行时机

要理解 rcS 的执行时机,得先看 init 进程的启动流程。我画了一张图,你可以对照着看。

rcS 执行时机流程图 rcS 执行时机流程图 1. 内核启动 2. 挂载根文件系统 3. 执行 /sbin/init 4. 读取 /etc/inittab 5. 执行 ::sysinit: /etc/init.d/rcS 6. rcS 执行完成 关键时间点 rcS 在 init 进程 解析 inittab 后立即执行 属于 sysinit 动作 注意 rcS 执行期间 其他 inittab 动作会等待

从这张图你能看到,rcS 是在 init 进程读取 /etc/inittab 后,匹配到 ::sysinit:/etc/init.d/rcS 这一行时被调用的。它属于 sysinit 动作,这意味着:

  • rcS 在系统启动阶段最早被执行
  • rcS 执行期间,其他 respawn、askfirst 等动作不会被触发
  • rcS 必须正常退出,init 才会继续处理后续的 inittab 条目

我曾经在一个项目中,把 rcS 脚本里放了一个 sleep 30,目的是等某个硬件就绪。结果整个系统启动慢了半分钟,客户直接投诉。后来我改成用 polling 加超时机制,才把启动时间压下来。记住:rcS 里不要做长时间的阻塞等待。

4.3 rcS 的基本结构

一个标准的 rcS 脚本,结构其实很清晰。我拿一个实际项目中的例子来说明:

#!/bin/sh
# /etc/init.d/rcS - 系统初始化脚本
# 版本: 1.2
# 日期: 2024-03-15

# 设置 PATH
PATH=/sbin:/bin:/usr/sbin:/usr/bin
export PATH

# 挂载虚拟文件系统
mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs none /tmp
mount -t devtmpfs none /dev

# 设置主机名
hostname -F /etc/hostname

# 配置网络(如果存在配置文件)
if [ -f /etc/network.conf ]; then
    . /etc/network.conf
    ifconfig eth0 $IP_ADDR netmask $NETMASK up
    route add default gw $GATEWAY
fi

# 启动系统日志服务
syslogd

# 启动 SSH 服务(如果存在)
if [ -x /usr/sbin/dropbear ]; then
    dropbear
fi

# 加载用户自定义驱动
for module in /lib/modules/*.ko; do
    [ -f "$module" ] && insmod "$module"
done

# 启动主应用程序
if [ -x /usr/bin/myapp ]; then
    /usr/bin/myapp &
fi

# 返回成功
exit 0

这个脚本的结构,我把它拆成几个逻辑段:

段落 作用 注意事项
环境准备 设置 PATH、导出环境变量 PATH 要包含常用目录,否则后续命令可能找不到
虚拟文件系统挂载 挂载 proc、sysfs、tmpfs、devtmpfs 顺序不能乱,先挂 proc 再挂 sysfs
基础配置 主机名、网络、时区等 网络配置建议放在后面,避免依赖未就绪
服务启动 syslogd、dropbear、telnetd 等 建议用 & 后台运行,不要阻塞
用户自定义 驱动加载、应用启动 注意检查文件是否存在,避免报错
退出 exit 0 必须返回 0,否则 init 会认为初始化失败

4.4 我的一些经验总结

写 rcS 脚本这么多年,我总结了几条铁律:

  • 先挂载,再做事:proc、sysfs、tmpfs 必须在最前面挂载,否则很多工具用不了。我见过有人把 mount -t proc 写在后面,结果 ps 命令直接报错。
  • 能用后台就用后台:服务类程序(syslogd、dropbear)加上 &,让它们后台运行。rcS 只负责“启动”,不负责“守护”。
  • 检查文件存在性:用 [ -f ] 或 [ -x ] 判断文件是否存在,避免脚本执行到一半报错退出。
  • 不要写死路径:用变量代替硬编码路径,方便后期维护。
  • 加注释:你想想看,三个月后你自己回来看这个脚本,没有注释的话,你可能都忘了当时为什么这么写。

我个人习惯在 rcS 脚本开头加一个 set -e,这样任何一条命令失败都会让脚本立即退出。但要注意,有些命令(比如 ifconfig 配置网络)可能因为硬件未就绪而失败,这时候 set -e 反而会坏事。所以我的做法是:关键步骤用 set -e,非关键步骤手动加 || true 忽略错误。

4.5 常见陷阱与避坑

这里列几个我实际踩过的坑:

  • 陷阱一:rcS 里用 exit 1 —— 如果 rcS 返回非零值,init 会认为初始化失败,有些 Busybox 版本会直接重启系统。所以 rcS 最后一定要 exit 0。
  • 陷阱二:忘记挂载 /dev —— 没有 devtmpfs 或 mdev,设备节点无法自动创建,很多驱动加载后会找不到设备文件。
  • 陷阱三:网络配置放在最前面 —— 如果网络依赖某个驱动,而驱动加载在后面,网络配置就会失败。顺序很重要。
  • 陷阱四:在 rcS 里启动前台进程 —— 前台进程会阻塞 rcS 的执行,导致后续的 inittab 条目无法被处理。所有长期运行的服务都应该后台启动。