4、内核启动优化:内核镜像压缩方式选择、内核配置裁剪、设备树优化、initcall 异步化
好,咱们进入内核启动优化这个硬骨头。说实话,IVI 系统里内核启动占用的时间,往往比你想的要多的多。我见过不少项目,uboot 都快到飞起,结果卡在内核解压和初始化上,白白浪费一两秒。这一秒在用户体验上,就是「卡」和「流畅」的分界线。
4.1 内核镜像压缩方式选择
内核编译出来是个大块头,不压缩不行。但压缩方式选错了,解压时间可能比传输时间还长。这里我踩过坑,跟大家聊聊。
常见的压缩算法有这么几种:
| 压缩方式 | 压缩比 | 解压速度 | 典型场景 |
|---|---|---|---|
| gzip | 中等 | 快 | 通用,兼容性好 |
| lz4 | 较低 | 极快 | 追求启动速度 |
| lzo | 较低 | 快 | 嵌入式常用 |
| bzip2 | 高 | 慢 | 存储极度受限 |
| xz | 极高 | 极慢 | 不推荐用于启动 |
我个人习惯,在 IVI 项目里首选 lz4。为什么?因为 IVI 的存储空间通常不是瓶颈(至少 8GB 起步),但启动时间每毫秒必争。lz4 的解压速度几乎是 gzip 的 3-4 倍,虽然镜像体积大个 10%-15%,但换来的是实打实的启动加速。
核心结论: 在 IVI 场景下,用空间换时间,选 lz4 或 lzo。别用 xz,解压慢到让你怀疑人生。
配置方法很简单,在内核配置里找到:
CONFIG_KERNEL_LZ4=y
# 或者
CONFIG_KERNEL_LZO=y
嗯,这里要注意,uboot 也要支持对应的解压算法。我遇到过一回,内核配了 lz4,结果 uboot 没开对应驱动,启动直接崩了。所以改之前,先确认 bootloader 的配置。
4.2 内核配置裁剪
内核默认配置里,有大量 IVI 用不到的驱动和功能。你想想看,一个车载娱乐系统,需要支持几十种网卡驱动吗?需要支持各种稀奇古怪的文件系统吗?不需要。
裁剪的原则很简单:用不到的,统统关掉。但实际操作起来,很多人不敢下手,怕关错了系统起不来。我刚开始也这样,后来总结了一套安全裁剪法。
第一步:基于 defconfig 做减法
不要从零开始配,太累。用厂商提供的 defconfig 作为基础,然后逐项检查:
- 关闭所有不用的架构:比如你用的是 ARM64,就把 ARM、x86、MIPS 全关了。
- 关闭不用的驱动:网卡、声卡、显卡、USB 设备,只保留你硬件上有的。
- 关闭调试选项:
CONFIG_DEBUG_KERNEL、CONFIG_PRINTK(生产环境可以关)、CONFIG_DYNAMIC_DEBUG这些,调试完就关掉。 - 关闭不用的文件系统:ext2、ext3、btrfs、xfs 这些,只留 ext4 和 squashfs 就够了。
小技巧: 用 make menuconfig 配合搜索功能(按 / 键),可以快速定位某个配置项。我习惯先搜一遍,把明显没用的关掉,再编译测试。
第二步:用工具分析依赖
手动裁剪容易漏。我推荐用 scripts/diffconfig 脚本,对比裁剪前后的配置差异。另外,make savedefconfig 可以生成最小化的 defconfig,方便版本管理。
我曾经在一个项目里,把内核从 8MB 裁剪到 3.5MB,启动时间直接少了 400ms。效果非常明显。
注意: 裁剪要逐步进行,每关一批选项就编译测试一次。别一次性关太多,否则出了问题很难定位。我吃过这个亏,一次关了 200 多个选项,结果内核起不来,排查了一整天。
4.3 设备树优化
设备树(DTB)是内核描述硬件的语言。但很多人不知道,设备树本身也会影响启动速度。原因很简单:内核解析设备树需要时间,设备树越大,解析越慢。
优化点一:删除无用节点
IVI 主板上有很多外设,但并不是每个都需要内核去管理。比如一些 GPIO 扩展芯片、不用的 I2C 设备、调试用的串口,都可以从设备树里删掉。删掉后,内核就不需要为它们分配资源和初始化。
优化点二:精简属性
有些设备树节点里,包含了大量注释性的属性或者冗余信息。比如 status = "okay" 是默认值,可以不写。再比如一些 regulator 的电压范围,如果硬件固定,直接写死,别让内核去猜。
优化点三:使用 dtb 覆盖
如果你的硬件有多个变体,别用一个大而全的设备树。用基础 DTB 加上 overlay 的方式,按需加载。这样每个 DTB 都很小,解析速度快。
// 精简前的设备树节点
&i2c3 {
status = "okay";
clock-frequency = <400000>;
pinctrl-0 = <&i2c3_pins>;
pinctrl-names = "default";
touch@38 {
compatible = "goodix,gt911";
reg = <0x38>;
interrupt-parent = <&gpio1>;
interrupts = <13 IRQ_TYPE_EDGE_FALLING>;
// 下面这些如果不用,可以删掉
// touchscreen-size-x = <1024>;
// touchscreen-size-y = <600>;
// touchscreen-inverted-x;
// touchscreen-inverted-y;
};
};
我个人习惯,在项目初期就把设备树里所有不用的节点注释掉,只保留必须的。这样后期维护也方便,一眼就能看出哪些外设是真正在用的。
4.4 initcall 异步化
这是内核启动优化的高阶技巧。内核启动时,会按顺序执行大量的初始化函数(initcall)。这些函数是串行执行的,一个卡住,后面的都得等。
你想想看,有些驱动初始化需要等待硬件就绪,比如等待一个 I2C 设备应答,或者等待一个 regulator 稳定输出。这些等待时间,完全可以利用起来做别的事。
initcall 的级别:
| 级别 | 宏定义 | 说明 |
|---|---|---|
| 0 | pure_initcall | 最早,用于核心功能 |
| 1 | core_initcall | CPU、中断、定时器等 |
| 2 | postcore_initcall | 基础架构 |
| 3 | arch_initcall | 架构相关 |
| 4 | subsys_initcall | 子系统初始化 |
| 5 | fs_initcall | 文件系统 |
| 6 | device_initcall | 设备驱动(常用) |
| 7 | late_initcall | 最晚,用于后期初始化 |
异步化的思路:把那些不依赖其他模块、且初始化时间较长的驱动,放到一个单独的线程里去执行。内核从 4.x 开始,引入了 async_schedule() 机制,可以方便地实现这一点。
具体做法:
- 找到那些初始化慢的驱动(比如触摸屏、WiFi 模块、蓝牙)。
- 在驱动的 probe 函数里,把耗时的操作放到
async_schedule()中执行。 - 确保这些异步任务不阻塞其他驱动的初始化。
#include <linux/async.h>
static void my_driver_init_async(void *data, async_cookie_t cookie)
{
struct my_device *dev = data;
// 这里放耗时的初始化操作
msleep(100); // 模拟等待硬件就绪
dev->ready = true;
}
static int my_driver_probe(struct platform_device *pdev)
{
struct my_device *dev;
// ... 分配资源等 ...
// 把耗时操作异步化
async_schedule(my_driver_init_async, dev);
return 0;
}
关键点: 异步化不是万能的。如果驱动 A 依赖驱动 B 初始化完成,那就不能异步。需要仔细分析依赖关系,否则会出现竞态条件,导致系统不稳定。
我曾经在一个项目里,把触摸屏和 WiFi 的初始化异步化,启动时间又快了 200ms。但调试过程很痛苦,因为异步后的时序变了,有些 bug 只在特定情况下复现。所以我的建议是:先做裁剪和压缩优化,最后再考虑异步化。异步化是锦上添花,不是雪中送炭。
好了,内核启动优化这部分就聊到这儿。下一章咱们聊聊根文件系统的挂载优化,那也是个大头。