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 跳转后,会经历:
- 解压自身(如果是压缩内核,比如 zImage)。
- 架构初始化:设置页表、中断向量、时钟、定时器。
- 驱动初始化:按设备树描述,逐个 probe 驱动。存储控制器(如 MMC、NAND)必须在这步完成初始化。
- 挂载 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 的登场时机画清楚了:
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 的角色开始。