3、Linux内核源码结构与配置:内核源码目录结构、Kconfig与Makefile机制、内核配置工具(menuconfig)、内核编译与模块编译。
做BSP开发,打交道最多的就是Linux内核源码。很多人一开始面对几万个文件、上千个目录,直接就懵了。我当年刚入行时也一样,对着内核源码不知道从哪下手。
其实没那么复杂。内核源码的结构是有章可循的。你只要搞懂它的骨架——目录结构、配置系统和编译系统,剩下的就是往里面填功能了。
3.1 内核源码目录结构
先说说顶层目录。你下载一份内核源码,解压后看到的那些文件夹,每个都有明确分工。我列个表,你对照着看就清楚了。
| 目录 | 作用 | 我常用的关注点 |
|---|---|---|
arch/ |
体系结构相关代码 | BSP开发的核心区域,arch/arm/、arch/arm64/、arch/x86/ 最常用 |
drivers/ |
设备驱动 | 占了源码一半以上,字符设备、网络设备、I2C、SPI、USB 都在这里 |
kernel/ |
核心调度、进程管理 | 一般不动,除非做内核调优 |
mm/ |
内存管理 | 页分配、slab、VMA 相关 |
fs/ |
文件系统 | ext4、procfs、sysfs、debugfs 都在这里 |
net/ |
网络协议栈 | TCP/IP、socket、netfilter |
include/ |
头文件 | 找宏定义、结构体定义的第一站 |
scripts/ |
编译脚本、工具 | Kconfig 解析、dtc 设备树编译都在这里 |
Documentation/ |
内核文档 | 我遇到配置选项看不懂时,经常来这里翻 |
嗯,这里要注意。做BSP时,你打交道最多的就是 arch/ 和 drivers/。比如你要移植一个板子,就得在 arch/arm/mach-xxx/ 下加板级文件,或者在 arch/arm64/boot/dts/ 下写设备树。
arch/ 下有没有我目标芯片的目录。没有?那就得从相近的芯片目录复制一份改。有?那就省大事了。
3.2 Kconfig 与 Makefile 机制
内核的配置和编译,靠的是两套文件:Kconfig 和 Makefile。它们像一对搭档,一个管"要不要编译",一个管"怎么编译"。
3.2.1 Kconfig——配置选项的定义
Kconfig 文件定义了内核的配置选项。每个驱动、每个功能模块,都会在对应的 Kconfig 里声明。语法其实不复杂,我写个例子你就明白了。
# drivers/i2c/Kconfig 中的一段
config I2C_CHARDEV
tristate "I2C character device"
depends on I2C
help
Say Y here to support I2C character device interface.
This is needed for i2c-dev userspace access.
这里 tristate 表示三种状态:Y(编译进内核)、M(编译成模块)、N(不编译)。depends on 是依赖关系,select 是反向选择,help 是帮助信息。
我曾经踩过一个坑:给一个新板子加驱动,明明在 menuconfig 里看到了选项,选上后编译却报错。查了半天,发现是 Kconfig 里漏写了 depends on,导致依赖的子系统没被选上。所以,写 Kconfig 时一定要把依赖链理清楚。
3.2.2 Makefile——编译的执行
Makefile 负责把 Kconfig 的选择变成实际的编译动作。内核的 Makefile 是递归的,顶层 Makefile 会逐层进入子目录。
# drivers/i2c/Makefile
obj-$(CONFIG_I2C_CHARDEV) += i2c-dev.o
obj-$(CONFIG_I2C_ALGOBIT) += i2c-algo-bit.o
obj-$(CONFIG_I2C_GPIO) += i2c-gpio.o
这里的 obj-y 表示编译进内核,obj-m 表示编译成模块,obj-n 表示不编译。变量名 CONFIG_I2C_CHARDEV 就是 Kconfig 里定义的那个配置项。
.config 文件,Makefile 读取 .config 中的变量,决定哪些 .c 文件被编译。说白了,就是"配置驱动编译"。
3.3 内核配置工具(menuconfig)
手动改 .config 文件太容易出错了。我刚开始学内核时,直接编辑 .config,结果漏了一个依赖,编译出来内核直接 panic。后来就老老实实用 menuconfig 了。
启动方式很简单:
make menuconfig
它会打开一个基于 ncurses 的图形界面。你想想看,几万个配置选项,如果没有搜索功能,找起来得多痛苦。menuconfig 支持 / 键搜索,这个功能我几乎天天用。
常用的操作键:
- 方向键:上下移动
- 空格:切换状态(Y/M/N)
- /:搜索配置项
- ?:查看帮助和依赖关系
- Z:显示所有选项(包括被隐藏的)
3.4 内核编译与模块编译
配置好了,就该编译了。内核编译分两种:整体编译 和 模块编译。做BSP开发时,这两种场景都会遇到。
3.4.1 整体编译
最常用的命令:
# 编译内核和模块
make -j$(nproc)
# 只编译内核,不编译模块
make -j$(nproc) zImage
# 只编译设备树
make -j$(nproc) dtbs
-j$(nproc) 是并行编译,用满所有CPU核心。我习惯先 make -j4 试一次,没问题再开满核心。为什么?因为并行编译时错误信息会乱序,不好排查。
3.4.2 模块编译
开发驱动时,每次都全量编译太慢了。我一般只编译自己改的那个模块:
# 编译单个模块
make -j$(nproc) M=drivers/i2c
# 编译并安装模块到临时目录
make -j$(nproc) M=drivers/i2c modules_install INSTALL_MOD_PATH=/tmp/modules
这里 M= 指定模块路径,内核会只编译那个目录下的代码。速度比全量编译快一个数量级。
3.4.3 编译后的产物
编译完成后,关键文件都在 arch/arm/boot/(以ARM为例)下:
| 文件 | 说明 |
|---|---|
zImage |
压缩后的内核镜像,最常用 |
Image |
未压缩的内核镜像 |
uImage |
U-Boot 格式的内核镜像 |
dts/ |
编译后的设备树 .dtb 文件 |
vmlinux |
顶层目录下的未压缩内核,带调试信息 |
vmlinux,因为它包含符号表,gdb 可以直接用。发布时用 zImage,体积小、加载快。
3.5 知识体系总览
说了这么多,我把整个流程画成一张图,方便你理解。
这张图把整个流程串起来了。从源码目录开始,Kconfig 定义配置项,menuconfig 让你交互式选择,生成 .config,然后 Makefile 根据 .config 决定编译哪些文件,最终输出内核镜像、模块和设备树。
做BSP开发,这套流程你几乎每天都要走一遍。刚开始可能觉得繁琐,但熟练之后,你会发现它其实很优雅——配置和编译完全解耦,想加功能就加 Kconfig,想改编译就改 Makefile,互不干扰。