3. Linux内核源码结构与配置:从源码树到定制镜像
内核源码,说白了就是一座巨大的乐高城堡。我第一次接触Linux内核源码时,面对密密麻麻的目录,说实话有点懵。但后来我发现,只要掌握了它的骨架结构,再配合Kconfig和Makefile这两把钥匙,你就能轻松地裁剪、配置、编译出适合自己硬件平台的内核镜像。
这一章,我们就来拆解这座城堡。我会结合我这些年做过的项目,把内核源码的目录结构、配置机制、编译流程,还有设备树(DTS)的语法,掰开揉碎了讲给你听。
3.1 内核源码目录分析:骨架与血肉
先看看顶层目录。你下载一个主线内核,解压后第一眼看到的就是这些文件夹。它们分工明确,各司其职。
| 目录名 | 核心内容 | 我的经验备注 |
|---|---|---|
arch/ |
体系结构相关代码(ARM、x86、RISC-V等) | 移植时,这里是你最常待的地方 |
drivers/ |
设备驱动(占源码一半以上) | 写驱动,基本就在这个目录里转 |
kernel/ |
核心调度、进程管理、信号等 | 一般不动,除非你在做深度优化 |
mm/ |
内存管理 | 调优内存时才会进来看看 |
fs/ |
文件系统(ext4、btrfs、NFS等) | 加新文件系统支持时用 |
net/ |
网络协议栈 | 网络驱动和协议都在这里 |
include/ |
头文件(公共API定义) | 找结构体定义,先来这里翻 |
Documentation/ |
内核文档 | 遇到坑,先看这里,比百度靠谱 |
scripts/ |
编译、配置、分析脚本 | Kconfig解析、模块签名等工具 |
嗯,这里要注意。很多人一上来就扎进 drivers/ 里找代码,结果迷路了。我建议你先看 arch/ 目录,因为你的板子是什么CPU架构,决定了你从哪个入口开始。比如你做ARM64的板子,那 arch/arm64/ 就是你的主战场。
核心原则: 移植工作的第一步,不是写代码,而是找到正确的 arch/ 目录下的板级文件或设备树。
3.2 Kconfig与Makefile机制:配置的“语法”
内核的配置系统,说白了就是一套“菜单生成器”。Kconfig 定义菜单项,Makefile 根据菜单选择决定编译什么。这两兄弟配合得天衣无缝。
3.2.1 Kconfig:菜单的“菜谱”
每个驱动目录下都有一个 Kconfig 文件。它定义了哪些功能可以被选中。比如:
config SENSORS_LM75
tristate "LM75 temperature sensor"
depends on I2C
help
If you say yes here, support for LM75 temperature sensor will be included.
这里 tristate 表示有三种状态:编译进内核(y)、编译成模块(m)、不编译(n)。depends on I2C 表示这个驱动依赖I2C子系统,如果I2C没选,它就不会出现。
我的习惯: 在配置内核时,我一般先用 make menuconfig 做一次全量搜索(按 / 键),找到目标驱动的路径,再决定是编进内核还是模块。这样比瞎翻快得多。
3.2.2 Makefile:编译的“执行者”
Kconfig 定义完菜单,Makefile 就根据配置变量来编译。比如:
obj-$(CONFIG_SENSORS_LM75) += lm75.o
如果 CONFIG_SENSORS_LM75 被设为 y,则 lm75.o 会被编译进内核;如果设为 m,则编译成 lm75.ko 模块。
我曾经在一个项目中,因为 Kconfig 里写错了 depends on,导致驱动在菜单里死活不出现。排查了半天,最后发现是依赖的子系统名字拼错了。嗯,这种低级错误,犯过一次就记住了。
3.3 内核配置裁剪与编译:从通用到定制
拿到一个通用内核源码,直接编译出来的镜像可能几十兆,对于嵌入式设备来说,太大了。我们需要裁剪。
3.3.1 配置裁剪的步骤
- 选择默认配置:
make ARCH=arm64 defconfig或make ARCH=arm64 xxx_defconfig(针对特定板子)。 - 手动微调:
make ARCH=arm64 menuconfig,进入图形界面,去掉不需要的功能(比如蓝牙、Wi-Fi、多余的文件系统)。 - 保存配置: 退出时保存为
.config文件。 - 备份配置: 把
.config复制一份,命名为my_defconfig,方便以后复用。
避坑指南: 我曾经在裁剪时,不小心把 CONFIG_DEVTMPFS 去掉了,结果系统启动后 /dev 目录是空的,设备节点一个都没有。折腾了一天才发现是这个问题。所以,裁剪时一定要保留基础的文件系统和设备管理选项。
3.3.2 编译内核
配置好了,编译就简单了:
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j4
-j4 表示用4个线程并行编译,能快不少。编译完成后,会在 arch/arm64/boot/ 下生成 Image 或 zImage,这就是我们要的内核镜像。
3.4 内核设备树(DTS)语法与实战
设备树,是嵌入式Linux移植中最核心、也最容易出错的部分。它用来描述硬件资源,告诉内核:你的板子上有哪些外设、它们挂在哪里、中断号是多少。
3.4.1 DTS基础语法
一个简单的设备树节点长这样:
/dts-v1/;
/ {
model = "MyBoard";
compatible = "mycompany,myboard";
chosen {
stdout-path = &uart0;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; // 512MB内存
};
uart0: serial@10000000 {
compatible = "ns16550";
reg = <0x10000000 0x1000>;
interrupts = <0 33 4>;
clock-frequency = <24000000>;
};
};
关键点解释:
/dts-v1/;:版本声明,必须放在第一行。/:根节点,整个板子的描述从这里开始。compatible:匹配字符串,内核通过它找到对应的驱动。reg:地址和大小,格式是<起始地址 长度>。interrupts:中断描述,格式取决于中断控制器。
3.4.2 实战:添加一个I2C设备
假设你的板子上有一个I2C温度传感器(LM75),挂在I2C总线地址0x48上。你需要在设备树里这样描述:
&i2c0 {
status = "okay";
clock-frequency = <100000>;
lm75@48 {
compatible = "lm75";
reg = <0x48>;
};
};
这里 &i2c0 是引用I2C控制器的标签,status = "okay" 表示启用这个控制器。子节点 lm75@48 就是我们的传感器,reg = <0x48> 是它的I2C地址。
我的经验: 写设备树时,最容易犯的错误是地址写错或中断号不对。我一般会先看芯片的参考手册,确认外设的基地址和中断号,再对照内核里已有的类似板子的DTS文件来写。抄作业,但别抄错。
3.4.3 编译设备树
设备树源文件(.dts)需要编译成二进制(.dtb)才能被内核使用:
make ARCH=arm64 dtbs
或者单独编译:
dtc -I dts -O dtb -o myboard.dtb myboard.dts
生成的 .dtb 文件,要么和内核镜像一起打包,要么由bootloader单独加载。
3.5 知识结构图:内核配置与设备树流程
下面这张图,把从源码到最终启动的整个流程串起来了。你可以对照着看,每一步都对应我们刚才讲的内容。
3.6 小结与避坑清单
这一章的内容,说白了就是三件事:看懂目录结构、学会配置裁剪、写对设备树。我最后给你列一个避坑清单,都是我在项目里踩过的雷:
- 配置丢失: 每次修改配置后,记得
make savedefconfig保存,否则下次make distclean就没了。 - DTS编译错误: 语法错误最常见的是少分号或括号不匹配。用
dtc -I dts -O dtb -o /dev/null myboard.dts只检查语法,不生成文件,快速排错。 - 中断号不对: 设备树里的中断号,一定要和芯片手册里的GIC中断号对应。ARM64一般用
GIC_SPI和GIC_PPI宏,别搞混。 - 地址重叠: 两个外设的
reg地址不能重叠,否则内核会报错。用cat /proc/iomem可以查看已占用的地址范围。