第三章 内核启动优化(上):压缩算法、配置裁剪与设备树

内核启动优化,说白了就是让 Linux 内核从「睡醒」到「干活」的时间尽可能短。我做了这么多年嵌入式,见过太多产品因为启动慢了几秒就被客户退货。这一章我们先啃三块硬骨头:压缩算法怎么选、内核怎么瘦身、设备树怎么优化。

核心观点:内核启动时间 = 解压时间 + 初始化时间。压缩算法影响前者,配置裁剪和设备树影响后者。两条腿都要跑得快。

3.1 内核压缩算法选型:gzip vs lz4 vs lzma vs zstd

内核镜像在 Flash 里通常是压缩存储的。Bootloader 把它读到内存,然后解压、跳转。压缩算法的选择直接影响两个指标:镜像大小和解压速度。

我整理了一张对比表,这四种算法我都实际测过:

算法 压缩比 解压速度 内核镜像大小(典型) 适用场景
gzip 中等(~3x) 中等 ~3.5 MB 通用默认,兼容性最好
lz4 较低(~2.5x) 极快 ~4.2 MB 启动时间敏感,Flash 空间充裕
lzma 最高(~4.5x) 慢 ~2.3 MB Flash 空间极度紧张
zstd 高(~4x) 快 ~2.8 MB 兼顾大小和速度,新项目首选

我的经验:如果 Flash 有 16MB 以上,我无脑选 lz4。解压速度能比 gzip 快 3-4 倍,启动时间能省下 200-500ms。别小看这几百毫秒,在汽车仪表盘上就是「秒开」和「等一会」的区别。

在 Buildroot 里怎么切换?很简单,改内核配置:

# 在 Buildroot 的 kernel 配置中
# 进入 General setup ---> Kernel compression mode
# 可选:Gzip / LZ4 / LZMA / XZ / ZSTD

# 或者直接在 defconfig 里写:
CONFIG_KERNEL_GZIP=n
CONFIG_KERNEL_LZ4=y
CONFIG_KERNEL_LZMA=n
CONFIG_KERNEL_ZSTD=n

我曾经在一个项目里踩过坑:选了 lzma,镜像确实小,但解压花了将近 2 秒。客户要求冷启动 3 秒内出画面,结果内核解压就占了大半。后来换成 lz4,镜像大了 1MB 多,但解压只用了 300ms,整个启动流程就达标了。

注意:Bootloader 必须支持对应的解压算法。U-Boot 默认支持 gzip 和 lz4,lzma 和 zstd 可能需要额外补丁。我建议先确认 U-Boot 版本再选算法,否则内核解压时会直接卡死。

3.2 内核配置裁剪:把不需要的功能统统砍掉

内核默认配置里塞了大量你根本用不到的东西。你想想看,一个跑在 ARM Cortex-A7 上的 IoT 设备,需要支持 SCSI 硬盘吗?需要支持 IPv6 隧道吗?不需要。这些驱动和子系统不仅占内存,还会在初始化阶段浪费 CPU 周期。

我一般按这个顺序裁剪:

  1. 架构无关的驱动:网络协议栈里用不到的协议(DECnet、IPX)、不用的文件系统(NTFS、HFS+)
  2. 硬件无关的驱动:你的板子上没有的 PCIe 设备、USB 控制器、显示驱动
  3. 调试和追踪功能:FTRACE、PROC_EVENTS、DEBUG_FS 等,产品发布前全部关掉
  4. 电源管理子系统:如果你的设备不需要深度休眠,可以裁剪掉一部分

具体操作,我习惯用 make menuconfig 逐项检查,然后生成最小配置:

# 在 Buildroot 的 output/build/linux-xxx/ 目录下
make menuconfig
# 逐项关闭不需要的功能
# 保存为 .config

# 生成最小依赖配置
make savedefconfig
cp defconfig arch/arm/configs/my_minimal_defconfig

裁剪效果有多明显?我拿一个实际项目举例:

配置项 裁剪前 裁剪后 优化幅度
内核镜像大小 4.8 MB 2.1 MB -56%
内核初始化时间 1.2 s 0.6 s -50%
内存占用(启动后) 28 MB 16 MB -43%

避坑指南:我曾经把 EXT4 文件系统驱动关了,结果根文件系统挂载失败,板子直接 kernel panic。裁剪时一定要确认你的根文件系统类型、网络协议、存储接口这些基础功能还在。建议每关一项就编译一次,别攒一堆再测,否则定位问题会疯掉。

3.3 设备树优化:减少解析开销

设备树(Device Tree)是内核用来描述硬件信息的结构。U-Boot 把 .dtb 文件传给内核,内核解析它来初始化驱动。设备树越大,解析时间越长。

我见过有些开发者的设备树里塞了几百行用不到的节点——什么 HDMI 音频、MIPI DSI 显示、多个 I2C 总线,实际上板子上根本没这些外设。这些节点都会被内核遍历一遍,白白浪费时间。

优化思路就两条:

  • 删除无用节点:只保留板子上实际存在的硬件
  • 精简属性:去掉不必要的 status、reg、interrupts 描述

举个例子,原始设备树里可能有这样的内容:

// 裁剪前:包含大量无用节点
&i2c1 {
    status = "okay";
    touchscreen@38 {
        compatible = "edt,edt-ft5406";
        reg = <0x38>;
    };
    accelerometer@1d {
        compatible = "st,lis3dh";
        reg = <0x1d>;
    };
};

&hdmi {
    status = "okay";
    pinctrl-0 = <&hdmi_pins>;
};

// 裁剪后:只保留实际硬件
&i2c1 {
    status = "okay";
    touchscreen@38 {
        compatible = "edt,edt-ft5406";
        reg = <0x38>;
    };
};
// HDMI 节点整个删除

设备树大小和解析时间的关系,我实测过:

.dtb 大小 解析时间(典型) 说明
~30 KB ~5 ms 精简后的设备树
~80 KB ~15 ms 默认设备树
~150 KB ~30 ms 包含大量冗余节点

注意:设备树裁剪不能太激进。有些节点虽然你的板子没有,但内核启动流程里会引用它们作为依赖。我建议裁剪后至少做一次完整启动测试,确认所有外设都能正常工作。

3.4 本章知识体系总览

下面这张图把内核启动优化的三个维度串起来了。你可以看到,压缩算法、配置裁剪、设备树优化三者是并行关系,最终都指向同一个目标——缩短启动时间。

内核启动优化知识体系 内核启动优化三大维度 内核启动优化 压缩算法选择 内核配置裁剪 设备树优化 gzip lz4 lzma zstd 驱动裁剪 协议裁剪 调试关闭 电源管理 删除无用节点 精简属性 优化目标 缩短解压时间 + 减少初始化开销 = 更快的启动速度