Buildroot性能调优与启动流程加速实战 - 第4节
内核启动参数优化:给内核传递正确的“指令”
内核启动参数,说白了就是你在 bootloader 里传给内核的那一串字符串。别小看它,参数写对了,启动能快不少;写错了,系统可能直接起不来。
我个人习惯把启动参数分成三类:必选参数、性能参数、调试参数。生产环境里,调试参数一个都不要留。
常用性能相关参数一览
| 参数 | 作用 | 推荐值 |
|---|---|---|
quiet | 减少内核打印信息 | 生产环境必加 |
loglevel=3 | 只打印 KERN_ERR 以上级别 | 3 或 4 |
initcall_debug | 打印 initcall 耗时(调试用) | 调试时加,生产去掉 |
rdinit=/sbin/init | 指定 init 进程路径 | 按实际路径设置 |
console=ttyS0,115200 | 指定控制台设备 | 按硬件配置 |
no_console_suspend | 休眠时不关控制台 | 调试时用 |
我的经验: 曾经有个项目,内核启动花了 3 秒,加了 quiet 和 loglevel=3 之后,直接降到 2.2 秒。别小看打印,UART 输出是很慢的。
如何传递启动参数
在 Buildroot 里,启动参数通常在 bootloader 配置里设置。以 U-Boot 为例:
setenv bootargs 'console=ttyS0,115200 root=/dev/mmcblk0p2 rw rootwait quiet loglevel=3'
bootz ${kernel_addr} - ${fdt_addr}
如果你用 extlinux.conf,那就更简单了:
LABEL linux
KERNEL /zImage
FDT /board.dtb
APPEND console=ttyS0,115200 root=/dev/mmcblk0p2 rw rootwait quiet loglevel=3
注意: rootwait 参数会让内核一直等待根文件系统就绪。如果你的存储设备初始化很快,可以去掉它,能省 0.5-1 秒。但如果你不确定,还是留着吧——我曾经去掉后,系统在 10% 的机器上启动失败,排查了两天。
initcall 调试与裁剪:找到启动慢的“元凶”
内核启动过程中,会按优先级依次调用各个模块的初始化函数。这些函数就是 initcall。每个模块都觉得自己很重要,但有些模块其实你根本用不上。
initcall 的优先级顺序
| 级别 | 宏 | 说明 |
|---|---|---|
| 0 | pure_initcall | 最早,纯函数 |
| 1 | core_initcall | 核心子系统 |
| 2 | postcore_initcall | 核心之后 |
| 3 | arch_initcall | 架构相关 |
| 4 | subsys_initcall | 子系统 |
| 5 | fs_initcall | 文件系统 |
| 6 | device_initcall | 设备驱动(最常用) |
| 7 | late_initcall | 最晚 |
如何测量每个 initcall 的耗时
在启动参数里加上 initcall_debug,内核就会打印每个 initcall 的起始时间和耗时:
bootargs = ... initcall_debug ...
启动后用 dmesg 查看,你会看到类似这样的输出:
[ 0.123456] initcall i2c_init+0x0/0x80 returned 0 after 1234 usecs
[ 0.234567] initcall spi_init+0x0/0x90 returned 0 after 5678 usecs
[ 1.345678] initcall mmc_init+0x0/0xa0 returned 0 after 12345 usecs
关键指标: 把耗时超过 1ms 的 initcall 列出来,这些就是你要优化的目标。我曾经在一个项目里发现某个 GPIO 驱动初始化花了 200ms,结果那个 GPIO 根本没用——直接裁剪掉,启动时间立减 200ms。
裁剪 initcall 的三种方法
- 内核配置裁剪:
make menuconfig里去掉不需要的驱动和子系统。这是最根本的方法。 - 使用 initcall_blacklist:在启动参数里屏蔽某个 initcall,适合临时测试。
- 修改驱动源码:把
module_init()改为late_initcall(),推迟加载。
举个例子,屏蔽某个驱动:
bootargs = ... initcall_blacklist=spi_init ...
避坑指南: 我曾经在生产环境里用 initcall_blacklist 屏蔽了一个看起来没用的驱动,结果系统启动后 Wi-Fi 死活连不上。后来才发现那个驱动是 Wi-Fi 模块的前置依赖。所以,裁剪之前一定要搞清楚依赖关系。
驱动延迟加载技术:让该快的先快,该慢的后面再说
你想想看,内核启动时所有驱动都挤在一起初始化,就像早高峰的地铁——谁都进不去。不如让关键驱动先上,非关键的后面再说。
三种延迟加载方案对比
| 方案 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| deferred_probe | 驱动返回 -EPROBE_DEFER | 内核原生支持,无需修改 | 依赖关系复杂时可能多次重试 |
| module 化 + 用户空间加载 | 编译为模块,init 脚本按需 insmod | 灵活可控 | 需要文件系统支持模块 |
| firmware 分类加载 | 将固件分为 fast/slow 两类 | 适合复杂硬件 | 需要修改驱动 |
deferred_probe 实战
内核里,如果一个驱动依赖的资源还没准备好,可以返回 -EPROBE_DEFER:
static int my_driver_probe(struct platform_device *pdev)
{
struct regulator *reg;
reg = regulator_get(&pdev->dev, "vcc");
if (IS_ERR(reg)) {
if (PTR_ERR(reg) == -EPROBE_DEFER) {
return -EPROBE_DEFER; // 告诉内核:我还没准备好,等会儿再叫我
}
return PTR_ERR(reg);
}
// ... 正常初始化 ...
return 0;
}
内核会自动把返回 -EPROBE_DEFER 的驱动放到延迟队列里,等依赖的资源就绪后再重新尝试。
注意: 内核默认最多重试 PROBE_DEFER_TIMEOUT 次(通常是 5 次)。如果超过次数还没成功,驱动就永远不会被加载了。我曾经遇到过某个电源管理芯片初始化特别慢,导致依赖它的驱动重试超时——最后在 dmesg 里看到一堆 deferred probe timeout 错误。
模块化加载的最佳实践
我个人更推荐把非关键驱动编译为模块,然后在用户空间按需加载。比如:
# /etc/init.d/S99late_drivers.sh
#!/bin/sh
# 等系统完全启动后再加载
sleep 2
insmod /lib/modules/4.19.0/extra/sound_card.ko
insmod /lib/modules/4.19.0/extra/bluetooth.ko
这样,内核启动时只加载必要的驱动,用户看到系统启动的时间会大大缩短。
本章知识体系
下面这张图总结了内核启动优化的核心逻辑:
嗯,到这里内核启动优化的核心内容就讲完了。记住三个关键词:测量、分析、优化。别一上来就瞎改,先看看数据再说。