1. 嵌入式系统启动流程全景:从硬件上电到用户空间,rootfs的角色与定位

做嵌入式系统优化这么多年,我经常被问到同一个问题:「系统启动到底慢在哪?」

很多人一上来就盯着内核裁剪、驱动加载时间。这当然没错。但说实话,如果你不理解整个启动链条里每个环节的职责,尤其是 rootfs 扮演的角色,你很难找到真正的瓶颈。

这一章,我们先拉通全景。从按下电源键到用户空间出现,看看每一步发生了什么,rootfs 又是在哪个节点登场的。

1.1 启动五阶段:一条完整的生命线

我把嵌入式启动拆成五个阶段。你想想看,这其实很像一个人从起床到开始工作:

阶段 核心动作 耗时特征 rootfs 参与度
Stage 1 硬件上电 POR、PLL 锁相、DDR 初始化 固定,几十 ms 无
Stage 2 Bootloader SPL → U-Boot,加载内核 可变,100ms~1s 无(仅传递参数)
Stage 3 内核启动 解压、初始化、驱动加载 可变,200ms~2s 无(挂载前)
Stage 4 挂载 rootfs VFS 识别、超级块读取、init 进程启动 关键瓶颈 核心角色
Stage 5 用户空间 init 执行、服务启动、应用就绪 可变,500ms~数秒 持续依赖

关键认知:Stage 4 是「冷启动」中最容易被忽视的瓶颈。内核启动再快,如果 rootfs 挂载慢,整体体验一样拉胯。

1.2 硬件上电到 Bootloader:那些「看不见」的初始化

按下电源键那一刻,发生了什么?

  • POR(Power On Reset):芯片内部复位,所有寄存器回到默认值。
  • PLL 锁相:从外部晶振(比如 24MHz)倍频到 CPU 主频。这个时间取决于 PLL 的稳定时间,一般在几十微秒到几毫秒。
  • DDR 初始化:这是硬件阶段最耗时的部分。DDR 控制器需要训练时序、校准阻抗。我在一个 i.MX6 项目上遇到过,DDR 初始化花了 80ms,换了一颗不同品牌的 DDR3 颗粒后,时序参数变了,又得重新调。

嗯,这里要注意:硬件阶段的耗时基本是固定的,你很难通过软件优化来压缩。但你可以通过 Bootloader 的早期输出来判断——如果串口打印在 DDR 初始化之后才出现,那前面那段「黑屏时间」就是硬件在忙。

1.3 Bootloader 的使命:把内核请进内存

Bootloader 分两阶段:SPL(或 MLO)和 U-Boot proper。

  • SPL:非常小,负责初始化最基本的时钟和 DDR,然后把 U-Boot proper 从存储介质(NAND、eMMC、SD 卡)加载到 DDR。
  • U-Boot proper:功能完整,提供命令行、环境变量、网络启动等。最终它把内核镜像和设备树(DTB)加载到内存,然后跳转到内核入口。

个人习惯:我一般会在 U-Boot 里加一个 bootstage 测量,精确记录每个子阶段的耗时。这样你就能知道「加载内核花了 120ms,解压花了 80ms」,而不是靠猜。

Bootloader 阶段,rootfs 还没出现。但 U-Boot 会通过 bootargs 环境变量告诉内核:rootfs 在哪、用什么文件系统、挂载参数是什么。比如:

setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk0p2 rootfstype=ext4 rootwait rw'

这个字符串,就是内核在 Stage 4 挂载 rootfs 时的「导航地图」。

1.4 内核启动:VFS 的诞生

内核被 Bootloader 跳转后,会经历:

  1. 解压自身(如果是压缩内核,比如 zImage)。
  2. 架构初始化:设置页表、中断向量、时钟、定时器。
  3. 驱动初始化:按设备树描述,逐个 probe 驱动。存储控制器(如 MMC、NAND)必须在这步完成初始化。
  4. 挂载 rootfs:这是内核启动的最后一步。内核调用 prepare_namespace(),根据 root= 参数找到块设备,读取超级块,挂载到根目录 /。

