2. 启动时间度量与分析:使用 printk time、bootgraph、bootchart 等工具测量启动各阶段耗时
启动优化不能靠“感觉”。在动手裁剪内核、调整 init 脚本之前,必须用工具精确度量每个阶段的耗时。本章介绍三种最常用的度量手段:printk time(内核日志时间戳)、bootgraph(内核启动可视化)、bootchart(用户空间进程启动时序)。
2.1 度量工具选型对比
| 工具 | 度量范围 | 输出形式 | 精度 | 适用场景 |
|---|---|---|---|---|
| printk time | 内核启动阶段(start_kernel → init 进程) | 文本日志,每行带时间戳 | 微秒级 | 快速定位内核各初始化函数的耗时 |
| bootgraph | 内核启动阶段(同 printk time) | SVG 时序图 | 微秒级 | 可视化内核初始化调用链与耗时 |
| bootchart | 用户空间启动阶段(init → 桌面/服务就绪) | SVG 条形图 + 日志 | 毫秒级 | 分析 init 脚本、服务并行度、进程启动顺序 |
建议: 先使用 printk time 做第一轮内核耗时扫描,再用 bootgraph 生成可视化报告确认瓶颈,最后用 bootchart 分析用户空间启动流程。三个工具配合使用,可以覆盖从上电到系统就绪的完整启动链。
2.2 使用 printk time 度量内核启动耗时
printk time 是内核内置的日志时间戳功能,无需额外安装工具。启用后,每条内核日志都会附加从系统启动开始到该日志打印时刻的微秒级时间戳。
2.2.1 启用方法
方法一:内核启动参数(推荐)
在 bootargs 中添加:printk.time=1
示例:console=ttyS0,115200 printk.time=1
方法二:内核配置(编译时固定开启)
Kernel hacking → printk and dmesg options → [*] Show timing information on printks
对应配置项:CONFIG_PRINTK_TIME=y
2.2.2 关键时间戳解读
启用后,dmesg 输出类似:
[ 0.000000] Booting Linux on physical CPU 0x0
[ 0.000123] Initializing cgroup subsys cpu
[ 0.450231] Calibrating delay loop... 1998.84 BogoMIPS (lpj=999424)
[ 1.234567] Freeing unused kernel memory: 1024K
[ 1.240001] Run /init as init process
重点关注以下时间节点:
- [0.000000]:内核解压完成,start_kernel 开始执行。
- [0.450231]:Calibrating delay loop 完成,表示定时器子系统就绪。
- [1.234567]:释放未使用的内核内存,表示内核初始化基本完成。
- [1.240001]:执行 /init,表示内核启动阶段结束,进入用户空间。
通过计算相邻关键点的时间差,可以快速定位哪个子系统耗时异常。例如:
定时器校准耗时 = 0.450231 - 0.000123 = 0.450108 秒
内核初始化总耗时 = 1.240001 - 0.000000 = 1.240001 秒
注意: printk 本身有 I/O 开销,在慢速串口上可能引入额外延迟。对于高精度测量,建议使用
earlyprintk 或 initcall_debug 配合 printk time 使用,以捕获更早期的日志。
2.3 使用 bootgraph 可视化内核启动流程
bootgraph 是内核源码 scripts/bootgraph.pl 提供的 Perl 脚本,它解析 dmesg 日志中的 initcall 时间戳,生成 SVG 格式的时序图,直观展示每个内核初始化函数的调用顺序和耗时。
2.3.1 生成步骤
- 确保内核开启了
CONFIG_PRINTK_TIME=y和CONFIG_DEBUG_KERNEL=y。 - 在 bootargs 中添加
initcall_debug,使内核打印每个 initcall 的进入和退出时间。 - 启动系统,保存 dmesg 日志:
dmesg > dmesg.log。 - 在主机上运行 bootgraph.pl 生成 SVG:
perl scripts/bootgraph.pl dmesg.log > bootgraph.svg
2.3.2 bootgraph SVG 解读
下图展示了一个典型的 bootgraph 输出结构,包含 initcall 调用链、耗时条形图以及关键时间点标注。
2.3.3 瓶颈识别方法
在生成的 SVG 中,重点关注:
- 条块长度:超过 100ms 的 initcall 应优先优化。
- 串行依赖:如果某个 initcall 后面紧跟着大量空白(CPU 空闲),说明存在等待事件。
- 异常顺序:某些驱动初始化顺序不符合预期,可能导致不必要的延迟。
2.4 使用 bootchart 分析用户空间启动
bootchart 是一个轻量级的启动分析工具,它在 init 阶段启动一个后台进程,收集所有进程的 CPU 占用、I/O 等待、进程创建时间等信息,最终生成 SVG 格式的启动时序图。
2.4.1 集成与配置
在嵌入式 Linux 中集成 bootchart:
- 交叉编译 bootchart 工具链:
git clone https://github.com/mmeeks/bootchart.git
cd bootchart
./configure --host=arm-linux-gnueabihf
make
make install DESTDIR=/path/to/rootfs
- 在 init 脚本中启动 bootchartd:
# 在 /etc/inittab 或 init 脚本中添加:
::sysinit:/sbin/bootchartd start
# 在关机脚本中添加停止命令:
::shutdown:/sbin/bootchartd stop
- 启动完成后,bootchart 数据保存在
/var/log/bootchart.tgz,将其拷贝到主机并用pybootchartgui渲染:
pybootchartgui -i bootchart.tgz -o bootchart.svg
2.4.2 bootchart SVG 解读
2.4.3 关键指标分析
| 指标 | 含义 | 优化方向 |
|---|---|---|
| 进程启动延迟 | 从 init 创建进程到进程开始执行的时间 | 减少 fork/exec 开销,使用预加载或共享库 |
| CPU 空闲时间 | 进程等待 CPU 调度的时间 | 调整进程优先级,减少不必要的唤醒 |
| I/O 等待时间 | 进程等待磁盘/闪存 I/O 完成的时间 | 使用异步 I/O、调整文件系统挂载参数、使用 squashfs |
| 并行度 | 同时运行的进程数量 | 增加并行启动的服务数量,减少串行依赖 |
2.5 实战案例:从 printk time 到 bootchart 的完整分析流程
以一个典型的嵌入式 Linux 系统(i.MX6ULL + 4.19 内核 + BusyBox 用户空间)为例,展示完整的度量流程:
- 第一轮:printk time 扫描
dmesg | grep -E "\[.*\]" | head -20
[ 0.000000] Linux version 4.19.35
[ 0.450231] Calibrating delay loop... 1998.84 BogoMIPS
[ 1.234567] Freeing unused kernel memory: 1024K
[ 1.240001] Run /init as init process
→ 内核启动总耗时:1.24 秒
- 第二轮:bootgraph 定位内核瓶颈
perl scripts/bootgraph.pl dmesg.log > bootgraph.svg
→ 发现 mm_init 耗时 320ms,driver_initcalls 耗时 450ms
- 第三轮:bootchart 分析用户空间
pybootchartgui -i bootchart.tgz -o bootchart.svg
→ 发现 init 脚本中串行启动 5 个服务,总耗时 3.2 秒
- 优化措施:
- 内核:裁剪不必要的驱动,将 mm_init 中的大块内存预分配改为按需分配。
- 用户空间:将串行启动改为并行启动,使用
start-stop-daemon --background。
- 优化后验证:
内核启动耗时从 1.24s 降至 0.85s
用户空间启动耗时从 3.2s 降至 1.8s
总启动时间从 4.44s 降至 2.65s
经验法则: 每次优化后必须重新度量,避免“优化”引入新的瓶颈。建议将度量脚本集成到 CI/CD 流程中,每次构建后自动生成启动时间报告。
2.6 常见问题与避坑指南
| 问题 | 原因 | 解决方案 |
|---|---|---|
| printk time 时间戳不准确 | 串口波特率过低,日志输出延迟 | 使用 earlyprintk 或增加串口缓冲区 |
| bootgraph 生成的 SVG 为空 | 内核未开启 initcall_debug | 在 bootargs 中添加 initcall_debug |
| bootchart 数据不完整 | bootchartd 启动太晚,错过早期进程 | 将 bootchartd 放在 init 脚本的最前面启动 |
| 度量结果波动大 | NAND Flash 读速度不稳定 | 多次测量取平均值,或使用 eMMC 替代 NAND |