3、Linux内核源码结构:内核源码目录分析、Kconfig与Makefile机制、内核版本号与分支管理

拿到一份Linux内核源码,第一反应往往是「这么大一坨,从哪看起?」。我刚开始接触内核时也懵过,几万个文件,光目录就几十个。其实内核源码的组织方式非常讲究,搞懂了它的骨架,后面配置、裁剪、移植才能得心应手。

这一章咱们就拆开内核源码,看看每个目录是干嘛的,Kconfig和Makefile怎么配合工作,以及版本号里藏了哪些信息。

3.1 内核源码顶层目录结构

先看顶层。你下载一份主线内核,解压后第一眼看到的目录大概有这些:

目录/文件 作用 我个人的关注点
arch/ 体系结构相关代码,每个CPU架构一个子目录 做移植时主要泡在这里
block/ 块设备层核心代码 调IO性能时会看
crypto/ 加密算法实现 一般不动,除非做安全模块
Documentation/ 内核文档,含设备树绑定说明 调试时翻得最多的地方
drivers/ 设备驱动,占源码一半以上 写驱动时天天打交道
fs/ 文件系统实现 构建根文件系统时必看
include/ 内核头文件 找宏定义和结构体定义
init/ 内核初始化代码 启动流程分析入口
ipc/ 进程间通信 信号量、消息队列相关
kernel/ 核心调度、进程管理 调优时偶尔深入
lib/ 内核通用库函数 字符串操作、CRC等
mm/ 内存管理 调内存压力时必看
net/ 网络协议栈 做网络设备时绕不开
scripts/ 编译、配置辅助脚本 Kconfig解析器就在这里
security/ 安全模块(SELinux等) 嵌入式一般关掉
sound/ 音频子系统 ALSA驱动相关
usr/ initramfs打包相关 构建最小系统时常用

你看,drivers/ 占了半壁江山。我做过一个项目,裁剪内核时发现驱动目录里光 i2c/ 子目录就有上百个文件,实际用到的不到五个。所以嵌入式裁剪,第一刀往往砍在驱动上。

我的习惯:拿到一份新内核,先 ls -d */ 扫一眼顶层,再进 arch/ 确认目标架构是否存在。如果连架构目录都没有,那这个内核版本基本没法直接用。

3.2 Kconfig 与 Makefile 机制

内核的配置系统,说白了就是 Kconfig 定义「有什么选项」,Makefile 决定「怎么编译这些选项」。两者通过 CONFIG_XXX 宏串联起来。

3.2.1 Kconfig:菜单是怎么来的

每个目录下几乎都有一个 Kconfig 文件。它用一套专用语法描述配置项的类型、依赖和默认值。举个例子:

menu "Device Drivers"

config SENSORS_LM75
    tristate "LM75 temperature sensor"
    depends on I2C
    help
      If you say yes here, support for LM75 temperature sensor
      will be included.
      
endmenu

这里 tristate 表示可以编译进内核(y)、编译成模块(m)、或不编译(n)。depends on I2C 表示必须先开启 I2C 支持,这个选项才会出现。

我曾经踩过一个坑:在 menuconfig 里死活找不到某个驱动,后来发现是它的 depends on 依赖项没打开。你想想看,Kconfig 的依赖链有时候能串五六层,漏掉一个就全藏起来了。

3.2.2 Makefile:编译规则怎么写

内核的 Makefile 写法很简洁。同样是上面的 LM75 驱动,对应的 Makefile 里就一行:

obj-$(CONFIG_SENSORS_LM75) += lm75.o

如果 CONFIG_SENSORS_LM75 被配置为 y,这行就变成 obj-y += lm75.o,编译进内核。如果是 m,就变成 obj-m,编译成模块。如果是 n,整行被忽略。

这种设计非常巧妙。你不需要写 if-else,Makefile 自动根据配置展开。

