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 里定义的那个配置项。

核心逻辑:Kconfig 生成 .config 文件,Makefile 读取 .config 中的变量,决定哪些 .c 文件被编译。说白了,就是"配置驱动编译"。

3.3 内核配置工具(menuconfig)

手动改 .config 文件太容易出错了。我刚开始学内核时,直接编辑 .config,结果漏了一个依赖,编译出来内核直接 panic。后来就老老实实用 menuconfig 了。

启动方式很简单:

make menuconfig

它会打开一个基于 ncurses 的图形界面。你想想看,几万个配置选项,如果没有搜索功能,找起来得多痛苦。menuconfig 支持 / 键搜索,这个功能我几乎天天用。

常用的操作键:

  • 方向键:上下移动
  • 空格:切换状态(Y/M/N)
  • /:搜索配置项
  • ?:查看帮助和依赖关系
  • Z:显示所有选项(包括被隐藏的)
避坑指南:我曾经在 menuconfig 里选了一个驱动,保存退出后重新进入,发现选项又变回 N 了。后来发现是因为它的依赖项没选上,内核自动把它置为不可选。所以,选配置时一定要看底部的提示信息,它会告诉你哪些依赖没满足。

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 知识体系总览

说了这么多,我把整个流程画成一张图,方便你理解。

Linux内核源码结构与配置流程 内核源码目录 Kconfig 配置定义 Makefile 编译规则 menuconfig 配置工具 .config 配置文件 编译决策 (Y/M/N) zImage / Image .ko 内核模块 .dtb 设备树 Kconfig 定义选项 → menuconfig 交互配置 → .config 记录选择 → Makefile 按规则编译 → 输出内核/模块/设备树

这张图把整个流程串起来了。从源码目录开始,Kconfig 定义配置项,menuconfig 让你交互式选择,生成 .config,然后 Makefile 根据 .config 决定编译哪些文件,最终输出内核镜像、模块和设备树。

做BSP开发,这套流程你几乎每天都要走一遍。刚开始可能觉得繁琐,但熟练之后,你会发现它其实很优雅——配置和编译完全解耦,想加功能就加 Kconfig,想改编译就改 Makefile,互不干扰。