Linux 内核架构:源码探索者的导航地图
前置知识
- ›C 语言基础编程知识
- ›对操作系统的基本概念有所了解
Linux 内核架构:源码探索者的导航地图
Linux 内核拥有约 3000 万行代码,由数千名开发者在三十年间共同构建。第一次打开这个代码库,很容易感到不知所措——并非因为某个文件难以理解,而是根本找不到入口。它不像一个 Web 框架那样有清晰的 main() 和请求处理器;内核是一个庞大的、依条件编译的系统,代码树中有一半的代码在任意给定的构建中都不会被编译进去。本文就是你的导航地图。
我们将依次介绍顶层目录结构、将硬件相关代码与可移植逻辑分离的分层架构、内核内部头文件与用户空间头文件之间的关键边界,以及决定哪些代码最终会被编译的 Kconfig/Kbuild 系统。读完本文,你在探索任何内核子系统时都能知道该去哪里找。
3000 万行代码的挑战
阅读内核时,首先要明白一件事:你永远不会读完整个内核。在典型的 x86-64 服务器构建中,实际编译的源文件大约只占代码树的 10%–15%。其余的是针对其他架构的代码、被禁用的特性,以及你手头并没有的硬件驱动。构建系统存在的意义,正是管理这种选择性编译。
MAINTAINERS 文件超过 29,000 行,是你的"罗塞塔石碑"。每个子系统、驱动和文件路径模式都对应着具体的维护者、邮件列表和状态信息。当你遇到不熟悉的目录时,MAINTAINERS 能告诉你它归谁负责、讨论在哪里发生。
提示: 对任意文件运行
scripts/get_maintainer.pl,可以立即找到对应的维护者和邮件列表。它会帮你解析MAINTAINERS文件:./scripts/get_maintainer.pl fs/ext4/super.c。
顶层目录分类体系
顶层目录结构体现了内核被刻意分解为各个子系统的设计思路。各目录的用途如下:
| 目录 | 用途 |
|---|---|
init/ |
启动时的初始化,包含 start_kernel() |
kernel/ |
核心内核:调度、信号、锁、定时器、追踪 |
mm/ |
内存管理:页分配器、slab、虚拟内存 |
fs/ |
虚拟文件系统(VFS)及所有文件系统实现 |
net/ |
网络协议栈:TCP/IP、socket、netfilter |
drivers/ |
设备驱动——代码树中体量最大的目录(约占 60%) |
arch/ |
架构相关代码:x86、arm64、riscv 等 |
include/ |
头文件:内核内部头文件与 UAPI 头文件 |
block/ |
文件系统与磁盘驱动之间的块 I/O 层 |
io_uring/ |
异步 I/O 子系统(2022 年升级为顶层目录) |
security/ |
LSM 框架:SELinux、AppArmor 等 |
crypto/ |
加密算法与 API |
rust/ |
Rust 语言支持及 kernel crate |
virt/ |
虚拟化支持(KVM 位于 arch/ 下,但 virt/ 包含共享部分) |
sound/ |
ALSA 音频子系统 |
scripts/ |
构建脚本、代码检查工具、维护者辅助工具 |
tools/ |
用户空间工具:perf、bpftool、selftests |
lib/ |
内核内部库函数:排序、解压、字符串处理等 |
ipc/ |
System V IPC:共享内存、信号量、消息队列 |
certs/ |
用于模块验证的签名证书 |
usr/ |
initramfs 生成 |
graph TD
subgraph "Core Kernel"
init["init/"]
kernel["kernel/"]
mm["mm/"]
lib["lib/"]
ipc["ipc/"]
end
subgraph "Subsystems"
fs["fs/"]
net["net/"]
block["block/"]
io_uring["io_uring/"]
security["security/"]
crypto["crypto/"]
sound["sound/"]
virt["virt/"]
end
subgraph "Hardware Abstraction"
arch["arch/"]
drivers["drivers/"]
end
subgraph "Newer Additions"
rust["rust/"]
end
init --> kernel
kernel --> mm
kernel --> arch
fs --> block
block --> drivers
io_uring --> fs
构建顺序在顶层 Kbuild 文件中被显式指定,并非按字母排列,而是经过深思熟虑的:
obj-y += init/
obj-y += usr/
obj-y += arch/$(SRCARCH)/
...
obj-y += kernel/
obj-y += mm/
obj-y += fs/
...
obj-$(CONFIG_BLOCK) += block/
obj-$(CONFIG_IO_URING) += io_uring/
obj-$(CONFIG_RUST) += rust/
...
obj-y += drivers/
注意这里的规律:obj-y 表示"始终编译",而 obj-$(CONFIG_*) 表示"仅在该配置项被启用时才编译"。块层、io_uring、Rust 支持、网络协议栈以及示例代码都是条件编译的。这就是 Kconfig 系统的运作方式,我们稍后会详细介绍。
三层架构
内核代码遵循三层架构,但这是一种约定俗成的规范,而非由编译器强制保证:
flowchart TB
subgraph Layer1["Layer 1: Architecture-Specific"]
direction LR
x86["arch/x86/"]
arm64["arch/arm64/"]
riscv["arch/riscv/"]
end
subgraph Layer2["Layer 2: Core Kernel"]
direction LR
sched["kernel/sched/"]
mm2["mm/"]
vfs["fs/ (VFS)"]
netcore["net/core/"]
end
subgraph Layer3["Layer 3: Drivers & Filesystems"]
direction LR
dri["drivers/gpu/"]
ext4["fs/ext4/"]
tcp["net/ipv4/"]
nvme["drivers/nvme/"]
end
Layer1 -->|"defined interfaces<br/>(asm/ headers)"| Layer2
Layer2 -->|"operations structs<br/>(vtables)"| Layer3
第一层(架构相关) 包含因 CPU 架构不同而存在差异的最底层代码:启动汇编、中断处理、页表格式、系统调用入口点。每种架构都位于 arch/<name>/ 下,并通过 arch/<name>/include/asm/ 中的头文件对外暴露定义良好的接口。
第二层(核心内核) 包含可移植的子系统:kernel/sched/ 中的调度器、mm/ 中的内存管理、fs/ 中的 VFS 以及 net/ 中的网络栈。这层代码通过架构相关的函数原型调用第一层,同时通过基于 C 结构体的"虚函数表"为第三层提供扩展点——这一模式将在第 3、4 篇文章中深入探讨。
第三层(驱动与文件系统实现) 是代码量最集中的地方。以 ext4 为例,它实现了 struct file_operations、struct inode_operations 和 struct super_operations,以此接入 VFS。设备驱动则实现其所在总线(PCI、USB、platform)对应的操作结构体。这一层永远不会直接接触架构相关代码,始终通过第二层的抽象进行交互。
提示: 阅读陌生的驱动时,先找到它的操作结构体。这些结构体清楚地告诉你该驱动实现了哪些第二层接口。
内核头文件与用户空间 API 头文件
include/ 目录有一个至关重要的细分结构,许多新手都会忽视:
include/linux/— 内核内部头文件。这些文件可能在版本之间不经通知地发生变更,只有内核代码才会包含它们。include/uapi/linux/— 面向用户空间的 API 头文件。这些文件定义了内核与用户空间应用之间稳定的 ABI。修改它们是一件严肃的事,必须保证向后兼容。
这种分离源于 2012 年前后的"UAPI 拆分"工作。在此之前,内核内部定义和用户空间定义混杂在同一批头文件中,依靠 #ifdef __KERNEL__ 守卫来隐藏内部内容。现在的目录结构则在层级上明确划定了这条边界。
举个例子,include/uapi/linux/io_uring.h 定义了用户空间程序写入的提交队列条目(struct io_uring_sqe)。它采用双重许可证(GPL OR MIT),以便 liburing 等用户空间库可以直接包含它。而 include/linux/io_uring_types.h 则定义了用户空间永远不会看到的内核内部结构体 struct io_ring_ctx。
规则很简单:位于 uapi/ 下的内容,是对用户空间程序做出的承诺;其余一切都是实现细节。
MAINTAINERS 归属模型
内核并非由一个团队统一管理的整体,而是一个由各子系统组成的联邦,每个子系统都有指定的维护者。MAINTAINERS 文件通过如下条目来编码这种结构:
FILESYSTEMS (VFS and infrastructure)
M: Alexander Viro <viro@zeniv.linux.org.uk>
M: Christian Brauner <brauner@kernel.org>
L: linux-fsdevel@vger.kernel.org
S: Maintained
F: fs/*
F: include/linux/fs.h
每个条目将文件路径模式(F:)映射到维护者(M:)、审阅者(R:)、邮件列表(L:)和状态(S:)。get_maintainer.pl 脚本利用这些模式来确定某个补丁应该由谁来审查。
这种归属模型意味着,不同子系统在代码质量标准、命名规范和审查严格程度上各有差异。例如,网络协议栈的代码审查就以严格著称。了解一个子系统归谁负责,有助于你理解其代码风格与文化。
Kconfig 与 Kbuild:大规模条件编译
内核的构建系统由两部分组成:Kconfig 定义哪些选项可以配置,Kbuild 负责编译已选定的内容。
Kconfig:配置语言
根目录下的 Kconfig 文件按照既定顺序引入各子系统的配置文件:
mainmenu "Linux/$(ARCH) $(KERNELVERSION) Kernel Configuration"
source "scripts/Kconfig.include"
source "init/Kconfig"
source "kernel/Kconfig.freezer"
source "fs/Kconfig.binfmt"
source "mm/Kconfig"
source "net/Kconfig"
source "drivers/Kconfig"
...
source "io_uring/Kconfig"
每个被引入的文件都定义了若干 config 条目,包含类型(bool、tristate、int、string)、默认值、依赖关系和帮助文本。tristate 选项可以取三个值:y(内置编译)、m(编译为模块)或 n(禁用)。CONFIG_* 符号的总数超过 20,000 个。
flowchart LR
A["make menuconfig"] --> B[".config file<br/>(CONFIG_* = y/m/n)"]
B --> C["include/config/auto.conf<br/>(processed config)"]
C --> D["Kbuild Makefiles<br/>obj-$(CONFIG_*) rules"]
D --> E["vmlinux binary"]
Kbuild:条件对象包含
内核中的每个目录都有一个 Makefile(或 Kbuild 文件),通过 obj-y 和 obj-$(CONFIG_*) 来控制哪些文件会被编译。这套模式简洁而优雅:
obj-$(CONFIG_EXT4_FS) += ext4/
obj-$(CONFIG_XFS_FS) += xfs/
obj-$(CONFIG_BTRFS_FS) += btrfs/
当 CONFIG_EXT4_FS=y 时,表达式展开为 obj-y += ext4/,ext4 被编译进内核。当 CONFIG_EXT4_FS=m 时,展开为 obj-m += ext4/,构建为可加载模块。当 CONFIG_EXT4_FS=n(或未设置)时,整个 ext4 目录被跳过。
顶层 Makefile 负责协调最终的链接过程,将所有内置对象打包进 vmlinux.a,再链接生成 vmlinux 二进制文件:
targets += vmlinux.a
vmlinux.a: $(KBUILD_VMLINUX_OBJS) scripts/head-object-list.txt FORCE
$(call if_changed,ar_vmlinux.a)
flowchart TD
subgraph "Build Phases"
K["Kconfig<br/>Configuration"] --> P["Preprocessing<br/>bounds.h, offsets.h"]
P --> C["Compilation<br/>Per-directory Makefiles"]
C --> A["Archive<br/>vmlinux.a"]
A --> L["Link<br/>vmlinux"]
L --> Z["Compress<br/>bzImage/zImage"]
end
这套系统意味着,内核不只是一个程序——它是一个可以生成数千种不同程序的框架,每种程序都针对特定的硬件和功能需求量身定制。一个嵌入式 IoT 内核可能只编译 2,000 个文件,而发行版内核可能编译 10,000 个。整个源码树同时支持这两种场景。
下一步
有了这份地图,你已经知道各部分代码住在哪里,以及背后的设计原因。在下一篇文章中,我们将从上电后的第一条指令开始,跟踪内核进入 start_kernel() 初始化序列的完整过程,亲眼见证这些子系统如何在一场精心编排的启动流程中依次唤醒。