4. 启动时间分析工具:使用 ftrace、bootgraph、initcall_debug 定位耗时节点

在 DTS 优化工作中,测量是优化的前提。没有精确的启动时间分布数据,任何节点裁剪都是盲目的。本章深入讲解三种内核启动分析工具:initcall_debug、bootgraph 和 ftrace,帮助你精准定位每个设备节点的初始化耗时,从而制定高效的 DTS 精简策略。

核心原则:先测量,后裁剪。使用本节工具生成启动时间火焰图或调用图,找出耗时排名前 10% 的设备节点,优先优化它们。

4.1 工具对比与选型

三种工具各有侧重,适用于不同优化阶段。下表从精度、开销、使用场景三个维度进行对比:

工具 精度 运行时开销 最佳使用场景 输出形式
initcall_debug 函数级(ms 精度) 极低(仅打印日志) 快速扫描所有 initcall 耗时 内核日志(dmesg)
bootgraph 函数级(ms 精度) 低(基于 initcall_debug) 生成可视化启动图 SVG 图表
ftrace 指令级(us 精度) 中等(跟踪缓冲区) 深入分析单个驱动内部耗时 文本 trace / 火焰图
注意:在生产环境中,ftrace 的持续开启会引入额外开销。建议在开发调试阶段使用,优化完成后关闭所有跟踪选项。

4.2 initcall_debug:快速扫描所有设备节点耗时

initcall_debug 是内核提供的最简单的启动分析参数。它会在每个 initcall(包括设备驱动 probe)执行前后打印时间戳,让你一目了然地看到哪个驱动耗时最长。

4.2.1 启用方法

在 kernel cmdline 中添加参数:

initcall_debug

启动后,通过 dmesg 查看输出,典型日志格式如下:

[    0.123456] calling  i2c_driver_init+0x0/0x20 @ 1
[    0.123789] initcall i2c_driver_init+0x0/0x20 returned 0 after 333 usecs
[    0.456789] calling  spi_master_probe+0x0/0x100 @ 2
[    0.789012] initcall spi_master_probe+0x0/0x100 returned 0 after 332223 usecs

其中 after X usecs 就是该 initcall 的耗时(微秒)。

4.2.2 快速定位耗时节点脚本

使用以下 shell 命令从 dmesg 中提取耗时排名:

dmesg | grep "initcall.*returned" | \
  sed 's/.*after \([0-9]*\) usecs.*/\1 &/' | \
  sort -rn | head -20

输出示例:

332223 initcall spi_master_probe+0x0/0x100 returned 0 after 332223 usecs
154321 initcall mmc_blk_probe+0x0/0x200 returned 0 after 154321 usecs
...

排名靠前的驱动就是 DTS 优化的重点目标。

实战技巧:如果某个驱动耗时异常(如超过 100ms),首先检查其 DTS 节点中是否包含 status = "okay" 但实际硬件未连接。其次检查该驱动的 clocks、resets 属性是否引用了未就绪的时钟源,导致 probe 等待超时。

4.3 bootgraph:生成可视化启动时间图

bootgraph 是内核源码 scripts/bootgraph.pl 提供的 Perl 脚本,它解析 initcall_debug 的日志,生成一张 SVG 格式的启动时间图,直观展示每个 initcall 的起始时间和持续时间。

4.3.1 使用步骤

  1. 收集日志:启用 initcall_debug 启动系统,保存 dmesg 输出:dmesg > boot.log
  2. 生成 SVG:运行脚本:perl scripts/bootgraph.pl boot.log > boot.svg
  3. 查看图表:用浏览器打开 boot.svg,可以看到类似甘特图的启动时间分布。

4.3.2 SVG 图:bootgraph 启动时间分布示意

下图模拟了 bootgraph 生成的 SVG 图表结构,展示不同设备驱动的 initcall 耗时分布:

bootgraph 启动时间分布图(模拟) 0ms 100ms 200ms 300ms 400ms uart i2c0 spi_master (332ms) mmc_blk (154ms) eth0 usb_host (200ms) 🔍 分析:spi_master 和 usb_host 耗时最长,应优先检查其 DTS 节点中的时钟和复位属性。
解读技巧:在 bootgraph SVG 图中,横轴是时间,纵轴是不同 initcall。条块越长,表示该驱动初始化耗时越多。重点关注那些条块长度明显超过同类的驱动。

4.4 ftrace:深入分析驱动内部函数耗时

当 initcall_debug 定位到某个驱动整体耗时较长后,ftrace 可以进一步深入该驱动的内部函数调用链,找出具体是哪个子函数拖慢了速度。

