4. 内核配置与编译:从源码到可运行镜像
内核配置与编译,说白了就是给硬件“配衣服”。衣服合不合身,直接决定系统能不能跑起来。我做了这么多年嵌入式,见过太多“内核起不来”的案例,十有八九是配置出了问题。
这一章,我们聚焦四个核心话题:源码管理(linux.hash、linux.mk)、配置片段(kernel.config)、设备树(Device Tree)以及内核模块的编译与安装。嗯,都是实战中绕不开的硬骨头。
4.1 内核源码管理:linux.hash 与 linux.mk
Buildroot 管理内核源码的方式,我个人觉得非常优雅。它不直接操作源码树,而是通过两个文件来“遥控”:linux.hash 和 linux.mk。
4.1.1 linux.hash:版本锁与完整性校验
这个文件的作用很简单——锁定内核源码的版本和哈希值。你想想看,如果每次构建都去网上下载最新源码,那项目还怎么保证一致性?
典型的 linux.hash 内容长这样:
# 从 kernel.org 下载的 Linux 6.1.28
sha256 a0a3c6b3f5d7e8f9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3 linux-6.1.28.tar.xz
# 本地补丁文件的哈希
sha256 b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2 my-board-fix.patch
为什么需要 hash 文件?
- 防篡改:下载过程中被中间人攻击?哈希不匹配直接报错
- 版本锁定:团队所有人用同一个源码版本,避免“在我机器上能跑”
- 缓存命中:Buildroot 根据哈希判断是否需要重新下载
我曾经在一个项目中,因为没锁版本,结果同事的构建环境自动拉了个小版本更新,导致某个驱动接口变了。排查了两天……嗯,从那以后,我每个项目都强制提交 linux.hash。
4.1.2 linux.mk:构建流程的“总导演”
linux.mk 才是真正干活的文件。它定义了内核的下载、配置、编译、安装四个阶段。Buildroot 的包框架非常统一,内核也不例外。
核心变量一览:
| 变量名 | 作用 | 典型值 |
|---|---|---|
| LINUX_SOURCE | 源码包文件名 | linux-6.1.28.tar.xz |
| LINUX_SITE | 下载地址 | https://cdn.kernel.org/pub/linux/kernel/v6.x/ |
| LINUX_DEPENDENCIES | 依赖的包 | host-lzop, host-uboot-tools |
| LINUX_KCONFIG_FILE | 内核配置文件路径 | board/myboard/kernel.config |
| LINUX_INSTALL_TARGET | 是否安装到目标系统 | YES / NO |
我个人习惯在 linux.mk 里加一些自定义钩子,比如:
define LINUX_APPLY_MY_PATCHES
# 在解压后、配置前打上板级补丁
for p in $(BR2_EXTERNAL)/board/myboard/patches/linux/*.patch; do \
(cd $(@D) && patch -p1 < $$p); \
done
endef
LINUX_POST_EXTRACT_HOOKS += LINUX_APPLY_MY_PATCHES
小技巧:如果你有多个板子共用同一个内核版本,可以把公共补丁放在 BR2_EXTERNAL 的公共目录下,板级特有补丁放在 board 目录里。这样既复用又隔离。
4.2 内核配置片段:kernel.config 的艺术
内核配置,说白了就是决定“要什么、不要什么”。Buildroot 推荐的做法是使用配置片段(fragment),而不是维护一个完整的 .config 文件。
4.2.1 为什么用配置片段?
完整的内核 .config 文件动辄几千行。你想想看,如果每次内核版本升级都要重新 diff 一遍,那得多痛苦?配置片段的思路是:
- 只保留你修改过的选项
- 其他选项使用内核的默认值
- 多个片段可以叠加
举个例子,我的 kernel.config 可能只有几行:
CONFIG_CMDLINE="console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait"
CONFIG_EXT4_FS=y
CONFIG_USB_NET_DRIVERS=y
# 关闭不需要的调试功能
# CONFIG_DEBUG_KERNEL is not set
4.2.2 配置片段的合并机制
Buildroot 在构建内核时,会执行以下步骤:
- 运行
make defconfig生成默认配置 - 读取
LINUX_KCONFIG_FILE指定的片段文件 - 使用
scripts/kconfig/merge_config.sh合并 - 最终生成完整的 .config
注意:配置片段的语法必须严格遵循 Kconfig 格式。我曾经犯过一个错误——在片段里写了个 CONFIG_XXX=m,但内核里这个选项不支持模块化,结果合并后直接报错。所以,写片段前最好先 make menuconfig 确认一下选项类型。
4.2.3 最小化配置原则
我建议你遵循“够用就好”的原则。嵌入式设备资源有限,每多一个驱动就多一分风险。怎么做到最小化?
- 先用
make localmodconfig基于当前系统生成配置 - 然后手动裁剪掉不需要的驱动
- 最后用
make savedefconfig生成精简配置
记得有一次,我帮客户优化一个路由器项目。他们的内核镜像有 8MB,Flash 只有 16MB。我花了两天时间裁剪配置,最后内核缩到 3.2MB,还多出了 4MB 空间给应用层。客户当场就愣住了。
4.3 设备树(Device Tree)管理
设备树,说白了就是硬件的“说明书”。内核通过它知道:CPU 是什么、内存多大、外设挂在哪里。没有设备树,内核就是个瞎子。
4.3.1 设备树文件结构
一个典型的设备树项目包含:
| 文件类型 | 扩展名 | 作用 |
|---|---|---|
| 设备树源文件 | .dts | 板级描述,包含具体硬件信息 |
| 设备树包含文件 | .dtsi | SoC 级通用描述,可被多个板子复用 |
| 设备树编译输出 | .dtb | 二进制格式,Bootloader 加载后传给内核 |
| 设备树叠加层 | .dtbo | 运行时动态修改设备树 |
4.3.2 在 Buildroot 中管理设备树
Buildroot 通过 BR2_LINUX_KERNEL_DTS_SUPPORT 和 BR2_LINUX_KERNEL_INTREE_DTS_NAME 来指定设备树。我个人习惯把设备树文件放在板级目录下:
board/myboard/
├── kernel.config
├── patches/
│ └── linux/
│ └── 0001-add-myboard-dts.patch
└── myboard.dts
然后在 Buildroot 配置中设置:
BR2_LINUX_KERNEL_CUSTOM_DTS_PATH="board/myboard/myboard.dts"
设备树调试技巧:如果内核启动时设备树解析失败,可以在 U-Boot 中执行 fdt list 查看当前设备树内容。我曾经遇到过一个问题——设备树里 GPIO 编号写错了,导致 LED 死活不亮。用 fdt list /soc/gpio@... 一看,果然偏移量差了 32。
4.3.3 设备树叠加层(Overlay)
叠加层是个好东西。它允许你在不修改主设备树的情况下,动态添加或修改硬件描述。比如:
// my-overlay.dts
/dts-v1/;
/plugin/;
/ {
fragment@0 {
target = &i2c1;
__overlay__ {
status = "okay";
my_sensor@48 {
compatible = "my,pressure-sensor";
reg = <0x48>;
};
};
};
};
编译成 .dtbo 后,可以在运行时通过 configfs 加载:
# echo my-overlay.dtbo > /sys/kernel/config/device-tree/overlays/my-overlay/dtbo
4.4 内核模块编译与安装
内核模块,说白了就是“按需加载的驱动”。不是所有驱动都需要编译进内核,有些可以做成模块,需要时再加载。
4.4.1 模块编译流程
在 Buildroot 中,模块编译是自动的。你只需要在配置片段里把驱动设为 m:
CONFIG_USB_SERIAL=m
CONFIG_USB_SERIAL_PL2303=m
Buildroot 会执行 make modules 编译所有标记为 m 的驱动,然后通过 make modules_install 安装到目标文件系统的 /lib/modules/<kernel-version>/ 目录下。
4.4.2 模块依赖与 depmod
模块之间可能有依赖关系。比如加载 usbserial.ko 之前,必须先加载 usbcore.ko。Buildroot 在安装模块后会自动运行 depmod,生成 modules.dep 文件。
你可以通过 modinfo 查看模块信息:
# modinfo usbserial
filename: /lib/modules/6.1.28/kernel/drivers/usb/serial/usbserial.ko
license: GPL
description: USB Serial Driver core
depends: usbcore
经验之谈:如果你的目标系统空间紧张,可以只安装需要的模块。在 Buildroot 中设置 BR2_LINUX_KERNEL_INSTALL_MODULES=y 后,可以通过 LINUX_MODULES_DIR 指定安装路径,然后手动裁剪不需要的 .ko 文件。
4.4.3 模块自动加载
想让模块在系统启动时自动加载?有两种方式:
- /etc/modules-load.d/:在目标文件系统中创建配置文件,每行一个模块名
- udev 规则:当硬件被检测到时自动加载对应模块
我个人更推荐 udev 方式,因为它更灵活。比如:
# /etc/udev/rules.d/99-usb-serial.rules
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="067b", ATTR{idProduct}=="2303", RUN+="/sbin/modprobe pl2303"
避坑指南:我曾经在某个项目里,把所有 USB 串口驱动都编译成了模块,但忘了在文件系统里放 udev 规则。结果设备插上去后,内核虽然检测到了 USB 设备,但没加载对应的驱动模块。用户以为硬件坏了……嗯,从那以后,我每次都会检查模块加载机制是否完整。
4.5 本章小结
内核配置与编译,是嵌入式 Linux 开发中最基础也最容易出错的环节。记住几个关键点:
- 源码管理:用 linux.hash 锁版本,用 linux.mk 控制流程
- 配置片段:只维护修改过的选项,遵循最小化原则
- 设备树:把硬件描述和驱动代码分离,善用叠加层
- 内核模块:按需编译,注意依赖关系和自动加载机制