3. Linux 内核源码结构解析:内核源码目录树、Kconfig 与 Makefile 机制、内核配置系统

拿到一份 Linux 内核源码,第一反应往往是「这么大一坨,从哪看起?」。我刚开始接触内核时也懵,后来发现,只要搞懂三样东西——目录树、Kconfig 和 Makefile,整个内核的骨架就清晰了。说白了,这三者就是内核的「地图」、「菜单」和「施工图」。

3.1 内核源码目录树:一张地图走天下

解压内核源码后,顶层目录大概长这样(以 5.x 版本为例):

linux-5.x/
├── arch/          # 架构相关代码(ARM、x86、RISC-V 等)
├── block/         # 块设备层
├── crypto/        # 加密 API
├── Documentation/ # 内核文档
├── drivers/       # 设备驱动(占源码一半以上)
├── fs/            # 文件系统
├── include/       # 内核头文件
├── init/          # 内核初始化
├── ipc/           # 进程间通信
├── kernel/        # 核心子系统(调度、信号、时间等)
├── lib/           # 内核工具函数
├── mm/            # 内存管理
├── net/           # 网络协议栈
├── scripts/       # 编译脚本、工具
├── security/      # 安全模块
├── sound/         # 音频子系统
├── tools/         # 用户态工具
├── usr/           # initramfs 相关
├── virt/          # 虚拟化(KVM)
├── Kconfig        # 顶层配置菜单
├── Makefile       # 顶层构建文件
└── README         # 你懂的

嗯,这里要注意:arch/ 和 drivers/ 是我们嵌入式开发最常打交道的两个目录。arch/ 下面按 CPU 架构分,比如 arch/arm/、arch/arm64/、arch/x86/。drivers/ 下面按设备类型分,比如 drivers/i2c/、drivers/spi/、drivers/net/。

我的习惯:拿到一个新板子,先看 arch/arm/boot/dts/ 下的设备树文件,再看 drivers/ 下对应外设的驱动。这样能快速定位硬件相关的代码。

3.2 Kconfig 机制:内核的「点菜系统」

Kconfig 是什么?说白了就是内核的配置菜单描述语言。每个目录下的 Kconfig 文件定义了该目录有哪些配置选项,以及它们之间的依赖关系。

举个例子,drivers/i2c/Kconfig 里可能有这么一段:

config I2C_CHARDEV
    tristate "I2C device interface"
    depends on I2C
    help
      Say Y here to use I2C device interface.
      This is needed for i2c-dev driver.

这里的关键字我解释一下:

关键字 含义 取值
config 定义一个配置选项 选项名(如 I2C_CHARDEV)
tristate 三态:不编译/编译进内核/编译为模块 y / m / n
bool 布尔:编译或不编译 y / n
depends on 依赖另一个选项 如 depends on I2C
select 反向选择(自动选中另一个选项) 如 select CRC32
help 帮助信息 文本描述
我曾经踩过的坑:配置内核时,明明选了某个驱动,编译却没编进去。后来发现是 depends on 的依赖选项没开。比如 I2C_CHARDEV 依赖 I2C,I2C 没开,这个选项就自动隐藏了。所以配置时一定要先看依赖链。

3.3 Makefile 机制:内核的「施工图」

每个有源码的目录下都有 Makefile,告诉内核怎么编译这些文件。内核的 Makefile 写法跟普通应用不同,它用的是 obj-y、obj-m、obj-$(CONFIG_XXX) 这种模式。

看个实际例子,drivers/i2c/Makefile:

obj-y           += i2c-core.o i2c-dev.o
obj-$(CONFIG_I2C_CHARDEV) += i2c-dev.o
obj-$(CONFIG_I2C_GPIO)    += i2c-gpio.o

解释一下:

  • obj-y:无条件编译进内核
  • obj-m:编译成可加载模块(.ko 文件)
  • obj-$(CONFIG_XXX):根据配置决定是否编译

你想想看,如果 CONFIG_I2C_CHARDEV 配置为 y,那 obj-$(CONFIG_I2C_CHARDEV) 就变成 obj-y,i2c-dev.o 就编进内核。如果配置为 m,就变成 obj-m,编成模块。如果没配置,就变成 obj-n,不编译。

核心逻辑:Kconfig 定义「能不能选」,Makefile 决定「怎么编」。两者通过 CONFIG_XXX 这个变量串联起来。

3.4 内核配置系统:从 menuconfig 到 .config

整个配置系统的流程是这样的:

  1. make menuconfig 读取各目录的 Kconfig,生成配置界面
  2. 你在界面里选好选项,保存为 .config 文件
  3. 编译时,顶层 Makefile 读取 .config,生成 include/generated/autoconf.h
  4. 源码通过 #include 这个头文件,使用 CONFIG_XXX 宏来控制代码分支

我画了一张图,帮你理清这个流程:

内核配置系统流程图 内核配置系统工作流程 Kconfig 文件 make menuconfig .config 文件 读取 保存 autoconf.h 生成 源码编译 #include zImage / .ko 💡 关键点 Kconfig → 定义选项 Makefile → 使用选项 .config → 连接两者

3.5 实战:如何添加一个自己的配置选项

假设你要在 drivers/misc/ 下加一个自己的驱动,叫 my_driver。步骤很简单:

  1. 创建 Kconfig 文件(如果已有就编辑它)
config MY_DRIVER
    tristate "My custom driver"
    depends on I2C
    help
      This is a custom driver for my project.
      Say Y/M here if you need this driver.
  1. 编辑 Makefile
obj-$(CONFIG_MY_DRIVER) += my_driver.o
  1. 在上级 Kconfig 中 source 子目录
source "drivers/misc/Kconfig"
  1. 运行 make menuconfig,找到你的选项,选上
  2. 编译,搞定
我的建议:刚开始学内核裁剪,别一上来就改核心代码。先在 drivers/misc/ 或 drivers/staging/ 下练手,这两个目录比较「安全」,不会影响系统稳定性。

3.6 避坑指南:配置系统的常见问题

  • 配置选项找不到? 检查依赖链,看看 depends on 的选项开了没。我遇到过好几次,折腾半天发现是父选项没开。
  • 编译报 undefined reference? 大概率是某个依赖的模块没编进去。用 make menuconfig 的搜索功能(按 / 键)查一下符号定义。
  • .config 被覆盖? 别直接改 .config,用 make oldconfig 或 make menuconfig 来更新。手动改 .config 很容易被 make 的依赖检查覆盖掉。
我曾经犯过的错:有一次为了赶项目,直接手动改了 .config 里的 CONFIG_XXX=y,结果 make 的时候被自动重置了。后来才发现,内核的配置系统有依赖检查,手动改的选项如果依赖不满足,会被自动清掉。正确做法是用 make menuconfig 或 make oldconfig。