第三章 内核启动优化(上):压缩算法、配置裁剪与设备树
内核启动优化,说白了就是让 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 周期。
我一般按这个顺序裁剪:
- 架构无关的驱动:网络协议栈里用不到的协议(DECnet、IPX)、不用的文件系统(NTFS、HFS+)
- 硬件无关的驱动:你的板子上没有的 PCIe 设备、USB 控制器、显示驱动
- 调试和追踪功能:FTRACE、PROC_EVENTS、DEBUG_FS 等,产品发布前全部关掉
- 电源管理子系统:如果你的设备不需要深度休眠,可以裁剪掉一部分
具体操作,我习惯用 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 本章知识体系总览
下面这张图把内核启动优化的三个维度串起来了。你可以看到,压缩算法、配置裁剪、设备树优化三者是并行关系,最终都指向同一个目标——缩短启动时间。