启动时间分析:从按下电源到系统就绪

做安全启动性能调优,第一件事不是动手改代码,而是搞清楚时间都花在哪了。我见过太多团队上来就对着某个驱动猛优化,结果发现那个阶段只占整个启动时间的 3%——说白了,方向错了。

这一章我们就来聊聊怎么把启动过程拆开、称重、找瓶颈。我个人习惯把这套方法叫做「启动时间解剖学」。

启动流程的阶段划分

一个典型的安全启动流程,可以分成四个大阶段。每个阶段的特征和优化空间都不一样。

阶段 起始事件 结束事件 典型耗时 优化潜力
BootROM 上电复位 加载并验证第一级 Bootloader 50-200ms 低(固化在芯片内)
SPL / TPL BootROM 跳转 初始化 DDR、加载 U-Boot 100-500ms 中
U-Boot SPL 跳转 加载并验证内核 300-1500ms 高
Kernel 内核解压 init 进程启动 500-3000ms 高
我的经验:很多团队只盯着 U-Boot 和 Kernel 阶段优化,却忽略了 SPL 阶段。我在一个项目中遇到过 SPL 阶段因为 SPI Flash 读取速度太慢,硬生生占了 400ms——改了个 Quad I/O 模式,直接砍到 80ms。

你想想看,如果连时间花在哪都不知道,优化就是瞎猫碰死耗子。所以第一步,先把这四个阶段的边界搞清楚。

各阶段耗时分析

每个阶段内部还能继续拆。我习惯用「关键路径法」来分析——找到那条最长的依赖链。

BootROM 阶段

这个阶段基本是芯片厂家写死的。我们能做的只有两件事:确认 BootROM 的配置是否正确(比如启动介质选择),以及检查它是否因为安全校验失败而反复重试。嗯,这里要注意,有些芯片的 BootROM 会做多次重试,每次重试都重新读 Flash,时间就这么浪费了。

SPL / TPL 阶段

这个阶段的核心任务是初始化 DDR 和加载 U-Boot。我建议重点关注三个点:

  • DDR 初始化时间——有些开发板的 DDR 初始化代码是通用版,没针对具体颗粒做优化,能差出 100ms
  • Flash 读取速度——检查是否开启了 DMA、是否用了最优的 SPI 模式
  • 安全校验算法——RSA 2048 和 ECDSA P-256 的验签时间能差 3-5 倍

U-Boot 阶段

这是安全启动性能调优的主战场。U-Boot 里做的事情太多了:设备树解析、驱动初始化、文件系统挂载、安全校验、内核加载。我见过最夸张的一个案例,U-Boot 阶段花了 2.8 秒,其中 1.2 秒是在等一个没用的 USB 控制器初始化。

避坑指南:我曾经在一个项目里发现 U-Boot 的 bootm 命令在加载内核前会做两次完整的 FIT 镜像校验——一次在解析阶段,一次在加载阶段。去掉重复校验后,直接省了 300ms。

Kernel 阶段

内核启动的耗时大头通常在驱动初始化和根文件系统挂载。安全启动在这里的影响主要是:内核签名验证、模块签名验证、以及 IMA(Integrity Measurement Architecture)的度量操作。

性能瓶颈定位方法

定位瓶颈,说白了就是「测量-分析-假设-验证」的循环。我常用的工具有这么几个:

1. 硬件计时法

最原始但最可靠的方法。用 GPIO 在关键节点拉高拉低,然后用示波器或者逻辑分析仪量时间。我在 BootROM 阶段就靠这个办法,发现某个芯片的 Flash 读取时序有问题。

// 在 SPL 入口和出口加 GPIO 标记
gpio_set_value(DEBUG_GPIO, 1);  // 开始
// ... SPL 初始化代码 ...
gpio_set_value(DEBUG_GPIO, 0);  // 结束

2. 打印时间戳法

U-Boot 和内核都支持时间戳打印。U-Boot 里可以用 get_timer(),内核里可以用 printk 配合 initcall_debug。

// U-Boot 中测量某段代码耗时
unsigned long start = get_timer(0);
// ... 要测量的代码 ...
printf("耗时: %lu ms\n", get_timer(start));

3. bootgraph 工具

内核自带的一个神器。把内核启动的 initcall 调用关系和时间画成图。我建议每个做启动优化的工程师都跑一遍这个工具。

# 内核启动参数加 initcall_debug
# 启动后用 dmesg 抓日志
dmesg > boot.log
# 用 bootgraph.pl 生成 SVG 图
perl scripts/bootgraph.pl boot.log > boot.svg

4. ftrace 和 trace-cmd

当需要更细粒度的分析时,ftrace 是首选。它能追踪到每个函数调用的耗时。

# 启动时开启 ftrace
trace-cmd start -p function_graph -g start_kernel
# 启动完成后停止并导出
trace-cmd stop
trace-cmd report > trace_report.txt
核心思路:先粗后细。先用 GPIO 法把四个大阶段的耗时卡出来,找到最慢的阶段;再用打印时间戳法拆这个阶段内部的子步骤;最后用 ftrace 或 bootgraph 定位到具体的函数或驱动。

启动时间分析框架图

下面这张图是我自己总结的启动时间分析框架。每次做新项目,我都会先按这个框架走一遍。

启动时间分析框架 阶段划分 耗时测量 瓶颈定位 优化验证 BootROM SPL / TPL U-Boot Kernel GPIO 硬件计时 打印时间戳 bootgraph ftrace 关键路径分析 重复校验检查 驱动初始化耗时 Flash 读取速度 A/B 对比测试 回归验证 安全功能确认 性能基线更新 每次优化后都要回到「耗时测量」步骤重新验证,形成闭环 不要一次性改多个参数,一次只改一个,否则你永远不知道哪个改动起了作用

实战:一个典型的瓶颈定位案例

我去年帮一个客户做启动优化。他们的设备启动要 8 秒,客户要求砍到 3 秒以内。我们按上面的框架走了一遍:

  1. 阶段划分——先加 GPIO 标记,测出四个阶段的耗时:BootROM 120ms、SPL 680ms、U-Boot 3.2s、Kernel 4.0s
  2. 锁定目标——U-Boot 和 Kernel 是主要瓶颈
  3. 拆解 U-Boot——加打印时间戳,发现 U-Boot 里有个 MMC 驱动初始化花了 1.1 秒,安全校验花了 0.9 秒
  4. 深入分析——MMC 驱动初始化慢是因为它用了 4-bit 模式,改成 8-bit 模式后降到 0.3 秒;安全校验慢是因为用了 RSA 4096,改成 ECDSA P-384 后降到 0.2 秒
  5. 验证——改完后 U-Boot 阶段从 3.2 秒降到 1.6 秒

你看,整个过程就是「测量-分析-假设-验证」的循环。没有玄学,只有数据。

一个小技巧:我习惯在项目一开始就建立一个「启动时间基线表」。每次改动后都更新这个表,这样能清楚看到每个优化的实际收益,也方便向领导汇报——数据说话,比什么都管用。