3. 硬件适配基础

内核裁剪这事儿,说白了就是「去掉你不需要的,留下你必需的」。很多刚入行的朋友总觉得内核越大越稳,其实恰恰相反——一个臃肿的内核不仅占存储、吃内存,还会拖慢启动速度,甚至引入安全风险。我在一个工业控制项目里就吃过这个亏,当时默认配置编译出来的内核有 8MB,Flash 只有 4MB,硬是塞不进去。后来花了整整两天裁剪,最后压到 2.3MB,跑得比原来还稳。

这一章,我就把内核裁剪的核心工具链——Kconfig、Makefile、编译选项——掰开揉碎了讲清楚。

3.1 Kconfig:内核的「菜单系统」

Kconfig 是内核配置的骨架。你运行 make menuconfig 时看到的那个图形界面,背后就是 Kconfig 文件在驱动。

每个内核子系统、驱动、功能模块,都会有一个 Kconfig 文件。它的语法其实不复杂,核心就几个关键字:

关键字 作用 示例
config 定义一个配置选项 config MY_DRIVER
tristate 三态:不编译/编译进内核/编译为模块 tristate "My Driver Support"
bool 二态:编译或不编译 bool "Enable Feature X"
depends on 依赖关系 depends on PCI
select 反向选择(自动开启依赖项) select CRC32
help 帮助文本 help\n Say Y if you need...

举个例子,一个简单的驱动 Kconfig 长这样:

config MY_SENSOR_DRIVER
    tristate "My Custom Sensor Driver"
    depends on I2C
    select CRC8
    help
      This driver supports My Custom Sensor over I2C.
      Say Y to compile it into the kernel.
      Say M to compile it as a module.
我的习惯: 每次裁剪前,先跑一遍 make defconfig 生成默认配置,然后在此基础上做减法。别从零开始配,那太容易漏东西了。

3.2 Makefile:内核的「构建脚本」

Kconfig 决定了「要不要编译」,Makefile 决定了「怎么编译」。每个驱动目录下都有一个 Makefile,里面用 obj-$(CONFIG_XXX) 的语法来控制编译行为。

看个例子:

# drivers/iio/imu/Makefile
obj-$(CONFIG_BMI160) += bmi160_core.o
obj-$(CONFIG_BMI160_I2C) += bmi160_i2c.o
obj-$(CONFIG_BMI160_SPI) += bmi160_spi.o

当 CONFIG_BMI160=y 时,bmi160_core.o 会被编译并链接进内核。如果是 =m,则编译成独立的 .ko 模块文件。

这里有个坑——我曾经遇到过:

我曾经踩过的坑: 在裁剪时,我把某个驱动设成了 =m,但忘了把它的依赖模块也设成 =m 或 =y。结果编译出来的内核模块加载时报「Unknown symbol」,查了半天才发现是依赖没跟上。所以,depends on 和 select 一定要仔细检查。

3.3 裁剪内核模块:从大到小做减法

裁剪不是一上来就删东西,而是先搞清楚「哪些是必须的」。我一般按这个流程走:

  1. 确定硬件清单——CPU 架构、外设接口、存储设备、网络芯片,列个表。
  2. 关闭无关架构——比如你跑 ARM,就把 x86、MIPS、RISC-V 全关掉。
  3. 关闭无关驱动——声卡、显卡、电视调谐器、蓝牙……用不到的统统关掉。
  4. 关闭调试选项——DEBUG_KERNEL、DEBUG_FS、DYNAMIC_DEBUG 这些在正式产品里必须关。
  5. 关闭文件系统——只保留你需要的,比如 ext4、squashfs,其他的全关。
  6. 关闭网络协议——如果不需要 IPv6、802.11、蓝牙协议栈,就关掉。

下面这张图是我自己总结的裁剪决策流程:

内核裁剪决策流程图 确定硬件清单 关闭无关架构/驱动 关闭调试/冗余功能 配置为模块 (M) 或内置 (Y) 编译 & 验证启动 大小/功能达标? 不达标,回退调整 ✅ 裁剪完成

3.4 编译优化选项:让内核跑得更快更小

配置裁剪完了,编译选项还能再压一压。我常用的优化手段有这几个:

  • 使用 -Os 优化——针对尺寸优化,比 -O2 能小 10%~20%。代价是性能略有下降,但对嵌入式设备来说,尺寸往往比那点性能更重要。
  • 开启 CC_OPTIMIZE_FOR_SIZE——内核配置里就有这个选项,勾上就行。
  • 去掉内核符号表——CONFIG_KALLSYMS=n,能省不少空间。不过调试时建议开着,正式发布再关。
  • 使用 LTO(链接时优化)——内核 5.x 以后支持 CONFIG_LTO_CLANG,能跨文件优化,进一步减小体积。
  • 去掉打印信息——CONFIG_PRINTK=n 或降低日志级别,能省掉大量字符串常量。
实际效果参考: 我在一个 ARM Cortex-A7 项目上,从默认的 6.8MB 裁剪到 1.9MB,启动时间从 12 秒降到 4 秒。关键操作就是:关掉所有不用的驱动、关掉调试选项、用 -Os 编译。

3.5 避坑指南:我踩过的几个雷

裁剪这事儿,说起来简单,做起来全是细节。我把自己踩过的坑列出来,你遇到了能少走弯路:

  • 别把 initramfs 裁没了——有一次我把 CONFIG_BLK_DEV_INITRD 关了,结果内核启动到一半就 panic,因为找不到根文件系统。
  • 模块依赖要理清——比如你用了 CONFIG_SOUND,但忘了开 CONFIG_SND,编译直接报错。用 make olddefconfig 可以自动补全依赖。
  • 别在裁剪后直接部署——先拿 QEMU 或开发板跑一遍,确认所有外设都能正常工作。我吃过一次亏,裁剪后网卡驱动没编进去,板子成了「孤儿」。
  • 保留一份原始配置——每次裁剪前,把 .config 备份一下。改乱了还能回退。
特别提醒: 如果你裁剪的是产品级内核,一定要做回归测试。别以为「能启动就万事大吉」,有些功能在裁剪后可能静默失效,比如硬件监控、看门狗、RTC 等。我见过一个项目,裁剪后系统跑了一个月才暴露问题——因为温度监控驱动被裁掉了,设备过热都没报警。

3.6 小结

内核裁剪的核心就三件事:Kconfig 决定要不要、Makefile 决定怎么编、编译选项决定多快多小。这三者配合好了,你就能得到一个既小又稳的内核。

嗯,这一章的内容就到这儿。记住,裁剪不是一锤子买卖,而是一个迭代优化的过程。每改一次配置,编译一次,验证一次,慢慢你就会找到最适合你硬件的那套配置。