关键点:Kconfig 和 Makefile 通过 CONFIG_ 前缀的宏名绑定。命名必须完全一致,大小写敏感。我见过新手把 Kconfig 里写成 CONFIG_MY_DRV,Makefile 里写成 obj-$(CONFIG_MY_DRV),结果编译出来驱动根本没进去——因为宏名拼错了。

3.2.3 配置文件的生成流程

整个配置过程是这样的:

  1. make menuconfig 读取各目录的 Kconfig,生成菜单界面
  2. 你选择选项后,结果写入 .config 文件
  3. make 时,.config 被转换为 include/generated/autoconf.h,C 代码里就能用 #ifdef CONFIG_XXX 了
  4. 同时 Makefile 根据 .config 决定编译哪些文件

下面这张图把流程串起来了:

Kconfig / Makefile 配置编译流程 Kconfig 文件 定义配置选项 make menuconfig 生成配置界面 .config 保存配置结果 autoconf.h C 代码条件编译 Makefile 决定编译对象 vmlinux / .ko 最终内核/模块 说明: Kconfig 定义选项 → menuconfig 交互选择 → .config 保存结果 .config 同时驱动 autoconf.h(C代码)和 Makefile(编译规则)

3.3 内核版本号与分支管理

版本号这东西,看着简单,其实门道不少。你执行 uname -r 看到的字符串,比如 5.10.120-rt65,每一段都有含义。

3.3.1 版本号格式

标准格式是:

主版本号.次版本号.补丁级别[-附加标签]

举个例子:

版本 含义
5.10.120 主线版本 5.10 的第 120 个补丁
5.10.120-rt65 同上,但打了实时内核补丁(PREEMPT_RT)
6.1.0-rc3 6.1 版本的第三个候选发布版
5.15.0-100-generic Ubuntu 发行版定制内核

这里有个历史惯例:次版本号为偶数的表示稳定版(如 5.10、6.1),奇数的表示开发版(如 5.11、6.2)。不过从 5.0 之后这个规则已经没那么严格了,但很多长期支持(LTS)版本还是遵循这个习惯。

注意:嵌入式项目选内核版本时,我建议优先选 LTS 版本。比如 5.10 LTS、6.1 LTS,维护周期长,社区持续修 bug。我曾经在一个产品上用了非 LTS 的 5.13,半年后社区停止维护,遇到安全漏洞只能自己 backport 补丁,非常痛苦。

3.3.2 分支管理策略

内核社区的分支管理,说白了就是「多级维护树」:

  • Linus Torvalds 主线(mainline):最上游,每天都有新提交,变化最快
  • 稳定树(stable):由 Greg Kroah-Hartman 维护,只合入 bug 修复,不添加新功能
  • LTS 树(longterm):从稳定树中选出的长期维护分支,嵌入式首选
  • 发行版内核:如 Debian、Ubuntu、Yocto 各自维护的分支,会 backport 驱动和补丁

我个人的做法是:产品开发阶段跟踪 LTS 主线,量产时锁定某个 LTS 小版本,只合入关键安全补丁。这样既保证稳定性,又避免被上游的频繁变动拖垮。

3.3.3 如何查看当前内核版本

在目标板上:

# 查看完整版本
cat /proc/version

# 或者
uname -a

# 只看版本号
uname -r

在源码目录里:

# 顶层 Makefile 顶部定义了版本
head -5 Makefile

# 输出类似:
# VERSION = 5
# PATCHLEVEL = 10
# SUBLEVEL = 120
# EXTRAVERSION = -rt65
小技巧:如果你在多个内核版本间切换,可以用 git tag 查看当前源码对应的标签。比如 git describe --tags 会输出类似 v5.10.120-rt65 的字符串,一眼就知道源码版本。

3.4 本章小结

内核源码结构其实不复杂,关键是把几个核心目录搞明白:arch/ 管架构、drivers/ 管驱动、fs/ 管文件系统。Kconfig 和 Makefile 的配合机制是配置裁剪的基础,理解了 obj-$(CONFIG_XXX) 这种写法,你就能看懂内核的编译系统。版本号方面,记住 LTS 优先,别追新。