4.4.1 启用 ftrace 跟踪特定驱动

以下步骤演示如何跟踪 SPI 控制器的 probe 函数:

# 挂载 tracefs
mount -t tracefs tracefs /sys/kernel/tracing

# 设置跟踪器为 function_graph
echo function_graph > /sys/kernel/tracing/current_tracer

# 设置只跟踪 spi_master_probe 函数
echo spi_master_probe > /sys/kernel/tracing/set_ftrace_filter

# 开始跟踪
echo 1 > /sys/kernel/tracing/tracing_on

# 触发驱动 probe(例如重新绑定)
echo spi_master.0 > /sys/bus/spi/drivers/spi_master/unbind
echo spi_master.0 > /sys/bus/spi/drivers/spi_master/bind

# 停止跟踪
echo 0 > /sys/kernel/tracing/tracing_on

# 查看结果
cat /sys/kernel/tracing/trace

4.4.2 ftrace 输出解读

输出示例:

# tracer: function_graph
#
# CPU  DURATION                  FUNCTION CALLS
# |     |   |                     |   |   |   |
 2)   0.125 us    |  spi_master_probe();
 2)   0.250 us    |    of_spi_parse_dt();
 2) 332.000 us    |    spi_setup();
 2)   0.125 us    |    spi_add_device();

可以看到,spi_setup() 函数耗时 332us,是整个 probe 的瓶颈。进一步分析 spi_setup 内部,可能发现是某个 GPIO 请求或时钟使能操作耗时过长。

4.4.3 SVG 图:ftrace 函数调用链分析

下图展示 ftrace 跟踪到的 SPI 驱动 probe 内部函数调用关系及耗时分布:

ftrace 函数调用链耗时分析(SPI 驱动) spi_master_probe 总耗时: 332.5us of_spi_parse_dt 0.25us spi_setup 332.0us ⚠️ spi_add_device 0.125us 🔍 分析结论:spi_setup() 耗时 332us,占 probe 总耗时的 99.9%。 进一步查看 spi_setup 内部,发现 clk_enable() 等待 PLL 锁定超时。 优化方案:在 DTS 中为 SPI 控制器添加 clocks 属性,指定已稳定的时钟源。

4.5 综合实战:从日志到 DTS 优化决策

以下是一个完整的分析流程,将三种工具串联起来:

  1. 全局扫描:启用 initcall_debug,使用排序脚本找出耗时前 5 的驱动。
  2. 可视化确认:使用 bootgraph 生成 SVG 图,确认耗时驱动的启动顺序和持续时间。
  3. 深入分析:对排名第一的驱动使用 ftrace,跟踪其 probe 函数内部调用链,定位具体瓶颈函数。
  4. DTS 优化:根据瓶颈原因,修改 DTS 节点:
    • 如果瓶颈是等待时钟稳定:在 DTS 中指定 assigned-clocks 和 assigned-clock-rates,确保时钟提前使能。
    • 如果瓶颈是 GPIO 请求超时:检查 gpios 属性是否引用了正确的 GPIO 控制器,或考虑使用 pinctrl 预配置。
    • 如果驱动完全不需要:直接设置 status = "disabled"。
  5. 验证:重新编译内核和设备树,再次使用 initcall_debug 测量,确认耗时下降。
案例数据:某嵌入式项目通过上述流程,发现 USB 控制器驱动 probe 耗时 200ms,原因是等待外部晶振稳定。在 DTS 中为 USB 节点添加 assigned-clocks = <&clk_usb_phy> 并设置 assigned-clock-rates = <48000000> 后,probe 耗时降至 15ms,整机启动时间减少 185ms。

4.6 常见问题与避坑指南

问题现象 可能原因 解决方案
initcall_debug 日志中某些驱动耗时显示为 0 该驱动使用了异步 probe(probe_type = PROBE_PREFER_ASYNCHRONOUS) 使用 ftrace 单独跟踪,或在内核配置中关闭异步 probe
bootgraph 生成的 SVG 为空 dmesg 日志中缺少 calling 和 initcall 配对记录 确认内核 cmdline 已正确添加 initcall_debug,且日志级别足够高
ftrace 跟踪不到目标函数 函数名被内核优化内联,或跟踪过滤器语法错误 使用 cat /sys/kernel/tracing/available_filter_functions 确认函数名
性能影响提醒:ftrace 的 function_graph 模式会显著增加内核开销,导致 probe 耗时比实际更长。建议在分析时记录相对比例而非绝对数值,或使用 trace_printk 进行轻量级插桩。