这里有个容易踩的坑:如果 rootfs 所在的存储设备驱动还没初始化完成,内核就会挂载失败,然后 panic。所以很多方案里会加 rootwait 参数,让内核等一等。

我曾经在一个项目里忘了加 rootwait,结果 eMMC 初始化比内核挂载 rootfs 慢了几十毫秒,系统反复重启。查了两天才找到原因——从那以后,我所有项目都默认加 rootwait。

1.5 rootfs 的角色与定位:不只是「文件系统」

很多人把 rootfs 简单理解成「放文件的磁盘分区」。其实它的角色远不止这些:

  • 进程的起点:内核挂载 rootfs 后,会执行 /sbin/init(或 /init)。这是用户空间的第一个进程,PID 为 1。没有 rootfs,内核就不知道启动哪个程序。
  • 系统调用的桥梁:用户空间的程序通过系统调用访问内核服务。而系统调用的很多实现(比如打开文件、创建进程)都依赖 VFS 层,VFS 层又依赖 rootfs 提供的根目录结构。
  • 动态库的仓库:几乎所有用户空间程序都依赖动态链接库(如 libc.so、libm.so)。这些库必须放在 rootfs 的 /lib 或 /usr/lib 下。如果 rootfs 挂载失败,连 ls 命令都跑不起来。
  • 设备节点的家园:在 devtmpfs 出现之前,设备节点需要静态创建在 /dev 目录下。即使现在用了 devtmpfs,/dev 目录本身也是 rootfs 的一部分。

一句话总结:rootfs 是内核与用户空间之间的「着陆场」。没有它,内核再强大也只是空中楼阁。

1.6 一张图看懂启动流程

下面这张 SVG 图,把整个启动链条和 rootfs 的登场时机画清楚了:

嵌入式系统启动流程与 rootfs 角色 Stage 1 硬件上电 / POR Stage 2 Bootloader Stage 3 内核启动 Stage 4 挂载 rootfs Stage 5 用户空间 rootfs 在 Stage 4 的核心职责 • 提供根目录结构(/bin, /sbin, /etc, /lib, /dev...) • 提供 /sbin/init 或 /init 作为 PID 1 进程 • 提供动态链接库(libc.so, ld-linux.so...) • 提供设备节点(/dev/console, /dev/null...) 优化启动的核心思路:压缩 Stage 2~4 的耗时,尤其是 rootfs 的挂载与初始化 t=0 ~50ms ~300ms ~800ms ~1.5s+ 硬件初始化 Bootloader 内核启动 rootfs + 用户空间

1.7 启动时间都去哪了?

我习惯用 printk.time 和 initcall_debug 来抓内核启动的详细时间戳。下面是一个典型嵌入式 Linux 系统的启动耗时分布:

阶段 典型耗时 优化空间
硬件上电 + DDR 初始化 30~80 ms 低(硬件决定)
Bootloader(SPL + U-Boot) 100~500 ms 中(裁剪驱动、减少延迟)
内核解压 + 初始化 200~800 ms 中(裁剪内核、使用 XIP)
挂载 rootfs 50~500 ms 高(文件系统类型、存储介质)
init 进程 + 服务启动 300 ms ~ 数秒 高(并行化、延迟加载)

我建议:拿到一个新板子,第一件事就是测量完整的启动时间线。用示波器抓 GPIO 电平变化,或者用内核的 bootconfig 和 ftrace。数据说话,比感觉靠谱得多。

1.8 本章小结

嵌入式系统启动,是一条从硬件到软件的链条。rootfs 在 Stage 4 登场,但它影响的不只是挂载那几百毫秒——它决定了 init 进程能否启动、动态库能否加载、应用能否运行。

说白了,rootfs 是整个用户空间的基石。优化启动,必须从理解 rootfs 的角色开始。