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的执行流程:

  1. 设置CPU为SVC模式,关中断
  2. 初始化时钟和DDR控制器
  3. 从存储介质读取U-Boot proper到DDR
  4. 跳转到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 全景流程图

下面这张图把整个启动流程串起来了。你可以把它打印出来贴在工位上,排查问题时顺着箭头走,基本不会漏。

嵌入式Linux启动全生命周期 Stage 0: BootROM 上电复位 → 加载SPL Stage 1: SPL/MLO 初始化DDR → 加载U-Boot Stage 2: U-Boot 加载内核/DTB → 跳转 Stage 3: 内核启动 解压 → 驱动 → 挂载根FS Stage 4: 用户空间 init → 服务 → 应用就绪 各阶段关键指标与常见瓶颈 • BootROM: 启动介质读取速度(SD/NAND/SPI) • SPL: DDR初始化时序、ZQ校准耗时 • U-Boot: bootdelay、文件系统加载、校验开销 • 内核: 驱动probe顺序、initcall耗时、DMA缓冲分配 • 用户空间: init系统选择、服务依赖、脚本效率 优化优先级: Stage 0 > Stage 1 > Stage 2 > Stage 3 > Stage 4 越靠前的阶段,优化收益越大(串行瓶颈) 调试工具推荐 • BootROM阶段: 示波器抓GPIO、串口bootrom调试信息 • SPL/U-Boot阶段: 加自定义GPIO测量点、打开DEBUG宏 • 内核阶段: initcall_debug、printk.time、bootgraph • 用户空间阶段: systemd-analyze、strace、自定义时间戳

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,超过一半。这就是优化的突破口。

嗯,这一章的内容就到这儿。你先把这张全景图刻在脑子里,后面每一章我们都会深入其中一个阶段,把每个细节掰开揉碎了讲清楚。