板块:嵌入式 Linux
内核起来了,然后呢?
这个"然后"卡住了不少人。板子能进 U-Boot,内核启动日志刷了一屏,最后停在"Kernel panic - not syncing: No working init found",一脸懵。或者系统进来了,想加个开机自启的程序,翻了半天不知道该改哪个文件。或者 /dev 下面空空如也,程序打开设备节点直接失败。
这些问题根子都在同一个地方——根文件系统的启动链路。把这条链路搞清楚,上面那些问题就都有地方查了。
一、内核是怎么把控制权交出去的
内核完成自身初始化后,进入 init/main.c 里的 kernel_init() 函数,开始找用户空间的第一个进程来运行。查找逻辑直接看源码最清楚:[¹]
/* init/main.c,kernel_init() 函数,简化版 *//* 优先用 bootargs 里的 init= 参数 */if (execute_command) { ret = run_init_process(execute_command); if (!ret) return 0; panic("Requested init %s failed (error %d).", execute_command, ret);}/* 没指定就按顺序挨个试 */if (!try_to_run_init_process("/sbin/init") || !try_to_run_init_process("/etc/init") || !try_to_run_init_process("/bin/init") || !try_to_run_init_process("/bin/sh")) return 0;/* 全试完了还没有,panic */panic("No working init found. Try passing init= option to kernel.");
几个关键点:init= 参数在 U-Boot 的 bootargs 里设置,调试时可以直接写 init=/bin/sh 跳过正常启动流程,进一个裸的 shell 救场。默认查找顺序是 /sbin/init → /etc/init → /bin/init → /bin/sh,找到能执行的就停下来,全找不到才 panic。
BusyBox 构建的根文件系统里,/sbin/init 是指向 BusyBox 二进制的符号链接,BusyBox 检测到自己以 init 的名字被调用,就进入 init 模式。
[¹] 参考:内核源码 init/main.c;Linux 内核文档 "Explaining the No working init found message",https://www.kernel.org/doc/html/v6.0/admin-guide/init.html
二、根文件系统的目录结构
FHS(Filesystem Hierarchy Standard)定义了 Linux 各目录的职责,嵌入式系统用的是它的精简子集,最小可用结构大概是这样:
/├── bin/ # 基本命令(sh、ls、cp……),BusyBox 的符号链接群├── sbin/ # 系统命令(init、mdev、ifconfig……)├── lib/ # 动态链接库(libc.so、ld-linux.so……)├── usr/│ ├── bin/│ └── lib/├── etc/│ ├── inittab # BusyBox init 的主配置│ ├── fstab # 挂载表│ └── init.d/│ └── rcS # 系统初始化脚本├── dev/ # 设备节点,启动时由 mdev 填充,自身可以是空目录├── proc/ # 空目录,运行时挂载 procfs├── sys/ # 空目录,运行时挂载 sysfs├── tmp/ # 临时文件,通常挂 tmpfs├── var/ # 日志、运行时文件└── mnt/ # 外部存储挂载点
/proc 和 /sys 目录本身什么都没有,必须运行时挂载,挂上去之后内核才会通过这两个接口向用户空间暴露信息。ps、top、udevadm 这些命令,底层都在读 /proc,不挂就用不了。
三、BusyBox init 靠 inittab 驱动
BusyBox init 没有 SysVinit 的运行级别,也没有 systemd 的依赖关系图,设计上故意保持简单:读 /etc/inittab,按里面每行的配置执行对应动作,仅此而已。[²]
inittab 每行格式:
id:runlevel:action:process
id 是关联的终端设备,留空用 /dev/console。runlevel 字段 BusyBox 直接忽略,写了也没用。action 决定什么时候触发,常用的几个:
| |
|---|
sysinit | |
wait | |
once | |
respawn | |
askfirst | |
shutdown | |
ctrlaltdel | |
一个实际用的 inittab:
# /etc/inittab# 系统初始化,最先跑,跑完再往下::sysinit:/etc/init.d/rcS# 串口控制台,调试用,不要求登录console::askfirst:-/bin/sh# 生产环境改成这个,要求登录# ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100::ctrlaltdel:/sbin/reboot::shutdown:/bin/umount -a -r::shutdown:/sbin/swapoff -a
-/bin/sh 里那个 - 是让 shell 以登录模式启动(会读 .profile),和直接跑 /bin/sh 有区别。
如果根文件系统里压根没有 inittab,BusyBox init 会用内置默认值,等价于:
::sysinit:/etc/init.d/rcS::askfirst:-/bin/sh::ctrlaltdel:/sbin/reboot::shutdown:/bin/umount -a -r
[²] 参考:BusyBox 源码 init/init.c;Buildroot 用户手册 BusyBox init 章节,https://buildroot.org/downloads/manual/manual.html
四、rcS 脚本:系统初始化的核心
/etc/init.d/rcS 是 sysinit 触发的第一个脚本,板子能不能正常跑起来,很大程度上取决于这里写了什么。下面是一个带注释的完整版本:
#!/bin/sh# /etc/init.d/rcSPATH=/sbin:/bin:/usr/sbin:/usr/binexport PATHumask 022# proc 和 sys 是内核信息接口,不挂很多工具都废了mount -t proc none /procmount -t sysfs none /sys# 挂 devtmpfs,让内核在 /dev 下创建基础设备节点# 前提:内核开了 CONFIG_DEVTMPFS# 如果同时开了 CONFIG_DEVTMPFS_MOUNT,内核启动时会自动挂,# 这行会返回"already mounted",不影响后续流程,可以保留mount -t devtmpfs none /dev 2>/dev/null || true# devpts:SSH、telnet、screen 的伪终端需要这个mkdir -p /dev/ptsmount -t devpts devpts /dev/pts -o mode=0622# tmpfs 挂在 /tmp 和 /var,写操作不落 Flashmount -t tmpfs none /tmpmount -t tmpfs none /varmkdir -p /var/log /var/run /var/tmp# 注册 mdev 为热插拔处理程序echo /sbin/mdev > /proc/sys/kernel/hotplug# 扫描 sysfs,补全 /dev 下的设备节点/sbin/mdev -s# 挂载 /etc/fstab 里的其他条目# 注意:fstab 里不要重复写上面已经手动挂过的 proc/sys/dev,# 否则 mount -a 会报"already mounted"(虽然不影响功能,但不干净)mount -a# 配置 loopback,基本网络需要/sbin/ifconfig lo 127.0.0.1 netmask 255.0.0.0 up# 静态 IP 示例(按实际情况改)# /sbin/ifconfig eth0 192.168.1.100 netmask 255.255.255.0 up# /sbin/route add default gw 192.168.1.1 eth0# 动态 IP(需要 udhcpc)# /sbin/udhcpc -i eth0 &# 加载额外内核模块# modprobe g_serial# modprobe rtc-ds1307# 启动自定义应用# /usr/bin/myapp &echo "rcS done"
关于 mount -t devtmpfs 这里有个常见困惑:内核有两个相关配置,CONFIG_DEVTMPFS 是让内核能挂 devtmpfs,CONFIG_DEVTMPFS_MOUNT 是让内核启动时自动挂到 /dev。后者开了的话,rcS 里手动 mount 会碰到"already mounted"。加 2>/dev/null || true 让它静默通过就行,不必纠结。
五、/etc/fstab
格式:<设备> <挂载点> <类型> <选项> <dump> <fsck顺序>
嵌入式系统的 fstab 通常很短,最后两列(dump/fsck)基本都写 0:
# /etc/fstab# 如果 rcS 里已经手动挂了 proc/sys/dev,这里就不要写,避免 mount -a 报错# 如果想统一用 fstab 管理,就把 rcS 里的手动挂载删掉,只保留这里tmpfs /tmp tmpfs defaults 0 0tmpfs /var tmpfs defaults 0 0# eMMC 第二分区挂 /data(按实际设备名改)# /dev/mmcblk0p2 /data ext4 defaults,noatime 0 2
noatime 值得单独说一下:它禁止每次读文件时更新访问时间戳,对 SD 卡和 eMMC 来说可以减少不少写操作,嵌入式场景基本上都应该加。
六、BusyBox 和 GNU 工具的区别
BusyBox 把 300 多个常用命令塞进一个二进制,/bin、/sbin 下面那一堆命令都是指向这个二进制的符号链接,BusyBox 靠识别调用时的名字来决定执行哪个功能。[³]
ls -la /bin/ls# lrwxrwxrwx ... /bin/ls -> busyboxls -la /sbin/init# lrwxrwxrwx ... /sbin/init -> ../bin/busybox# 查看这份 BusyBox 编译了哪些命令busybox --list
整套 BusyBox 编译出来通常 1~2 MB,而 GNU 工具链完整安装几十 MB,差距就在这——BusyBox 每个命令只实现最常用的子集,很多 GNU 的选项根本不支持。
这里有个坑经常踩:从 PC 上写好的 shell 脚本复制到板子上跑不起来,往往不是脚本本身的问题,而是用了 bash 特性,而板子上的 /bin/sh 是 ash。脚本开头不要写 #!/bin/bash,写 #!/bin/sh,并且不用 bash 扩展语法(比如 [[ ]]、$'...'、${var//pattern/replacement} 这类)。
[³] 参考:BusyBox 官方文档,https://busybox.net/about.html
七、Buildroot 的 init.d 分发机制
用 Buildroot 构建的系统,/etc/init.d/rcS 本身只是个分发器:
#!/bin/sh# Buildroot 默认生成的 rcSfor i in /etc/init.d/S??* ; do [ ! -f "$i" ] && continue case "$i" in *.sh) . "$i" ;; # .sh 文件在当前 shell 环境里执行,能修改环境变量 *) "$i" start ;; # 其他文件作为独立进程启动,传 start 参数 esacdone
S??* 匹配以 S 加两位数字开头的文件,数字决定顺序,越小越早。一个典型的 init.d 目录:
/etc/init.d/├── rcS├── S01logging # syslog├── S10mdev # 设备节点├── S20urandom # 随机数种子├── S40network # 网络├── S50dropbear # SSH└── S99myapp # 自己的应用,排最后
要加开机自启程序,新建一个 S?? 脚本就行:
#!/bin/sh# /etc/init.d/S99myappDAEMON=/usr/bin/myappPIDFILE=/var/run/myapp.pidcase "$1" in start) echo "Starting myapp..." start-stop-daemon -S -q -m -p "$PIDFILE" -b -x "$DAEMON" ;; stop) echo "Stopping myapp..." start-stop-daemon -K -q -p "$PIDFILE" ;; restart) "$0" stop sleep 1 "$0" start ;; *) echo "Usage: $0 {start|stop|restart}" exit 1esac
加执行权限:chmod +x /etc/init.d/S99myapp。
start-stop-daemon 是 BusyBox 内置的,-S 启动、-K 停止、-b 后台运行、-m -p <pidfile> 记录 PID。
八、mdev:BusyBox 的设备管理器
之前写 udev 那篇提到过,资源受限的板子(iMX6ULL + Buildroot 这类)默认用 mdev 而不是 udev。mdev 的配置文件是 /etc/mdev.conf,语法完全不同:
# /etc/mdev.conf# 格式:设备名正则 属主:属组 权限 [符号链接/重命名] [@添加时|$删除时|*两者都 命令]null root:root 666zero root:root 666random root:root 444urandom root:root 444console root:root 600tty root:root 666tty[0-9]* root:root 660ttyS[0-9]* root:root 660# USB 串口:匹配到时额外创建一个固定名字的符号链接ttyUSB[0-9]* root:root 660 >ttyUSB# SD 卡分区:插入时触发挂载脚本mmcblk[0-9]p[0-9]* root:root 660 * /etc/mdev/mmc_insert.sh# 其他设备默认权限.* root:root 660
@、$、* 分别是"插入时执行"、"拔出时执行"、"插拔都执行",后面跟脚本路径。
mdev 和 udev 的核心差异:
| | |
|---|
| | |
| /etc/mdev.conf | /etc/udev/rules.d/*.rules |
| 写 /proc/sys/kernel/hotplug,内核直接调用 | netlink socket,udevd 常驻监听 |
| | udevadm test |
mdev 没有持续运行的守护进程,热插拔事件发生时内核直接 fork 出 mdev 执行一次然后退出,开销小但功能也有限。
九、启动问题排查
"Kernel panic - not syncing: No working init found"
内核找不到能执行的 init 程序。这个错误只有几种可能:
# 根文件系统没挂上——看内核日志里有没有这行# VFS: Mounted root (ext4 filesystem) readonly on device 179:2# 没有这行说明 rootfs 挂载失败,检查 bootargs 里的 root= 参数和分区格式# 根文件系统挂上了但 /sbin/init 不存在或没有执行权限ls -la /sbin/init # 应该是指向 busybox 的符号链接ls -la /bin/busybox # 确认 busybox 本体在# busybox 是动态链接的,lib 下没有对应的 so 文件file /bin/busybox# 如果显示 "dynamically linked",检查 /lib 下有没有 libc.so 之类的
/dev 下设备节点不全
# 手动触发 mdev 重新扫描/sbin/mdev -s# 确认 hotplug 已经指向 mdevcat /proc/sys/kernel/hotplug# 应该输出 /sbin/mdev# 看 sysfs 里这个设备内核有没有识别ls /sys/class/tty/ls /sys/bus/usb/devices/
rcS 跑失败,系统直接进了一个裸 shell
BusyBox init 的 sysinit 失败不会 panic,它继续处理后面的 askfirst/respawn 条目,所以可能看到系统"起来了"但没有正常初始化。在这个 shell 里手动排查:
sh -x /etc/init.d/rcS# -x 会把每条命令和执行结果都打印出来,看哪行出错
S?? 脚本没有被执行
不用怀疑 rcS 的逻辑,先查这两个:
# 1. 有没有执行权限(这个坑踩过不止一次)ls -l /etc/init.d/S99myapp# 2. 有没有 Windows 换行符(从 Windows 上传过来的脚本容易这样)file /etc/init.d/S99myapp# 显示 "with CRLF line terminators" 就用 dos2unix 转一下dos2unix /etc/init.d/S99myapp
十、动态库:手动搭根文件系统最容易忽略的坑
BusyBox 通常静态编译,一个二进制包含所有工具,不依赖额外的 so 文件。但自己写的应用程序几乎都是动态链接的——运行时找不到对应的 .so,报的是"No such file or directory",而不是更直观的"library not found",第一次碰到通常要绕一圈才想到原因。
在开发机上查清楚依赖,然后再复制:
# 查看需要哪些动态库arm-linux-gnueabihf-readelf -d myapp | grep NEEDED# NEEDED libstdc++.so.6# NEEDED libm.so.6# NEEDED libc.so.6# 从工具链 sysroot 里找到这些库find /usr/arm-linux-gnueabihf/lib/ -name "libstdc++.so*"# 复制到根文件系统(-a 保留符号链接,不能省)cp -a /usr/arm-linux-gnueabihf/lib/libstdc++.so* rootfs/lib/cp -a /usr/arm-linux-gnueabihf/lib/libm.so* rootfs/lib/cp -a /usr/arm-linux-gnueabihf/lib/libc.so* rootfs/lib/cp -a /usr/arm-linux-gnueabihf/lib/ld-linux*.so* rootfs/lib/
还有一个容易漏的:ld-linux.so(动态链接器本身)。没有它,动态链接的程序直接起不来,报错和缺库一样,但原因完全不同。Buildroot 构建时会自动处理这些,手动搭就得自己盯着。
小结
从内核交棒到系统就绪,整条链路是:
kernel_init() → 找 /sbin/init │ ▼BusyBox init → 读 /etc/inittab │ ▼sysinit → /etc/init.d/rcS ├─ 挂 proc / sys / dev ├─ mdev -s 创建设备节点 ├─ mount -a 处理 fstab └─ 拉起 S?? 服务脚本 │ ▼askfirst / respawn → getty 或 shell
"系统起不来"这类问题,按这条链路一段一段排查就有方向:内核 panic 看 rootfs 挂没挂上、init 找没找到;init 起了查 inittab 和 rcS;rcS 跑了查 mdev 和设备节点;节点有了查应用程序和动态库。每段都有迹可循,不用靠猜。
参考资料
- 1. Linux 内核文档:init 进程查找 — https://www.kernel.org/doc/html/v6.0/admin-guide/init.html
- 2. Linux 内核源码:
init/main.c — https://github.com/torvalds/linux/blob/master/init/main.c - 3. Buildroot 用户手册:init system — https://buildroot.org/downloads/manual/manual.html
- 4. Bootlin 嵌入式 Linux 培训讲义 — https://bootlin.com/doc/training/embedded-linux/
- 5. BusyBox 官方文档 — https://busybox.net/about.html
- 6. FHS v3.0 — https://refspecs.linuxfoundation.org/FHS_3.0/fhs/index.html