当前位置:首页>Linux>嵌入式 Linux — 根文件系统到底是怎么跑起来的?

嵌入式 Linux — 根文件系统到底是怎么跑起来的?

  • 2026-10-11 06:49:28
嵌入式 Linux — 根文件系统到底是怎么跑起来的?

板块:嵌入式 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 决定什么时候触发,常用的几个:

action
含义
sysinit
启动最早,同步执行,结束前不往下走
wait
执行并等待完成,之后继续
once
执行一次,不等
respawn
进程退出就重启,getty 常用这个
askfirst
类似 respawn,但先提示按回车,调试方便
shutdown
关机时执行
ctrlaltdel
Ctrl+Alt+Del 触发

一个实际用的 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 的核心差异:

mdev
udev
体积
几十 KB,BusyBox 内置
几 MB,独立守护进程
配置语法
/etc/mdev.conf
,正则
/etc/udev/rules.d/*.rules
热插拔机制
写 /proc/sys/kernel/hotplug,内核直接调用
netlink socket,udevd 常驻监听
调试手段
基本没有,靠 dmesg
udevadm test
 / udevadm monitor

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. 1. Linux 内核文档:init 进程查找 — https://www.kernel.org/doc/html/v6.0/admin-guide/init.html
  2. 2. Linux 内核源码:init/main.c — https://github.com/torvalds/linux/blob/master/init/main.c
  3. 3. Buildroot 用户手册:init system — https://buildroot.org/downloads/manual/manual.html
  4. 4. Bootlin 嵌入式 Linux 培训讲义 — https://bootlin.com/doc/training/embedded-linux/
  5. 5. BusyBox 官方文档 — https://busybox.net/about.html
  6. 6. FHS v3.0 — https://refspecs.linuxfoundation.org/FHS_3.0/fhs/index.html

最新文章

随机文章