1. 启动流程全景:从按下电源键到控制台
嵌入式Linux的启动过程,说白了就是一段从硬件复位到用户空间的「接力赛」。我做了这么多年嵌入式,见过太多人只盯着自己那一亩三分地——搞内核的不管bootloader,搞驱动的不管根文件系统。结果出了问题,谁都不知道锅该谁背。
这一章,咱们把整个生命周期走一遍。你心里有了这张全景图,后面优化起来才知道刀往哪儿砍。
1.1 启动的五个阶段
我把整个启动拆成五个阶段,每个阶段都有明确的起点和终点。这样排查问题时,你能快速定位到「卡在哪一段」。
| 阶段 | 起点 | 终点 | 典型耗时 |
|---|---|---|---|
| Stage 0:BootROM | 上电复位 | 加载SPL/MLO | 50~200ms |
| Stage 1:SPL/MLO | SPL入口 | 加载U-Boot proper | 100~500ms |
| Stage 2:U-Boot proper | U-Boot入口 | 跳转到内核 | 200ms~2s |
| Stage 3:内核启动 | 解压内核 | 挂载根文件系统 | 500ms~3s |
| Stage 4:用户空间初始化 | /sbin/init | 控制台/应用就绪 | 300ms~5s+ |
关键认知:启动优化的收益是「越靠前越大」。你在BootROM里省100ms,比在内核里省500ms还值钱——因为前面的阶段是串行的,后面的阶段往往可以并行。
1.2 Stage 0:BootROM——芯片出厂就写死的代码
按下电源键那一刻,CPU从复位向量地址开始执行。这个地址指向的是芯片内部ROM,代码是芯片出厂时固化的,你改不了。
BootROM干三件事:
- 初始化最基础的硬件——时钟、SRAM、启动介质控制器(SD/NAND/SPI NOR)
- 读取启动选择引脚——决定从eMMC、SD卡还是UART启动
- 加载下一级bootloader到SRAM——通常是SPL(Secondary Program Loader)
我遇到过一个问题:某款芯片的BootROM读SD卡时默认用4位模式,但我们的板子上SD卡座只接了2根数据线。结果BootROM死活读不到SPL。后来查勘误表才发现,得在PCB layout时把另外两根线拉高才行。嗯,这种坑,芯片手册里通常不会明说。
调试技巧:大部分SoC的BootROM支持通过UART输出调试信息。比如TI的AM335x,拉低某个GPIO再上电,串口就会打印"CCCCC...",告诉你它在等XMODEM传输。看到这个输出,至少说明BootROM活了。
1.3 Stage 1:SPL/MLO——从SRAM到DDR的桥梁
SPL是U-Boot的精简版,体积被压缩到十几KB到几十KB,刚好塞进芯片内部SRAM。它的使命只有一个:初始化DDR控制器,然后把完整的U-Boot从存储介质搬到DDR里。
为什么不能直接跑完整的U-Boot?因为SRAM太小了,通常只有64KB~512KB。而U-Boot proper动辄几百KB,根本放不下。
SPL的执行流程:
- 设置CPU为SVC模式,关中断
- 初始化时钟和DDR控制器
- 从存储介质读取U-Boot proper到DDR
- 跳转到U-Boot proper入口
// SPL中典型的DDR初始化调用链(以TI AM335x为例)
board_init_f() → sdram_init() → config_ddr_pll() → config_ddr_ctrl() → wait_ddr_ready()
我个人习惯在SPL阶段加一个GPIO翻转的测量点。用示波器抓一下,就能精确知道SPL花了多少时间。有一次我发现SPL阶段DDR初始化花了300多ms,查了半天发现是DDR的ZQ校准参数配错了,导致校准循环超时。改对之后,直接降到80ms。
1.4 Stage 2:U-Boot proper——启动的「总导演」
U-Boot proper是嵌入式Linux启动中最灵活、也最容易「藏雷」的阶段。它负责:
- 初始化更多外设(网卡、USB、显示控制器)
- 加载设备树(DTB)
- 加载并校验内核镜像
- 设置内核启动参数(bootargs)
- 跳转到内核
U-Boot的启动流程大致是这样的:
_start → board_init_f() → relocate_code() → board_init_r() → main_loop()
→ run_command("bootcmd") → bootm → load_kernel() → boot_jump_linux()
这里有个很多人忽略的点:relocate_code这一步会把U-Boot自身从Flash拷贝到DDR的高地址运行。如果DDR初始化有问题,或者拷贝过程中ECC校验失败,U-Boot就会莫名其妙地死掉。我曾经在一个项目里遇到U-Boot随机崩溃,查了两天才发现是DDR的时序参数在高温下漂移了,导致relocate时数据出错。
避坑指南:U-Boot的bootdelay变量默认是2~5秒。很多产品发布时忘了把这个改成0,结果每次开机都干等几秒。我建议你在产品化之前,务必检查CONFIG_BOOTDELAY和bootcmd环境变量。
1.5 Stage 3:内核启动——从压缩镜像到进程模型
U-Boot跳转到内核时,CPU处于SVC模式,MMU和Cache都是关闭的。内核入口(stext)要做的第一件事就是建立页表、打开MMU。
内核启动的关键里程碑:
| 时间点 | 事件 | 内核日志标记 |
|---|---|---|
| T0 | 解压内核(如果使用压缩镜像) | Uncompressing Linux... |
| T1 | 架构初始化(setup_arch) | (无固定输出) |
| T2 | 控制台初始化 | console [ttyS0] enabled |
| T3 | 内核调度器初始化 | smp: Bringing up secondary CPUs... |
| T4 | 设备驱动初始化 | driver_probe_device |
| T5 | 挂载根文件系统 | VFS: Mounted root ... |
| T6 | 执行init进程 | Run /sbin/init as init process |
你想想看,从T0到T6,哪个阶段最耗时?我统计过几十个嵌入式项目,设备驱动初始化通常占内核启动时间的40%~60%。尤其是MMC/SD卡驱动、USB控制器、网络PHY的初始化,动不动就几百毫秒。
内核日志里有个小技巧:用initcall_debug参数启动,内核会打印每个初始化函数的执行时间。这样你一眼就能看出哪个驱动在「摸鱼」。
// 在bootargs里加上 initcall_debug
bootargs=console=ttyS0,115200 root=/dev/mmcblk0p2 initcall_debug
// 日志里会看到类似这样的输出
[ 0.456] initcall mmc_init+0x0/0x40 returned 0 after 234567 usecs
[ 0.789] initcall dwc2_driver_init+0x0/0x30 returned 0 after 123456 usecs
1.6 Stage 4:用户空间初始化——从init到你的应用
内核启动完,会执行根文件系统里的第一个用户态程序——/sbin/init。这个程序负责拉起所有用户空间服务。
不同的init系统,启动速度天差地别:
| Init系统 | 特点 | 启动速度 | 适用场景 |
|---|---|---|---|
| BusyBox init | 串行执行inittab | 快(但死板) | 极简系统 |
| System V init | 按运行级别串行启动 | 慢 | 兼容性要求高 |
| systemd | 并行启动、socket激活 | 快(但复杂) | 功能丰富的系统 |
| 自定义init | 手写启动脚本 | 最快 | 深度优化 |
我个人的经验是:如果你的产品对启动时间要求苛刻(比如车载中控要求2秒内出画面),别用systemd。它虽然并行能力强,但自身的初始化开销就不小。我见过一个项目,用BusyBox init + 手写启动脚本,把用户空间初始化从1.8秒压到了0.3秒。
1.7 全景流程图
下面这张图把整个启动流程串起来了。你可以把它打印出来贴在工位上,排查问题时顺着箭头走,基本不会漏。
1.8 启动日志分析——你的第一把「手术刀」
搞启动优化,不会看日志等于盲人摸象。我建议你从今天开始,养成一个习惯:每次开机都保存一份完整的启动日志。
一份标准的启动日志,应该包含以下信息:
- BootROM输出(如果有)——通常是一串特殊字符或版本号
- U-Boot输出——包括板级信息、DDR大小、环境变量加载
- 内核输出——从
Booting Linux on physical CPU到Run /sbin/init - 用户空间输出——init系统的启动信息、服务状态
我的习惯:用script命令或者串口工具的日志功能,把整个启动过程录下来。然后给每个阶段打上时间戳标签。这样你优化前后一对比,就知道改动的效果是正是负。
举个例子,下面是一段典型的内核启动日志片段,我标注了关键时间点:
[ 0.000000] Booting Linux on physical CPU 0x0 // T0: 内核入口
[ 0.000123] Machine model: MyBoard v2.0
[ 0.456789] mmc0: new high speed SD card at address 1234 // T1: MMC初始化完成
[ 0.890123] VFS: Mounted root (ext4 filesystem) on device 179:2 // T2: 根文件系统挂载
[ 0.901234] Run /sbin/init as init process // T3: 进入用户空间
从T0到T3,内核启动花了约901ms。其中MMC初始化占了456ms,超过一半。这就是优化的突破口。
嗯,这一章的内容就到这儿。你先把这张全景图刻在脑子里,后面每一章我们都会深入其中一个阶段,把每个细节掰开揉碎了讲清楚。