Read OSS

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 文件中被显式指定,并非按字母排列,而是经过深思熟虑的:

Kbuild#L90-L111

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_operationsstruct inode_operationsstruct 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 条目,包含类型(booltristateintstring)、默认值、依赖关系和帮助文本。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-yobj-$(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() 初始化序列的完整过程,亲眼见证这些子系统如何在一场精心编排的启动流程中依次唤醒。