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 的优先级顺序

级别宏说明
0pure_initcall最早,纯函数
1core_initcall核心子系统
2postcore_initcall核心之后
3arch_initcall架构相关
4subsys_initcall子系统
5fs_initcall文件系统
6device_initcall设备驱动(最常用)
7late_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 的三种方法

  1. 内核配置裁剪:make menuconfig 里去掉不需要的驱动和子系统。这是最根本的方法。
  2. 使用 initcall_blacklist:在启动参数里屏蔽某个 initcall,适合临时测试。
  3. 修改驱动源码:把 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

这样,内核启动时只加载必要的驱动,用户看到系统启动的时间会大大缩短。

本章知识体系

下面这张图总结了内核启动优化的核心逻辑:

内核启动优化知识体系 内核启动优化知识体系 启动参数优化 initcall 调试与裁剪 驱动延迟加载 quiet / loglevel rootwait 取舍 initcall_debug initcall_blacklist deferred_probe 模块化 核心思路:减少不必要的初始化,推迟非关键的加载 目标:从按下电源到用户态 init 执行,控制在 2 秒以内 ① 测量:initcall_debug ② 分析:找出耗时大户 ③ 优化:裁剪/延迟/模块化

嗯,到这里内核启动优化的核心内容就讲完了。记住三个关键词:测量、分析、优化。别一上来就瞎改,先看看数据再说。