Skip to content

Latest commit

 

History

History
2778 lines (2261 loc) · 133 KB

File metadata and controls

2778 lines (2261 loc) · 133 KB

Linux 内核整体架构权威深度解析

基于 Linux 6.x 主线内核源码的深度分析文档


目录

  1. 内核概述与设计哲学
  2. 源码目录深度解析
  3. 内核启动流程(源码级)
  4. 系统调用机制(源码级)
  5. 核心数据结构体系
  6. 内核并发模型
  7. 内存管理架构
  8. 进程调度架构
  9. VFS 与文件系统架构
  10. 网络子系统架构
  11. 设备驱动模型
  12. 安全架构
  13. eBPF 可编程内核
  14. 性能关键设计
  15. 内核扩展性机制对比

第 1 章:内核概述与设计哲学

1.1 Linux 是什么

Linux 内核是一个宏内核(Monolithic Kernel),自 1991 年 Linus Torvalds 发布 0.01 版本以来,已经演进为人类历史上代码规模最大、参与者最多的开源软件项目之一。

在哲学层面,Linux 的核心设计目标可以归纳为三点:

  1. UNIX 兼容性:遵循 POSIX 标准,提供稳定的用户空间 ABI(尤其是系统调用接口)
  2. 性能优先:在安全与性能的权衡中,内核倾向于选择性能,将安全责任部分交给用户空间
  3. 渐进式演化:新特性通过小增量合并,避免大规模重写

1.2 单体内核 vs 微内核的权衡

1992 年,Linus 与操作系统学者 Andrew Tanenbaum 在 comp.os.minix 上爆发了著名的"LINUX IS OBSOLETE"辩论。Tanenbaum 认为微内核是未来,单体内核是过时的架构;Linus 则坚持认为性能和实用性更重要。

微内核的理论优势:

  • 各子系统运行在用户空间,崩溃不影响整个系统
  • 模块化程度高,易于形式化验证(如 seL4)
  • IPC 边界天然形成安全隔离

单体内核的实际优势(Linux 的选择):

  • 子系统间直接函数调用,无 IPC 开销(微内核的 IPC 代价可达 10~100x)
  • 共享内核数据结构(如 task_struct),避免数据序列化
  • 硬件直接访问,中断延迟低

30 年后的今天,这场辩论并没有明确的胜负——但事实是:

Linux 运行在全球 >97% 的 Top500 超算、>70% 的手机(Android)、
>60% 的公有云服务器上,以及火星直升机 Ingenuity 上。
微内核 Minix 则被 Intel 内嵌在 ME 固件中默默运行。

现代 Linux 已经通过以下机制吸收了微内核的部分优点:

  • 内核模块:驱动可以动态加载/卸载
  • eBPF:经验证器安全验证的用户提供程序在内核中运行
  • 命名空间 + cgroups:容器化隔离
  • io_uring:将大量 I/O 工作异步化,减少系统调用次数

1.3 内核的核心约束

理解 Linux 内核必须先理解它所处的执行环境约束,这些约束决定了内核的诸多设计选择:

约束一:不能使用浮点运算

内核代码不能使用 FPU(浮点处理单元)或 SIMD 指令,原因在于:

  • 每次上下文切换需要保存/恢复 FPU 状态(x86 上 XSAVE 区域可达 2.5KB)
  • 如果内核随意使用 FPU,则每次中断/异常都需要保存/恢复 FPU 状态,代价极高
  • 解决方案:内核提供 kernel_fpu_begin()/kernel_fpu_end() 显式区域,仅在必要时(如 AES-NI 加密)才启用

约束二:内核栈空间极小

x86_64 上,每个进程的内核栈默认只有 16KBTHREAD_SIZE = PAGE_SIZE * 2 = 16384 字节)。这导致:

  • 内核函数不能使用大的栈局部变量(通常应 < 1KB)
  • 递归调用深度受严格限制
  • 不能在栈上分配大型数据结构,应使用 kmalloc() 等堆分配
  • 内核提供 CONFIG_FRAME_WARN 编译时警告超大栈帧

约束三:中断上下文不能阻塞

在硬中断(hardirq)、软中断(softirq)、tasklet 上下文中:

  • 不能调用任何可能休眠的函数(mutex_lockkmalloc(GFP_KERNEL) 等)
  • 原因:中断可能在任意进程上下文中触发,如果中断处理程序阻塞,系统将死锁
  • 只能使用 spinlockkmalloc(GFP_ATOMIC) 等非阻塞原语

约束四:不可抢占区段

持有 spinlock 时,内核是不可抢占的(或者说,内核抢占被禁用)。这意味着:

  • spinlock 保护的临界区必须尽量短暂
  • 不能在持有 spinlock 时进行任何可能阻塞或耗时的操作

1.4 代码规模与版本节奏

Linux 内核代码规模(约 2024 年数据):

  总文件数:~75,000 个源文件
  总代码行:~35,000,000 行(含注释和空行)
  活跃贡献者:每个开发周期约 1,700 名开发者
  主要赞助商:Intel, Google, Red Hat, Samsung, Meta, ...

版本发布节奏:
  每 8-10 周发布一个主版本(如 6.0, 6.1, 6.2, ...)
  合并窗口:约 2 周,接受新特性
  RC 阶段:约 6-8 周,仅修复 bug
  LTS 版本:每年选出 1 个版本维护 6 年(如 5.15, 6.1)

第 2 章:源码目录深度解析

2.1 顶层目录结构全貌

linux/
├── arch/          # 体系结构相关代码(x86, arm64, riscv, ...)
├── block/         # 块设备层(bio, request queue, I/O 调度器)
├── certs/         # 内核模块签名证书
├── crypto/        # 密码学算法库(AES, SHA, RSA, ...)
├── Documentation/ # 内核文档
├── drivers/       # 设备驱动(最大目录,约 60% 代码量)
├── fs/            # 文件系统实现(ext4, btrfs, xfs, tmpfs, ...)
├── include/       # 内核头文件
├── init/          # 内核初始化代码
├── io_uring/      # io_uring 异步 I/O 框架
├── ipc/           # 进程间通信(System V IPC, POSIX mq)
├── kernel/        # 核心子系统(调度、信号、锁、cgroup, ...)
├── lib/           # 通用库函数(xarray, maple tree, 压缩算法)
├── mm/            # 内存管理子系统
├── net/           # 网络子系统
├── rust/          # Rust 语言内核绑定(较新)
├── samples/       # 示例代码(BPF 程序、驱动示例)
├── scripts/       # 构建脚本、工具链配置
├── security/      # LSM 安全模块(SELinux, AppArmor, BPF-LSM)
├── sound/         # 音频子系统(ALSA)
├── tools/         # 用户空间工具(perf, bpftool, ...)
├── usr/           # initramfs 生成工具
└── virt/          # 虚拟化(KVM 通用代码)

2.2 关键目录深度解析

arch/ — 体系结构抽象层

arch/ 是内核中最复杂的目录之一,它为每种处理器架构提供了一套完整的低层实现。

arch/
├── x86/           # x86/x86_64(最复杂,约 900K 行)
│   ├── boot/      # 早期启动代码(实模式→长模式)
│   ├── entry/     # 系统调用入口、中断向量表
│   ├── include/   # x86 特有头文件
│   ├── kernel/    # CPU 初始化、SMP、APIC、ACPI
│   ├── mm/        # x86 内存管理(页表、NUMA)
│   └── crypto/    # AES-NI、SHA-NI 硬件加速
├── arm64/         # ARMv8/AArch64(第二大,约 300K 行)
├── riscv/         # RISC-V(增长最快)
└── ...            # 共约 25 种架构

arch/ 目录通过以下机制对上层提供统一接口:

  • asm/ 符号链接指向具体架构的 include/asm/
  • 内核 Makefile 通过 ARCH= 变量选择编译哪个架构
  • 通用代码通过 include/asm-generic/ 提供默认实现,架构可以覆盖

drivers/ — 设备驱动王国

行数估算(约):
  drivers/gpu/     ~3,500,000 行(最大,DRM/KMS 框架 + GPU 驱动)
  drivers/net/     ~2,000,000 行
  drivers/scsi/    ~1,200,000 行
  drivers/sound/   已迁移到 sound/
  drivers/usb/     ~600,000 行
  drivers/pci/     ~300,000 行

drivers/ 的设计哲学是:驱动通过注册到各子系统框架(platform bus、PCI bus、USB bus 等)与内核集成,而不是直接调用内核内部函数。这种框架化设计使得驱动代码相对独立,可以被编译为可加载模块(.ko)。

kernel/ — 内核核心

kernel/ 的主要组件:
  sched/      进程调度器(CFS、RT、Deadline、sched_ext)
  locking/    锁机制(spinlock、mutex、rwlock、seqlock)
  rcu/        RCU(Read-Copy-Update)实现
  time/       时钟子系统、定时器、NOHZ
  irq/        中断描述符、中断线程化
  trace/      ftrace 追踪框架
  bpf/        eBPF 核心(verifier、JIT、maps)
  cgroup/     Control Group v1/v2 核心
  seccomp.c   seccomp 系统调用过滤
  signal.c    信号处理
  fork.c      进程创建(fork/clone/vfork)
  exit.c      进程退出

2.3 init/main.c 的特殊地位

init/main.c 是整个内核的**"乐谱"**——它不实现任何具体功能,而是以正确的顺序指挥所有子系统的初始化。

文件开头注释(init/main.c:1-11):

// SPDX-License-Identifier: GPL-2.0-only
/*
 *  linux/init/main.c
 *
 *  Copyright (C) 1991, 1992  Linus Torvalds
 *  ...
 */

这个文件的依赖关系极为复杂,包含了几乎所有子系统的头文件(init/main.c:15-118),超过 100 个 #include,涵盖调度、内存、网络、安全、RCU 等所有主要子系统。

2.4 include/linux/ vs include/uapi/ 的区别

这是一个重要的架构决策,直接关系到内核 ABI 的稳定性。

include/
├── linux/         # 内核内部头文件(可以随时改变)
├── uapi/          # 用户空间 API(必须保持稳定)
│   ├── linux/     # 系统调用参数、ioctl 结构体
│   └── asm/       # 架构相关的用户空间接口
└── asm-generic/   # 通用架构实现(可被覆盖)

include/uapi/ 的 ABI 稳定性承诺

  • 一旦合入主线,uapi/ 中的接口永远不能破坏性修改
  • 这是 Linux 的核心承诺:任何在旧内核上编译的用户程序,必须能在更新的内核上运行
  • 具体例子:struct stat 的布局自 1992 年以来从未改变(通过 stat64statx 扩展而不是修改)

include/linux/ 的自由度

  • 内核内部接口可以随时修改,不需要保持向后兼容
  • 这也是为什么内核不提供稳定的内核内部 ABI,加载模块必须针对特定内核版本编译

第 3 章:内核启动流程(源码级)

3.1 启动全貌

                     完整 Linux 内核启动时序

   ┌─────────────┐
   │  BIOS/UEFI  │  POST、硬件枚举、引导设备选择
   └──────┬──────┘
          │ 跳转到 MBR/GPT 引导扇区
   ┌──────▼──────┐
   │    GRUB2    │  加载 /boot/vmlinuz,解析 grub.cfg
   └──────┬──────┘
          │ 执行内核镜像
   ┌──────▼──────────────────────┐
   │  arch/x86/boot/header.S    │  16 位实模式入口
   │  (实模式 setup 代码)         │  • 检测内存布局 (e820)
   └──────┬──────────────────────┘  • 设置视频模式
          │                         • 为 64 位模式做准备
   ┌──────▼──────────────────────┐
   │  arch/x86/boot/compressed/  │  解压 vmlinux
   │  head_64.S                  │  • 开启保护模式
   │                             │  • 建立临时页表
   └──────┬──────────────────────┘  • 跳入 64 位代码
          │
   ┌──────▼──────────────────────┐
   │  arch/x86/kernel/head_64.S  │  64 位内核入口
   │  startup_64                 │  • 建立内核页表
   │                             │  • 初始化 BSP
   └──────┬──────────────────────┘  • 调用 x86_64_start_kernel()
          │
   ┌──────▼──────────────────────┐
   │  init/main.c:start_kernel() │  ← 内核 C 代码真正入口
   │                             │  • 子系统按序初始化
   └──────┬──────────────────────┘
          │
   ┌──────▼──────────────────────┐
   │  rest_init()                │  创建 kernel_init (PID 1)
   │  init/main.c:714            │  创建 kthreadd (PID 2)
   └──────┬──────────────────────┘
          │
   ┌──────▼──────────────────────┐
   │  kernel_init()              │  挂载根文件系统
   │  init/main.c:1573           │  执行 /sbin/init(或 systemd)
   └─────────────────────────────┘

3.2 GRUB2 → bzImage 解压过程

GRUB2 将 bzImage(压缩的内核镜像)加载到内存后,首先执行的是 arch/x86/boot/header.S 中的 16 位实模式代码。这段代码负责:

  1. 内存探测:通过 INT 15h/E820 探测可用物理内存区域
  2. 视频模式设置:配置 VGA/VESA 模式
  3. CPU 能力检测:确认支持 64 位模式
  4. 解压准备:调用 arch/x86/boot/compressed/misc.c 中的 extract_kernel() 解压内核

解压后的 vmlinux ELF 文件被加载到高地址(CONFIG_PHYSICAL_START = 0x1000000,即 16MB 处),然后跳转到 arch/x86/kernel/head_64.S 中的 startup_64 标签。

3.3 entry_64.S 实模式→长模式转换

arch/x86/boot/compressed/head_64.S 完成从保护模式到 64 位长模式的转换,关键步骤:

/* 启用 PAE(物理地址扩展)*/
movl %cr4, %eax
orl  $X86_CR4_PAE, %eax
movl %eax, %cr4

/* 设置临时 4 级页表(恒等映射,VA == PA)*/
leal pgtable(%ebx), %eax
movl %eax, %cr3

/* 设置 EFER.LME(长模式使能位)*/
movl $MSR_EFER, %ecx
rdmsr
btsl $_EFER_LME, %eax
wrmsr

/* 启用分页,同时进入保护模式 */
movl $(X86_CR0_PG | X86_CR0_PE), %eax
movl %eax, %cr0

/* 远跳转,加载 64 位代码段,处理器切换到 64 位模式 */
lret

进入 64 位模式后,arch/x86/kernel/head_64.S 中的 startup_64 建立完整的内核页表(包括 KASLR 随机偏移),然后调用 x86_64_start_kernel(),最终通过 start_kernel() 进入 C 语言世界。

3.4 start_kernel() 完整初始化序列

start_kernel() 是真正的内核 C 入口,声明在 init/main.c:1007-1008

asmlinkage __visible __init __no_sanitize_address __noreturn __no_stack_protector
void start_kernel(void)

注意这些修饰符的含义:

  • asmlinkage:函数参数通过栈传递(而非寄存器),因为汇编代码直接调用它
  • __visible:防止 LTO 优化将其内联或重命名
  • __init:标记为初始化代码,初始化完成后释放(include/linux/init.h:45#define __init __section(".init.text")
  • __noreturn:告知编译器此函数永不返回

start_kernel() 的初始化序列(init/main.c:1009-1220,按实际顺序):

// 阶段 1:最早期设置(中断仍禁用)
set_task_stack_end_magic(&init_task);   // 在 init 任务栈末尾设置 magic 数,用于栈溢出检测
smp_setup_processor_id();              // 设置 Boot CPU 的 CPU ID
cgroup_init_early();                   // cgroup 早期初始化(必须在调度器前)
local_irq_disable();                   // 确保中断禁用
boot_cpu_init();                       // 将 Boot CPU 标记为在线/活跃
page_address_init();                   // 初始化高端内存地址映射

// 阶段 2:架构相关初始化
setup_arch(&command_line);             // x86: 解析 e820、初始化 NUMA 拓扑、ACPI

// 阶段 3:基础设施初始化
jump_label_init();                     // 静态分支(static key)初始化
static_call_init();                    // 静态调用优化初始化
early_security_init();                 // LSM 早期初始化

// 阶段 4:调度器初始化(中断使能前完成)
sched_init();                          // 初始化运行队列、调度类

// 阶段 5:RCU 初始化
rcu_init();                            // 初始化 Tree RCU
kvfree_rcu_init();

// 阶段 6:中断子系统
early_irq_init();                      // 分配 irq_desc 数组
init_IRQ();                            // 架构相关中断初始化
tick_init();                           // 时钟滴答初始化
timers_init();                         // 低精度定时器
hrtimers_init();                       // 高精度定时器
softirq_init();                        // 软中断向量表
timekeeping_init();                    // 时间管理

// 阶段 7:内存管理
mm_core_init();                        // 内存分配器核心(kmem_cache)
maple_tree_init();                     // Maple Tree(VMA 树实现)

// 阶段 8:使能中断(init/main.c:1138)
local_irq_enable();                    // 中断此时才启用!

// 阶段 9:完整初始化
vfs_caches_init();                     // VFS 对象缓存(dcache, icache)
signals_init();                        // 信号队列
proc_root_init();                      // /proc 文件系统
security_init();                       // LSM 完整初始化(SELinux 等)
cgroup_init();                         // cgroup v1/v2 完整初始化

// 阶段 10:最终阶段
arch_call_rest_init();                 // -> rest_init()

3.5 do_initcalls()__initcall 宏机制

__initcall 是内核提供的一种惰性初始化机制,允许子系统/驱动注册初始化函数,在启动序列的特定阶段自动执行。

宏定义在 include/linux/init.h:277

#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)

这个宏将函数指针放入名为 .initcall<id>.init 的 ELF 节中。链接脚本(arch/x86/kernel/vmlinux.lds.S)将这些节按顺序排列。

8 个优先级级别include/linux/init.h:152-161):

extern initcall_entry_t __initcall0_start[];   // pure_initcall
extern initcall_entry_t __initcall1_start[];   // core_initcall
extern initcall_entry_t __initcall2_start[];   // postcore_initcall
extern initcall_entry_t __initcall3_start[];   // arch_initcall
extern initcall_entry_t __initcall4_start[];   // subsys_initcall
extern initcall_entry_t __initcall5_start[];   // fs_initcall
extern initcall_entry_t __initcall6_start[];   // device_initcall(默认)
extern initcall_entry_t __initcall7_start[];   // late_initcall

典型用法:

// 驱动注册自己的初始化函数
static int __init my_driver_init(void) { ... }
module_init(my_driver_init);   // 展开为 device_initcall(my_driver_init)

这个机制的精妙之处在于:初始化代码与模块代码完全解耦。驱动只需声明"我需要在 device 阶段初始化",而无需修改任何中央注册表。

__init 标记的函数(include/linux/init.h:45-46):

#define __init  __section(".init.text") __cold __latent_entropy \
                __no_kstack_erase

这意味着 __init 函数放置在 .init.text 节,free_initmem() 调用后该节被释放,可节省数 MB 内存。

3.6 rest_init() → 第一个用户进程

init/main.c:714-760rest_init() 完成最后的收尾工作:

static noinline void __ref __noreturn rest_init(void)
{
    // 创建 PID 1:init 进程(kernel_init 函数)
    pid = user_mode_thread(kernel_init, NULL, CLONE_FS);
    // 将 init 绑定到 boot CPU(任务迁移还未就绪)

    // 创建 PID 2:kthreadd(内核线程守护进程)
    pid = kernel_thread(kthreadd, NULL, NULL, CLONE_FS | CLONE_FILES);

    system_state = SYSTEM_SCHEDULING;    // 调度器正式启动
    complete(&kthreadd_done);

    // Boot CPU 进入 idle 循环(永不返回)
    schedule_preempt_disabled();
    cpu_startup_entry(CPUHP_ONLINE);
}

kernel_init() 函数(init/main.c:1573)最终会执行 userspace 的 /sbin/init

static int __ref kernel_init(void *unused)        // init/main.c:1573
{
    wait_for_completion(&kthreadd_done);
    kernel_init_freeable();
    async_synchronize_full();

    system_state = SYSTEM_FREEING_INITMEM;
    free_initmem();              // 释放 .init.text 和 .init.data 节
    mark_readonly();             // 将内核只读段标记为只读
    pti_finalize();              // PTI(Meltdown 缓解)最终设置

    system_state = SYSTEM_RUNNING;

    // 尝试执行 init,按优先级(init/main.c:1638-1641):
    if (!try_to_run_init_process("/sbin/init") ||
        !try_to_run_init_process("/etc/init") ||
        !try_to_run_init_process("/bin/init") ||
        !try_to_run_init_process("/bin/sh"))
        return 0;

    panic("No working init found.");
}

第 4 章:系统调用机制(源码级)

4.1 系统调用全貌

系统调用是用户空间进入内核的唯一正规途径(除了异常/中断)。Linux x86_64 通过 SYSCALL 指令实现,这是 AMD 引入的快速系统调用指令。

  用户空间                           内核空间

  ┌──────────────────┐              ┌─────────────────────────────────┐
  │  应用程序         │              │  sys_write()                    │
  │  write(fd, ...)  │              │    ↑ 系统调用实现                │
  │  libc wrapper    │              │    │                            │
  │  (syscall 指令)  │──────────────▶│  do_syscall_64()               │
  │                  │   SYSCALL     │    │ 验证 nr,查 syscall 表      │
  │  ← 返回值        │◀──────────── │  entry_SYSCALL_64              │
  │  (SYSRET/IRET)  │   SYSRET/IRET│    │ 保存寄存器,切换栈          │
  └──────────────────┘              └─────────────────────────────────┘

4.2 entry_SYSCALL_64 汇编入口(arch/x86/entry/entry_64.S)

arch/x86/entry/entry_64.S:87 定义了 64 位系统调用入口:

SYM_CODE_START(entry_SYSCALL_64)    ; entry_64.S:87
    UNWIND_HINT_ENTRY
    ENDBR                           ; IBT CET 安全特性

    swapgs                          ; 交换 GS:用户 GS ↔ 内核 GS(per-CPU 区域)

    /* 保存用户栈指针,切换到内核栈 */
    movq %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)  ; 保存用户 RSP
    SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp           ; KPTI: 切换到内核页表
    movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp ; 加载内核栈顶

    /* 在内核栈上构造 struct pt_regs */        ; entry_64.S:101-107
    pushq $__USER_DS                           ; pt_regs->ss
    pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp2)   ; pt_regs->sp(用户 RSP)
    pushq %r11                                 ; pt_regs->flags(RFLAGS)
    pushq $__USER_CS                           ; pt_regs->cs
    pushq %rcx                                 ; pt_regs->ip(返回地址)
    pushq %rax                                 ; pt_regs->orig_ax(调用号)

    PUSH_AND_CLEAR_REGS rax=$-ENOSYS           ; 保存其余寄存器,初始化 rax=-ENOSYS

    /* 调用 C 层处理函数 */                    ; entry_64.S:112-121
    movq  %rsp, %rdi   ; 第一参数:struct pt_regs *regs
    movslq %eax, %rsi  ; 第二参数:syscall nr(符号扩展到 64 位)

    IBRS_ENTER                                 ; Spectre v2 缓解
    UNTRAIN_RET                                ; Spectre v2 缓解
    CLEAR_BRANCH_HISTORY                       ; BHI 缓解

    call  do_syscall_64    ; entry_64.S:121,返回时中断已禁用

系统调用返回时的决策(entry_64.S:129-170):

    /* 优先尝试 SYSRET(快路径),失败则用 IRET(慢路径)*/
    ALTERNATIVE "testb %al, %al; jz swapgs_restore_regs_and_return_to_usermode", \
        "jmp swapgs_restore_regs_and_return_to_usermode", X86_FEATURE_XENPV

syscall_return_via_sysret:             ; entry_64.S:137
    IBRS_EXIT
    POP_REGS pop_rdi=0
    SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi
    swapgs
    CLEAR_CPU_BUFFERS
    sysretq                            ; entry_64.S:166
SYM_CODE_END(entry_SYSCALL_64)        ; entry_64.S:170

4.3 do_syscall_64() C 层实现

arch/x86/entry/syscall_64.c:87-141

__visible noinstr bool do_syscall_64(struct pt_regs *regs, int nr)
{
    add_random_kstack_offset();            // KASLR 栈随机化,防御 JOP 攻击
    nr = syscall_enter_from_user_mode(regs, nr);  // seccomp 过滤在此触发

    instrumentation_begin();

    if (!do_syscall_x64(regs, nr) &&      // 尝试标准 x86_64 系统调用
        !do_syscall_x32(regs, nr) &&      // 尝试 x32 ABI 系统调用
        nr != -1) {
        regs->ax = __x64_sys_ni_syscall(regs);  // 无效调用 -> ENOSYS
    }

    instrumentation_end();
    syscall_exit_to_user_mode(regs);       // 处理信号、TIF 标志等

    // 决定用 SYSRET(快)还是 IRET(慢)
    if (cpu_feature_enabled(X86_FEATURE_XENPV)) return false;
    if (unlikely(regs->cx != regs->ip))    return false;
    if (unlikely(regs->ip >= TASK_SIZE_MAX)) return false;
    if (unlikely(regs->flags & (X86_EFLAGS_RF | X86_EFLAGS_TF))) return false;
    return true;   // true -> SYSRET,false -> IRET
}

系统调用分发通过 do_syscall_x64()syscall_64.c:53-66):

static __always_inline bool do_syscall_x64(struct pt_regs *regs, int nr)
{
    unsigned int unr = nr;
    if (likely(unr < NR_syscalls)) {
        unr = array_index_nospec(unr, NR_syscalls);  // Spectre 防护
        regs->ax = x64_sys_call(regs, unr);          // 调用具体实现
        return true;
    }
    return false;
}

x64_sys_call() 通过巨型 switch 语句实现(syscall_64.c:35-41):

long x64_sys_call(const struct pt_regs *regs, unsigned int nr)
{
    switch (nr) {
    #include <asm/syscalls_64.h>   // 展开为所有系统调用的 case 语句
    default: return __x64_sys_ni_syscall(regs);
    }
}

4.4 VDSO 加速机制

某些系统调用(clock_gettimegettimeofdaytime)调用极其频繁,但不需要实际进入内核。Linux 通过 vDSO(virtual Dynamic Shared Object) 机制解决这个问题。

工作原理:

  1. 内核在进程地址空间中映射一个特殊的共享库 vdso.so(约 4KB)
  2. 这个库包含特定系统调用的用户空间实现
  3. clock_gettime 调用直接读取内核维护的 vsyscall_gtod_data 共享页,无需进入内核
  进程地址空间:

  0x7fff_ffff_f000  ┌─────────────────────┐
                    │    vDSO 映射        │  ← 内核维护,只读
                    │  clock_gettime()    │    直接读 vvar 页,无 SYSCALL
                    │  gettimeofday()     │
  0x7fff_ffff_e000  ├─────────────────────┤
                    │    vvar 页          │  ← 内核写入时间数据
                    │  (共享时间数据)      │    seqlock 保护
                    └─────────────────────┘

vDSO 相关代码在 arch/x86/entry/vdso/ 目录,使用 seqlock 保证时间数据读取的一致性。

4.5 seccomp 过滤机制

seccomp(Secure Computing)是系统调用过滤器,允许进程声明自己允许使用哪些系统调用。Docker、Chrome 沙箱、systemd 服务隔离都依赖它。

seccomp 在 syscall_enter_from_user_mode() 中触发,位于 do_syscall_64() 的最早期阶段,确保在执行任何系统调用逻辑之前先完成过滤。

过滤器以 BPF 字节码形式安装,内核 verifier 确保过滤器是安全的(不含循环、不会访问越界内存):

  进程发出 SYSCALL
       │
  entry_SYSCALL_64 (entry_64.S:87)
       │
  do_syscall_64() (syscall_64.c:87)
       │
  syscall_enter_from_user_mode()
       │
  seccomp_run_filters()   ← BPF 过滤器在此执行
       │
  ┌────┴──────────────────────────┐
  │ SECCOMP_RET_ALLOW  │ 继续      │
  │ SECCOMP_RET_KILL   │ SIGKILL  │
  │ SECCOMP_RET_TRAP   │ SIGSYS   │
  │ SECCOMP_RET_ERRNO  │ 返回错误 │
  │ SECCOMP_RET_TRACE  │ ptrace   │
  │ SECCOMP_RET_USER_NOTIF │ 通知  │
  └───────────────────────────────┘

4.6 系统调用完整路径 ASCII 图

用户空间
══════════════════════════════════════════════════════════════════
   libc: write(fd, buf, len)
   ┌──────────────────────────────────────────┐
   │  mov rax, 1          ; SYS_write = 1     │
   │  mov rdi, fd                             │
   │  mov rsi, buf                            │
   │  mov rdx, len                            │
   │  syscall             ; 触发 SYSCALL 指令  │
   └──────────────────────────────────────────┘
══════════════════ SYSCALL 指令(硬件)══════════════════════════
内核空间
   ┌─ entry_SYSCALL_64 (entry_64.S:87) ──────────────────────┐
   │  swapgs / 切换内核栈 / 构造 pt_regs                      │
   │  call do_syscall_64                                      │
   └──────────────────────────────────────────────────────────┘
              │
   ┌─ do_syscall_64 (syscall_64.c:87) ───────────────────────┐
   │  add_random_kstack_offset()                              │
   │  syscall_enter_from_user_mode()                          │
   │    └── seccomp_run_filters()   ← 安全过滤                │
   │  x64_sys_call(regs, nr)                                  │
   │    └── switch(nr==1) → __x64_sys_write()                │
   │  syscall_exit_to_user_mode()   ← 处理信号等              │
   └──────────────────────────────────────────────────────────┘
              │
   ┌─ __x64_sys_write() → ksys_write() ─────────────────────┐
   │  vfs_write()                                             │
   │    └── file->f_op->write_iter()                         │
   │          └── ext4/pipe/socket 的具体实现                 │
   └──────────────────────────────────────────────────────────┘
══════════════════ SYSRET/IRET(硬件)══════════════════════════
用户空间
   rax = 返回值

第 5 章:核心数据结构体系

5.1 task_struct — 进程描述符

task_struct 是 Linux 内核中最重要、也最复杂的数据结构,定义在 include/linux/sched.h:820。它在 64 位系统上大约有 7KB 大小,包含了描述一个进程所需的一切信息。

struct task_struct {                    /* include/linux/sched.h:820 */
  ┌─────────────────────────────────────────────────────────────────┐
  │  [线程信息区域 - 第一个字段,必须在此位置]                         │
  │  struct thread_info  thread_info;  /* sched.h:826 */             │
  │                                     /* 包含 flags, preempt_count */│
  ├─────────────────────────────────────────────────────────────────┤
  │  [任务状态]                                                        │
  │  unsigned int   __state;           /* sched.h:828                 │
  │                                     * TASK_RUNNING/INTERRUPTIBLE */│
  │  unsigned int   saved_state;       /* sched.h:831 spinlock sleepers */
  ├─────────────────────────────────────────────────────────────────┤
  │  [随机化字段起始 randomized_struct_fields_start]  sched.h:837   │
  │  void          *stack;             /* sched.h:839 内核栈指针 */   │
  │  refcount_t     usage;             /* sched.h:840 引用计数 */     │
  │  unsigned int   flags;             /* sched.h:842 PF_* 标志 */    │
  │  unsigned int   ptrace;            /* sched.h:843 */              │
  ├─────────────────────────────────────────────────────────────────┤
  │  [CPU 调度相关字段]                                               │
  │  int            on_cpu;            /* sched.h:849 运行在哪个 CPU */│
  │  int            prio;              /* sched.h:866 动态优先级 */   │
  │  int            static_prio;       /* sched.h:867 nice 值转换 */  │
  │  unsigned int   rt_priority;       /* sched.h:869 实时优先级 1-99 */│
  │  struct sched_entity   se;         /* sched.h:871 CFS 调度实体 */ │
  │  struct sched_rt_entity rt;        /* sched.h:872 RT 调度实体 */  │
  │  struct sched_dl_entity dl;        /* sched.h:873 Deadline 实体 */│
  │  const struct sched_class *sched_class; /* sched.h:878 调度类 */ │
  ├─────────────────────────────────────────────────────────────────┤
  │  [进程关系]                                                        │
  │  struct task_struct    *real_parent; /* 真实父进程 */              │
  │  struct task_struct    *parent;     /* 当前父进程(ptrace 可不同)*/│
  │  struct list_head       children;   /* 子进程链表 */               │
  │  struct list_head       sibling;    /* 兄弟进程链表 */             │
  ├─────────────────────────────────────────────────────────────────┤
  │  [进程标识]                                                        │
  │  pid_t    pid;                      /* Process ID */               │
  │  pid_t    tgid;                     /* Thread Group ID */          │
  │  struct pid *thread_pid;            /* PID 命名空间感知对象 */      │
  ├─────────────────────────────────────────────────────────────────┤
  │  [内存管理]                                                        │
  │  struct mm_struct  *mm;             /* 用户地址空间(内核线程=NULL)*/│
  │  struct mm_struct  *active_mm;      /* 当前活跃地址空间 */          │
  ├─────────────────────────────────────────────────────────────────┤
  │  [文件系统]                                                        │
  │  struct fs_struct   *fs;            /* 工作目录、根目录 */          │
  │  struct files_struct *files;        /* 打开文件描述符表 */          │
  ├─────────────────────────────────────────────────────────────────┤
  │  [命名空间]                                                        │
  │  struct nsproxy     *nsproxy;       /* 指向所有命名空间 */          │
  ├─────────────────────────────────────────────────────────────────┤
  │  [信号处理]                                                        │
  │  struct signal_struct  *signal;     /* 共享信号处理(线程组)*/     │
  │  struct sighand_struct *sighand;    /* 信号处理函数表 */            │
  │  sigset_t               blocked;   /* 阻塞的信号集合 */            │
  ├─────────────────────────────────────────────────────────────────┤
  │  [权限/凭据]                                                       │
  │  const struct cred    *cred;        /* 当前凭据(UID/GID/cap)*/   │
  ├─────────────────────────────────────────────────────────────────┤
  │  [BPF/安全]                                                       │
  │  struct bpf_local_storage *bpf_storage; /* BPF 本地存储 */        │
  │  void                    *security;    /* LSM 安全 blob */         │
  ├─────────────────────────────────────────────────────────────────┤
  │  [时间统计]                                                        │
  │  u64    utime;   /* 用户态 CPU 时间(纳秒)*/                       │
  │  u64    stime;   /* 内核态 CPU 时间(纳秒)*/                       │
  └─────────────────────────────────────────────────────────────────┘
}

5.2 进程状态机

include/linux/sched.h:107-127 定义了进程状态:

#define TASK_RUNNING            0x00000000  /* 正在运行或在就绪队列中 */
#define TASK_INTERRUPTIBLE      0x00000001  /* 可中断睡眠(等待信号或事件)*/
#define TASK_UNINTERRUPTIBLE    0x00000002  /* 不可中断睡眠(等待 I/O)*/
#define __TASK_STOPPED          0x00000004  /* 被 SIGSTOP 停止 */
#define __TASK_TRACED           0x00000008  /* 被 ptrace 追踪 */
#define EXIT_DEAD               0x00000010
#define EXIT_ZOMBIE             0x00000020  /* 等待父进程 wait() */
#define TASK_DEAD               0x00000080  /* 僵尸等待最终收割 */
#define TASK_FREEZABLE          0x00002000  /* 可被冻结(hibernate)*/

状态转换图:

              fork()
                │
         ┌──────▼──────┐
         │  TASK_NEW   │  (TASK_NEW | TASK_UNINTERRUPTIBLE)
         └──────┬──────┘
                │ wake_up_new_task()
    ┌───────────▼────────────┐
    │    TASK_RUNNING        │◄────────────────────────┐
    │  (在就绪队列 runqueue)  │                          │
    └───────────┬────────────┘                          │
                │ schedule() 选中                        │ 事件发生/信号
         ┌──────▼──────┐       sleep/wait         ┌────┴─────────────────┐
         │  正在运行    │──────────────────────────▶│ TASK_INTERRUPTIBLE  │
         │ (on CPU)    │──────────────────────────▶│ TASK_UNINTERRUPTIBLE│
         └──────┬──────┘       I/O 等待            └─────────────────────┘
                │ exit()
         ┌──────▼──────┐
         │ EXIT_ZOMBIE │  等待父进程 wait4()
         └──────┬──────┘
                │
         ┌──────▼──────┐
         │  TASK_DEAD  │  最终释放 task_struct
         └─────────────┘

5.3 mm_structvm_area_struct

mm_structinclude/linux/mm_types.h:1123)描述进程的完整地址空间:

struct mm_struct {                           /* mm_types.h:1123 */
    struct {
        struct maple_tree mm_mt;             /* mm_types.h:1140 VMA 树 */
        unsigned long mmap_base;             /* mm_types.h:1142 mmap 区域基址 */
        unsigned long task_size;             /* mm_types.h:1149 用户空间大小 */
        pgd_t *pgd;                          /* mm_types.h:1150 页全局目录 */

        atomic_t mm_users;                   /* mm_types.h:1171 用户计数 */
        atomic_t mm_count;                   /* mm_types.h:1137 引用计数 */
        int map_count;                       /* mm_types.h:1179 VMA 数量 */
        spinlock_t page_table_lock;          /* mm_types.h:1181 页表自旋锁 */
        struct rw_semaphore mmap_lock;       /* mm_types.h:1196 VMA 读写锁 */

        unsigned long total_vm;              /* mm_types.h:1238 总虚拟页数 */
        unsigned long locked_vm;             /* mm_types.h:1239 mlock 的页数 */
        unsigned long data_vm;               /* mm_types.h:1241 数据段 */
        unsigned long exec_vm;               /* mm_types.h:1242 代码段 */

        unsigned long start_code, end_code;  /* 代码段范围 */
        unsigned long start_data, end_data;  /* 数据段范围 */
        unsigned long start_brk, brk;        /* 堆范围 */
        unsigned long start_stack;           /* 栈底 */
    };
};

vm_area_struct(VMA,include/linux/mm_types.h:913)描述一个连续的虚拟内存区域:

struct vm_area_struct {                      /* mm_types.h:913 */
    unsigned long vm_start;     /* mm_types.h:919 VMA 起始地址(含)*/
    unsigned long vm_end;       /* mm_types.h:920 VMA 结束地址(不含)*/

    struct mm_struct *vm_mm;    /* mm_types.h:929 所属地址空间 */
    pgprot_t vm_page_prot;     /* mm_types.h:930 访问权限(PTE 标志)*/
    vm_flags_t vm_flags;       /* mm_types.h:939 VM_READ/WRITE/EXEC/SHARED 等 */

    struct list_head anon_vma_chain; /* mm_types.h:966 RMAP 链 */
    struct anon_vma *anon_vma;  /* mm_types.h:968 匿名映射 anon_vma */

    const struct vm_operations_struct *vm_ops; /* mm_types.h:971 VMA 操作集 */

    unsigned long vm_pgoff;    /* mm_types.h:974 文件映射偏移(页单位)*/
    struct file *vm_file;      /* mm_types.h:976 文件映射(NULL=匿名)*/
    void *vm_private_data;     /* mm_types.h:977 私有数据 */
};

5.4 struct filefile_operations

struct fileinclude/linux/fs.h:1259)代表一个已打开的文件实例:

struct file {                               /* fs.h:1259 */
    spinlock_t            f_lock;           /* 保护 f_ep 链表 */
    fmode_t               f_mode;           /* 文件访问模式 FMODE_READ/WRITE */
    const struct file_operations *f_op;     /* 操作函数集 */
    struct address_space  *f_mapping;       /* 页缓存 */
    void                  *private_data;    /* 文件系统私有数据 */
    struct inode          *f_inode;         /* 对应的 inode */
    unsigned int          f_flags;          /* O_RDONLY/O_WRONLY/O_APPEND 等 */
    const struct cred     *f_cred;          /* 打开时的凭据 */
    union { const struct path f_path; };    /* 路径(dentry + vfsmount)*/
    loff_t                f_pos;            /* 文件偏移量(当前位置)*/
    file_ref_t            f_ref;            /* 引用计数 */
} __randomize_layout;                       /* 字段顺序随机化(防 exploit)*/

struct file_operationsinclude/linux/fs.h:1926)是文件系统与 VFS 的接口:

struct file_operations {                    /* fs.h:1926 */
    struct module *owner;
    loff_t (*llseek)     (struct file *, loff_t, int);
    ssize_t (*read)      (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write)     (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    ssize_t (*write_iter)(struct kiocb *, struct iov_iter *);
    int     (*mmap)      (struct file *, struct vm_area_struct *);
    int     (*open)      (struct inode *, struct file *);
    int     (*release)   (struct inode *, struct file *);
    int     (*fsync)     (struct file *, loff_t, loff_t, int datasync);
    long    (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);
    // ... 约 40 个函数指针
};

5.5 struct socketproto_ops

struct socketinclude/linux/net.h:116)是 BSD socket 的内核表示:

struct socket {                             /* net.h:116 */
    socket_state     state;                 /* SS_FREE/SS_UNCONNECTED/SS_CONNECTED */
    short            type;                  /* SOCK_STREAM/SOCK_DGRAM/SOCK_RAW */
    unsigned long    flags;                 /* SOCKWQ_ASYNC_NOSPACE 等 */
    struct file     *file;                  /* 反向指针 */
    struct sock     *sk;                    /* 协议无关的内部 socket */
    const struct proto_ops *ops;            /* 协议特定操作(net.h:125)*/
    struct socket_wq wq;                    /* 等待队列 */
};

proto_opsinclude/linux/net.h:160)包含了 TCP/UDP/Unix socket 等协议的操作函数集,是网络栈与 VFS 层的分界线:

struct proto_ops {                          /* net.h:160 */
    int family;
    int (*release)   (struct socket *sock);
    int (*bind)      (struct socket *sock, ...);
    int (*connect)   (struct socket *sock, ...);
    int (*accept)    (struct socket *sock, ...);
    int (*listen)    (struct socket *sock, int len);
    int (*sendmsg)   (struct socket *sock, struct msghdr *m, size_t total_len);
    int (*recvmsg)   (struct socket *sock, struct msghdr *m, size_t total_len, int flags);
    // ...
};

5.6 sk_buff — 网络数据包描述符

struct sk_buffinclude/linux/skbuff.h:885)是网络栈中最核心的数据结构,代表一个网络数据包:

struct sk_buff {                            /* skbuff.h:885 */
    /* 链表指针(发送队列、接收队列等)*/
    struct sk_buff  *next, *prev;           /* skbuff.h:889-890 */
    struct net_device *dev;                 /* skbuff.h:893 关联网络设备 */

    struct sock     *sk;                    /* skbuff.h:906 关联套接字 */
    ktime_t          tstamp;               /* skbuff.h:909 时间戳 */

    char cb[48] __aligned(8);              /* skbuff.h:918 控制缓冲区(各层私用)*/

    unsigned long  _skb_refdst;            /* skbuff.h:922 路由目标缓存 */
    void (*destructor)(struct sk_buff *);  /* skbuff.h:923 析构函数 */

    unsigned int len;                       /* skbuff.h:934 有效数据长度 */
    unsigned int data_len;                  /* skbuff.h:935 分片数据长度 */
    __u16 mac_len, hdr_len;                /* skbuff.h:936-937 */

    /* 标志位 */
    __u8 cloned:1;                          /* skbuff.h:955 */
    __u8 nohdr:1;                           /* skbuff.h:956 */
    __u8 head_frag:1;                       /* skbuff.h:959 */

    /* 协议头偏移量 */
    __be16 protocol;                        /* 网络层协议 ETH_P_IP 等 */
    sk_buff_data_t transport_header;        /* 传输层头(TCP/UDP)偏移 */
    sk_buff_data_t network_header;          /* 网络层头(IP)偏移 */
    sk_buff_data_t mac_header;             /* 链路层头(ETH)偏移 */

    /* 数据缓冲区指针 */
    unsigned char *head;                    /* 缓冲区开始 */
    unsigned char *data;                    /* 有效数据开始 */
    sk_buff_data_t tail;                    /* 有效数据结束 */
    sk_buff_data_t end;                     /* 缓冲区结束 */
    unsigned int   truesize;               /* skb 总大小 */
};

sk_buff 的头部操作设计非常精妙,各协议层通过 skb_push()skb_pull() 在同一块内存上添加/移除协议头,避免内存拷贝:

  内存布局:
  head                      tail    end
    │←── headroom ──▶│◄── data ──▶│◄── tailroom ──▶│
    └────────────────┴────────────┴─────────────────┘

  发包:每层向前 push 协议头(skb_push)
  └─▶ ETH 层: skb_push(sizeof(struct ethhdr))
      IP 层:  skb_push(sizeof(struct iphdr))
      TCP 层: skb_push(sizeof(struct tcphdr))

  收包:每层向后 pull 剥离协议头(skb_pull)
  └─▶ ETH 层: skb_pull(sizeof(struct ethhdr))
      IP 层:  skb_pull(sizeof(struct iphdr))
      TCP 层: skb_pull(sizeof(struct tcphdr))

5.7 核心数据结构关系图

   task_struct (sched.h:820)
   ┌────────────────────────────────────────────────────────────┐
   │  mm_struct *mm ──────────────────────────────────────────┼──▶ mm_struct (mm_types.h:1123)
   │  files_struct *files                                      │   ├── pgd_t *pgd ──▶ 页表
   │  ├── struct file *fd[0]  (stdin)                          │   ├── maple_tree mm_mt
   │  ├── struct file *fd[1]  (stdout)                         │   │    ├── VMA1 [0x400000-0x401000]
   │  └── struct file *fd[n]                                   │   │    │    vm_file ──▶ 可执行文件
   │       ├── f_inode ──▶ struct inode (fs.h:766)             │   │    ├── VMA2 [0x7f...-0x7f...]
   │       ├── f_op ──▶ file_operations (fs.h:1926)            │   │    │    vm_file = NULL (匿名)
   │       └── f_mapping ──▶ address_space (page cache)        │   │    └── VMA3 [栈]
   │  nsproxy *nsproxy                                         │
   │  ├── pid_ns                                               │
   │  ├── net_ns ──▶ struct net                                │
   │  │    └── socket ──▶ struct sock                          │
   │  ├── mnt_ns                                               │
   │  └── uts_ns / ipc_ns / time_ns                            │
   │  sched_entity se                                          │
   │  ├── vruntime                                             │
   │  └── rb_node ──▶ cfs_rq 红黑树                           │
   └────────────────────────────────────────────────────────────┘

第 6 章:内核并发模型

6.1 上下文分类

内核代码在不同的执行上下文中运行,不同上下文有不同的约束:

执行上下文层次(中断优先级从高到低):

  NMI(不可屏蔽中断)           ← 最高优先级,不能被任何东西中断
       │                         不能用任何锁(可能造成死锁)
  MCE(Machine Check Exception) ← 硬件错误处理
       │
  硬中断(hardirq)               ← 外部设备中断(网卡、磁盘)
       │                           不能睡眠,不能持有 mutex
       │                           可以被 NMI 打断
  软中断(softirq)               ← 延迟处理(NET_RX_SOFTIRQ 等)
       │                           不能睡眠,静态定义(32 个向量)
       │                           同一 CPU 上串行执行
  tasklet                         ← 基于 softirq,同 CPU 同一时刻只有一个
       │
  工作队列(workqueue)            ← 在内核线程中执行,可以睡眠
       │
  进程上下文(syscall/kthread)    ← 最低优先级,可以睡眠

关键约束

  • 硬中断和 NMI 只能使用原子操作和 spinlock_irq* 系列锁
  • 软中断可以被硬中断打断,但不能被其他软中断打断(同一 CPU)
  • 只有进程上下文可以调用 schedule()mutex_lock() 等可睡眠函数

6.2 锁层次:从简单到复杂

spinlock — 最基础的锁

spinlock_t lock;
spin_lock(&lock);
/* 临界区 - 不可睡眠 */
spin_unlock(&lock);

/* 在中断处理器中使用(同时禁用中断)*/
spin_lock_irqsave(&lock, flags);
spin_unlock_irqrestore(&lock, flags);

何时用 spinlock

  • 临界区极短(几十纳秒),不超过 1 微秒
  • 中断上下文(必须用 spinlock,不能用 mutex)
  • 多 CPU 竞争少的热点路径

spinlock 的实现在 SMP 上是真正的自旋等待,在 UP(单处理器)上退化为关中断。

mutex — 可睡眠的互斥锁

struct mutex my_mutex;
mutex_init(&my_mutex);

mutex_lock(&my_mutex);   /* 如果锁被持有,当前进程睡眠等待 */
/* 临界区 - 可以睡眠 */
mutex_unlock(&my_mutex);

何时用 mutex

  • 进程上下文(syscall、kthread)
  • 临界区可能需要较长时间(毫秒级)
  • 持有锁时需要调用可能睡眠的函数

rwlock / rw_semaphore — 读写分离

当数据以读为主、偶尔写时,使用读写锁可以提高并发度:

struct rw_semaphore sem;

/* 读者(并发访问)*/
down_read(&sem);
/* 读临界区 */
up_read(&sem);

/* 写者(独占访问)*/
down_write(&sem);
/* 写临界区 */
up_write(&sem);

内核中 mmap_lockmm_struct.mmap_lockmm_types.h:1196)就是典型的 rw_semaphore:大多数内存访问只需读锁,只有 mmap/munmap 等修改操作需要写锁。

6.3 RCU — 性能最高的并发机制

RCU(Read-Copy-Update)是 Linux 内核中最重要的并发原语之一,专为读多写少的数据结构设计(如路由表、进程列表、文件系统缓存)。

核心思想:

  1. 读者完全无锁rcu_read_lock() 只是禁止抢占(不是真正的锁)
  2. 写者拷贝修改:写者先复制数据副本,修改副本,然后原子替换指针
  3. 宽限期:等待所有读者离开临界区后,才释放旧数据
/* 读者路径(极低开销)*/
rcu_read_lock();                          /* 禁止抢占 */
p = rcu_dereference(global_ptr);          /* 原子读取指针 */
if (p)
    do_something_with(p);
rcu_read_unlock();                        /* 重新允许抢占 */

/* 写者路径 */
new_data = kmalloc(sizeof(*new_data), GFP_KERNEL);
/* 填充 new_data */
rcu_assign_pointer(global_ptr, new_data);
synchronize_rcu();                        /* 等待所有旧读者完成(可能几毫秒)*/
kfree(old_data);

include/linux/rcupdate.h:51-53 定义了核心 API:

void call_rcu(struct rcu_head *head, rcu_callback_t func);  /* 异步回调版 */
void synchronize_rcu(void);                                  /* 同步等待版 */

RCU 为什么快

  • rcu_read_lock() 在 !PREEMPT 内核中完全是空操作(0 指令!)
  • 不需要缓存行 ping-pong(spinlock 争抢会导致缓存行反复在 CPU 之间传输)
  • 特别适合网络路由查找(per-packet,极高频率)

6.4 per-CPU 变量

per-CPU 变量是另一个重要的无锁技术,每个 CPU 有自己独立的变量副本,访问时无需任何锁(include/linux/percpu-defs.h):

/* 定义 per-CPU 变量 */
DEFINE_PER_CPU(int, my_counter);

/* 访问当前 CPU 的副本 */
this_cpu_inc(my_counter);                  /* 原子递增本 CPU 副本 */
val = this_cpu_read(my_counter);           /* 读本 CPU 副本 */

per-CPU 变量的布局:

  __per_cpu_start ... __per_cpu_end(模板区域)
  ┌─────────────────────────────────────────────┐
  │ 变量模板区域(copy 基址)                     │
  └─────────────────────────────────────────────┘

  运行时 per-CPU 区域(每个 CPU 独立副本):
  ┌──────────┬──────────┬──────────┬──────────┐
  │  CPU 0   │  CPU 1   │  CPU 2   │  CPU 3   │
  │ ctr = 1  │ ctr = 5  │ ctr = 3  │ ctr = 2  │
  └──────────┴──────────┴──────────┴──────────┘
  (各 CPU 访问自己的副本,无需锁,无缓存伪共享)

6.5 内存屏障分类

include/asm-generic/barrier.h 定义了内核内存屏障:

mb();    /* 完全内存屏障:之前的读写必须在之后的读写之前完成 */
rmb();   /* 读内存屏障:之前的读必须在之后的读之前完成 */
wmb();   /* 写内存屏障:之前的写必须在之后的写之前完成 */
smp_mb();  /* SMP 内存屏障(UP 上退化为编译器屏障)*/
smp_rmb(); /* SMP 读屏障 */
smp_wmb(); /* SMP 写屏障 */

x86 是强内存模型(TSO),smp_wmb() 是空操作,smp_mb()LOCK 前缀指令。ARM 是弱内存模型,这些屏障都是真实的 dmb 指令。

6.6 锁选择决策树

需要保护共享数据?
         │
    ┌────▼────────────────────────────────────────────┐
    │ 能不能用 per-CPU 变量?(每个 CPU 独立副本)       │
    │ → 计数器、缓存、运行队列等                         │
    └────┬────────────────────────────────────────────┘
         │ 不能
    ┌────▼────────────────────────────────────────────┐
    │ 读多写少?(>90% 是读操作)                        │
    │ → 路由表、进程列表、文件系统缓存                   │
    └────┬────────────────────────────────────────────┘
    是 ↓ │ 否(读写均衡)
  使用 RCU │
         ┌────▼──────────────────────────────────────┐
         │ 需要在中断上下文中持有锁?                   │
         └────┬──────────────────────────────────────┘
         是 ↓ │ 否
     spinlock │ ┌────▼────────────────────────────────────┐
  (irqsave)  │ │ 临界区会睡眠(调用可阻塞函数)?          │
              │ └────┬────────────────────────────────────┘
              │ 是 ↓ │ 否
              │ mutex│ ┌────▼──────────────────────────────┐
              │ (可睡) │ │ 读写分离有意义?                  │
              │       │ └────┬──────────────────────────────┘
              │       是 ↓   │ 否
              │   rw_semaphore spinlock(短临界区)
              │               mutex(长临界区)

第 7 章:内存管理架构

7.1 物理内存:NUMA node → zone → page

Linux 的物理内存组织采用三层结构:

  NUMA 架构的内存组织:

  Node 0 (Socket 0)          Node 1 (Socket 1)
  ┌─────────────────────┐    ┌─────────────────────┐
  │ Zone: DMA (0-16MB)  │    │ Zone: DMA           │
  │ Zone: DMA32 (0-4GB) │    │ Zone: DMA32         │
  │ Zone: NORMAL        │    │ Zone: NORMAL        │
  │   ├── page 0        │    │   ├── page N        │
  │   ├── page 1        │    │   └── ...           │
  │   └── ...           │    └─────────────────────┘
  └─────────────────────┘
    本地访问:~80ns     远端访问:~150ns(NUMA 代价)

Zone 分类(x86_64):

  • ZONE_DMA(0-16MB):旧式 ISA DMA 设备需要的低地址内存
  • ZONE_DMA32(0-4GB):32 位 DMA 地址空间
  • ZONE_NORMAL(4GB 以上):普通内核可用内存,大多数分配来自此区

struct pageinclude/linux/mm_types.h:79)是物理页的描述符:

  • 每个 4KB 物理页对应一个 struct page(约 64 字节)
  • 64GB RAM = 16M 个页 = 约 1GB struct page 数组
  • 使用 union 节省空间(同一页在不同用途下有不同的字段含义)
  • 现代内核引入 struct foliomm_types.h:401)作为新的多页单位抽象

7.2 虚拟内存:五级页表

x86_64 现代内核使用 5 级页表(CONFIG_X86_5LEVEL),支持 57 位虚拟地址(128PB):

  x86_64 五级页表结构(5-level paging):

  虚拟地址 (57 位有效位):
  ┌──────┬────┬────┬────┬────┬────────────────┐
  │  符号 │PGD │P4D │PUD │PMD │ PTE │ 页内偏移  │
  │扩展   │ 9位│ 9位│ 9位│ 9位│ 9位 │  12位     │
  └──────┴────┴────┴────┴────┴─────┴───────────┘

  CR3 ──▶ PGD[pgd_idx]
             │
             ▼
          P4D[p4d_idx]     ← 5级才有,4级时 P4D=PGD
             │
             ▼
          PUD[pud_idx]
             │
             ▼
          PMD[pmd_idx]
             │
             ▼
          PTE[pte_idx]
             │
             ▼
          物理页 + 页内偏移 ──▶ 最终物理地址

内核地址空间布局(x86_64,5 级页表):

  0x0000_0000_0000_0000  ┌──────────────────────────────┐
                         │      用户空间(0 ~ 128TB)    │
  0x0000_7fff_ffff_ffff  ├──────────────────────────────┤
                         │  non-canonical hole(非法)   │
  0xffff_8000_0000_0000  ├──────────────────────────────┤
                         │  direct mapping(物理内存直映)│
                         │  (最大 64TB)                  │
  0xffff_c000_0000_0000  ├──────────────────────────────┤
                         │  vmalloc/ioremap 区域         │
  0xffff_e000_0000_0000  ├──────────────────────────────┤
                         │  vmemmap(struct page 数组)  │
  0xffff_f000_0000_0000  ├──────────────────────────────┤
                         │  内核代码/数据/BSS             │
  0xffff_ffff_ffff_ffff  └──────────────────────────────┘

7.3 分配器层次

  内存分配器层次(从高到低):

  用户 malloc()
       │ brk() / mmap() 系统调用
  ──────── 内核边界 ────────
       │
  vmalloc()    ─── 虚拟连续,物理非连续,适合大块分配(>1MB)
       │
  kmalloc()    ─── 通用内核内存分配
       │             < PAGE_SIZE: SLUB slab cache
       │             >= PAGE_SIZE: 直接调用页分配器
  SLUB 分配器  ─── 小对象缓存(task_struct, inode, sk_buff, ...)
       │             使用 per-CPU freelist,减少锁竞争
       │             最小分配单位:kmem_cache 中定义
  页分配器(buddy system)
       │             以 2^n 个页为单位(order 0-10)
       │             每个 zone 有独立的 free_area[MAX_ORDER+1]
  NUMA 感知分配(优先从本地节点分配)
       │
  物理内存页(struct page)

Buddy 系统设计

  Order 0: 每个 block = 1 页(4KB)
  Order 1: 每个 block = 2 页(8KB)
  Order 2: 每个 block = 4 页(16KB)
  ...
  Order 10: 每个 block = 1024 页(4MB)

  分配 8KB(order=1):
  → 从 order=1 free list 取一个 block
  → 若 order=1 为空,从 order=2 分裂:
     一个 4 页 block 分成两个 2 页 block,
     一个给用户,一个放入 order=1 free list

7.4 缺页异常处理链

当进程访问没有映射物理页的虚拟地址时,CPU 触发缺页异常(Page Fault)。处理路径:

  CPU 访问虚拟地址 → PTE P 位为 0
       │
  #PF 异常(中断向量 14)
       │
  arch/x86/mm/fault.c: exc_page_fault()
       │
  do_user_addr_fault()
       │
  查找 VMA(mas_find 在 maple tree 中)
       │
  mm/memory.c:handle_mm_fault()  (行 6589)
       │
  __handle_mm_fault()  (行 6355)
       │
  ┌────▼──────────────────────────────────────────────┐
  │ handle_pte_fault()                                │
  │   ├── 匿名映射(do_anonymous_page)                │
  │   │     alloc_zeroed_user_highpage()              │
  │   │     → buddy 分配新页,零填充                   │
  │   ├── 文件映射(do_read_fault)                    │
  │   │     vma->vm_ops->fault()                      │
  │   │     → 从 page cache 加载或触发磁盘 I/O         │
  │   ├── COW(do_cow_fault)                         │
  │   │     复制父进程页,设置可写 PTE                  │
  │   └── 已映射但被换出(do_swap_page)               │
  │           从 swap 设备异步读回                      │
  └────────────────────────────────────────────────────┘
       │
  更新 PTE,TLB 刷新(INVLPG)
       │
  返回用户空间,重新执行触发缺页的指令

7.5 页面回收:kswapd 与 LRU

当物理内存不足时,内核需要回收页面:

kswapd:后台内核线程(每个 NUMA 节点一个),在内存低于 watermark 时周期性回收页面。

三级 watermark

  物理内存用量:
  ─────────────────── high watermark  ← 正常状态
  ─────────────────── low watermark   ← kswapd 开始回收
  ─────────────────── min watermark   ← 直接回收(阻塞分配者)
  ─────────────────── 0

MGLRU(Multi-Gen LRU,Linux 6.1+)

  传统 LRU(两代):
  active_anon   ← 最近访问的匿名页
  inactive_anon ← 长时间未访问的匿名页 → 候选换出
  active_file   ← 最近访问的文件缓存
  inactive_file ← 长时间未访问的文件缓存 → 候选释放

  MGLRU(多代):
  更细粒度,通过 generation 数字追踪多个"代",
  更精确识别冷热页面,显著改善工作集大于 RAM 时的性能。

7.6 内存子系统完整架构图

  ┌─────────────────────────────────────────────────────────────┐
  │                         用户空间                              │
  │   malloc()   mmap()   brk()   madvise()   mprotect()         │
  └──────────────────────┬──────────────────────────────────────┘
                         │ 系统调用
  ┌──────────────────────▼──────────────────────────────────────┐
  │                    虚拟内存管理(mm/)                         │
  │  do_mmap()  │  do_brk()  │  mremap()  │  mlock()            │
  │       VMA 操作(vm_area_struct + maple tree)                │
  │       缺页处理(handle_mm_fault, mm/memory.c:6589)           │
  │       匿名映射  │  文件映射  │  COW  │  swap                  │
  └──────┬───────────────────────────────────┬──────────────────┘
         │ 物理页分配                          │ 文件内容读写
  ┌──────▼──────────────────┐  ┌─────────────▼─────────────────┐
  │     物理内存管理          │  │       页缓存(Page Cache)     │
  │  Buddy Allocator         │  │   address_space + folio       │
  │  SLUB/SLAB 缓存          │  │   writeback(pdflush/bdi)    │
  │  vmalloc                 │  │   readahead                   │
  │  per-CPU page cache      │  └─────────────┬─────────────────┘
  └──────┬──────────────────┘               │ 磁盘 I/O
  ┌──────▼──────────────────┐  ┌─────────────▼─────────────────┐
  │     NUMA 策略            │  │        块设备层               │
  │   bind/interleave/prefer │  │  bio → request queue → driver│
  └──────────────────────────┘  └────────────────────────────────┘

第 8 章:进程调度架构

8.1 调度类层次

Linux 调度器采用模块化调度类架构,不同调度策略实现为独立的类,通过函数指针接口与核心调度器交互。

kernel/sched/sched.h:2497 定义了 sched_class 接口(部分关键函数):

struct sched_class {                        /* sched.h:2497 */
    void (*enqueue_task)(struct rq *rq, struct task_struct *p, int flags);
    bool (*dequeue_task)(struct rq *rq, struct task_struct *p, int flags);
    void (*yield_task)  (struct rq *rq);
    void (*wakeup_preempt)(struct rq *rq, struct task_struct *p, int flags);
    int  (*balance)(struct rq *rq, struct task_struct *prev, struct rq_flags *rf);
    struct task_struct *(*pick_task)(struct rq *rq, struct rq_flags *rf);
    struct task_struct *(*pick_next_task)(struct rq *rq, ...);
    void (*put_prev_task)(struct rq *rq, struct task_struct *p, ...);
    int  (*select_task_rq)(struct task_struct *p, int task_cpu, int flags);
    void (*task_tick)(struct rq *rq, struct task_struct *p, int queued);
    // ...
};

调度类优先级(高到低):

  优先级   调度类              调度策略              典型进程
  ─────────────────────────────────────────────────────────────
  最高    stop_sched_class     SCHED_STOP          CPU 热插拔/迁移
          dl_sched_class       SCHED_DEADLINE      实时截止期(EDF 算法)
          rt_sched_class       SCHED_FIFO          实时 FIFO(优先级 1-99)
                               SCHED_RR            实时轮转
          fair_sched_class     SCHED_NORMAL        普通进程(CFS)
                               SCHED_BATCH         批处理作业
                               SCHED_IDLE          空闲优先级进程
  最低    idle_sched_class                         CPU idle 线程

  (可选)ext_sched_class      sched_ext           BPF 自定义调度器(6.11+)

8.2 CFS 红黑树与 vruntime

CFS(Completely Fair Scheduler,完全公平调度器)是 Linux 的默认调度器。

核心思想:用虚拟时间(vruntime)代替真实时间衡量公平性。每个进程维护 vruntime,值越小表示历史上获得的 CPU 时间越少,应该优先调度。

kernel/sched/sched.h:678struct cfs_rq)关键字段:

struct cfs_rq {                             /* sched.h:678 */
    struct load_weight  load;               /* 就绪队列总权重 */
    unsigned int        nr_queued;          /* 可运行进程数 */
    struct rb_root_cached tasks_timeline;   /* 红黑树(按 vruntime 排序)*/
    struct sched_entity *curr;              /* 当前运行的实体 */
    struct sched_entity *next;              /* 下一个将运行的实体 */
    struct sched_avg    avg;                /* 负载追踪(PELT)*/
    // ...
};

vruntime 计算:

  vruntime 更新公式:

  delta_vruntime = delta_exec * NICE_0_LOAD / weight

  其中:
  delta_exec    = 实际运行时间(纳秒)
  NICE_0_LOAD   = nice=0 时的权重(1024)
  weight        = 进程权重(nice=-20 时约 88761,nice=+19 时约 15)

  结论:
  高优先级(小 nice)进程 vruntime 增长慢 → 经常排在红黑树最左边
  低优先级(大 nice)进程 vruntime 增长快 → 很快被其他进程"追上"

调度决策(pick_next_task_fair()):

  ① 从 cfs_rq->tasks_timeline 红黑树取最左节点
     (vruntime 最小的进程 = 最"饥饿"的进程)
  ② 该进程运行一个调度周期
     (min_granularity = max(sysctl_sched_min_granularity, ...))
  ③ 时间到后检查是否被其他进程"超过"(wakeup_preempt 检查)
  ④ 若是,设置 TIF_NEED_RESCHED,在下一个调度点抢占

8.3 负载均衡

多 CPU 系统需要在 CPU 之间均衡负载:

  调度域层次(SMT → MC → DIE → NUMA):

  ┌──────────────────────────────────────────────────┐
  │               NUMA 域(跨 socket)                 │
  │  ┌──────────────────┐  ┌──────────────────────┐  │
  │  │   DIE 域(pkg0) │  │   DIE 域(pkg1)      │  │
  │  │  ┌──────┐ ┌────┐ │  │  ┌──────┐ ┌────────┐ │  │
  │  │  │MC 域 │ │MC域│ │  │  │MC 域 │ │MC 域   │ │  │
  │  │  │CPU0  │ │CPU2│ │  │  │CPU4  │ │CPU6    │ │  │
  │  │  │CPU1  │ │CPU3│ │  │  │CPU5  │ │CPU7    │ │  │
  │  │  └──────┘ └────┘ │  │  └──────┘ └────────┘ │  │
  │  └──────────────────┘  └──────────────────────┘  │
  └──────────────────────────────────────────────────┘

  均衡粒度(代价从低到高):
  SMT 兄弟(同一物理核)< 同 LLC < 跨 LLC < 跨 NUMA

Load Balance 触发时机

  • 时钟中断(scheduler_tick)触发定期均衡
  • CPU 变为 idle 时触发(idle_load_balance)
  • fork() 时选择最空闲的 CPU(select_task_rq

8.4 实时调度

SCHED_FIFOSCHED_RR 是硬实时调度策略:

  • SCHED_FIFO:严格按优先级(1-99,99 最高),同优先级 FIFO,无时间片限制
  • SCHED_RR:同 SCHED_FIFO 加时间片(默认 100ms),同优先级轮转

实时进程优先级高于所有 CFS 进程,意味着一个实时进程可以 100% 占用 CPU(除非主动放弃或 RT throttling 介入)。

8.5 sched_ext BPF 调度器(Linux 6.11+)

sched_ext 允许用 BPF 程序实现自定义调度策略(kernel/sched/sched.h:1825):

extern const struct sched_class ext_sched_class;  /* sched.h:1825 */

BPF 调度器通过 struct sched_ext_ops 注册回调:

struct sched_ext_ops {
    s32  (*select_cpu)(struct task_struct *p, s32 prev_cpu, u64 wake_flags);
    void (*enqueue)(struct task_struct *p, u64 enq_flags);
    void (*dequeue)(struct task_struct *p, u64 deq_flags);
    void (*dispatch)(s32 cpu, struct task_struct *prev);
    void (*running)(struct task_struct *p);
    void (*stopping)(struct task_struct *p, bool runnable);
};

Meta 已在生产环境使用 scx_layered 调度器取得显著性能提升。

8.6 调度时序图

  时钟中断(1ms,可配置 HZ=100/250/1000)
       │
  scheduler_tick() → task_tick_fair()
       │
  ┌────▼─────────────────────────────────────────────┐
  │ 更新当前进程 vruntime(按 delta_exec 和权重计算)  │
  │ 检查是否超过调度周期(sched_slice)               │
  └────┬─────────────────────────────────────────────┘
       │ 超过
  设置 TIF_NEED_RESCHED 标志
       │
  系统调用返回 / 中断返回 / 显式 schedule()
       │
  __schedule()
       │
  pick_next_task()   ← 从调度类链依次尝试,最终选出下一个进程
       │
  context_switch()
       ├── switch_mm()   切换地址空间(更换 CR3,刷新 TLB)
       └── switch_to()   保存/恢复 callee-saved 寄存器(RSP/RBP 等)
               │
               ▼
           新进程从上次 schedule() 返回处继续执行

第 9 章:VFS 与文件系统架构

9.1 VFS 四大对象

VFS(Virtual File System)通过四个核心对象抽象所有文件系统的差异:

  VFS 四大对象及关系:

  ┌──────────────┐     1:N     ┌──────────────┐
  │ super_block  │────────────▶│    inode     │
  │(文件系统实例)│             │(文件元数据)  │
  │ s_blocksize  │             │ i_mode/uid   │
  │ s_op         │             │ i_size       │
  └──────────────┘             │ i_op/i_fop   │
                               └──────┬───────┘
                                      │ 1:N
                               ┌──────▼───────┐
                               │   dentry     │
                               │(目录项缓存)  │
                               │ d_name/hash  │
                               │ d_parent     │
                               │ d_inode ─────┼──▶ inode
                               └──────────────┘
                                     │ 1:N
                               ┌─────▼────────┐
                               │    file      │
                               │(打开文件实例)│
                               │ f_op/f_pos   │
                               │ f_inode ─────┼──▶ inode
                               └──────────────┘

**inode(include/linux/fs.h:766)**关键字段:

struct inode {                              /* fs.h:766 */
    umode_t i_mode;                         /* 文件类型和权限 */
    kuid_t  i_uid;                          /* 所有者 UID */
    kgid_t  i_gid;                          /* 所有者 GID */
    const struct inode_operations *i_op;    /* inode 操作集 */
    struct super_block *i_sb;               /* 所属文件系统 */
    struct address_space *i_mapping;        /* 页缓存 */
    unsigned long i_ino;                    /* inode 编号 */
    loff_t  i_size;                         /* 文件大小(字节)*/
    time64_t i_atime_sec;                   /* 访问时间 */
    time64_t i_mtime_sec;                   /* 修改时间 */
    time64_t i_ctime_sec;                   /* 元数据变更时间 */
    blkcnt_t i_blocks;                      /* 占用的 512B 块数 */
    struct inode_state_flags i_state;       /* I_DIRTY/I_SYNC 等 */
    struct rw_semaphore i_rwsem;            /* inode 读写锁 */
    // ...
};

9.2 路径查找(namei)流程

open("/home/user/file.txt", O_RDONLY) 的路径查找过程:

  open("/home/user/file.txt")
       │
  getname()    将路径从用户空间拷贝到内核
       │
  do_filp_open() → path_openat()
       │
  link_path_walk()   ← 核心路径分解函数
       │
  ┌────▼────────────────────────────────────────────────┐
  │ 从 '/' 或当前目录开始,循环处理每个路径组件:         │
  │                                                     │
  │   "home" → walk_component()                         │
  │     → d_lookup(parent_dentry, "home")  查 dcache   │
  │     → 命中:直接使用缓存的 dentry                   │
  │     → 未命中:inode->i_op->lookup() 从磁盘/驱动加载  │
  │              创建新 dentry 加入缓存                  │
  │                                                     │
  │   "user" → 同上                                     │
  │                                                     │
  │   "file.txt" → walk_component()                     │
  │     → 找到 dentry → permission() 检查权限           │
  └─────────────────────────────────────────────────────┘
       │
  vfs_open() → do_dentry_open()
       │
  file->f_op->open()(如 ext4_file_open)
       │
  返回 struct file *(即 fd 对应的内核对象)

dcache 性能关键:使用哈希表实现 O(1) 查找,使用 RCU 保护允许无锁并发读取。

9.3 页缓存(Page Cache)

页缓存是内核中文件数据的缓冲区,所有文件读写都经过它(除非 O_DIRECT):

  文件读取路径:

  read(fd, buf, len)
  → vfs_read() → file->f_op->read_iter()
  → generic_file_read_iter()
  → find_get_page(mapping, page_index)
        命中 ─── 直接复制到用户缓冲区(0 次 I/O)
        未命中 ─ alloc_page() + page_cache_add_speculative()
               → readpage()(触发磁盘 I/O)
               → wait_on_page_locked()
               → 复制到用户缓冲区

  文件写入路径(缓冲写,默认行为):

  write(fd, buf, len)
  → generic_file_write_iter()
  → find_or_create_page()
  → copy_from_user()   将数据写入页缓存
  → mark_page_dirty()  标记脏页
  → 写回由 writeback 线程异步完成(最长 30s 后或显式 fsync)

9.4 挂载子树(Mount Namespace)

Linux 允许通过 mount namespace 给每个进程(或容器)提供独立的挂载视图:

  Host 的 VFS 挂载树:

  /(根文件系统 ext4)
  ├── /proc(procfs)
  ├── /sys(sysfs)
  ├── /dev(devtmpfs)
  ├── /tmp(tmpfs)
  └── /home(btrfs)

  容器的挂载命名空间(clone(CLONE_NEWNS)):
  /(overlayfs,基于 Docker 镜像层)
  ├── /proc(该容器的进程视图)
  ├── /sys
  ├── /dev
  └── /app(容器特定目录)

  两个视图共享同一个内核,但互不干扰。

9.5 VFS 层次 ASCII 图

  ┌─────────────────────────────────────────────────────┐
  │                    用户空间                           │
  │  open()  read()  write()  stat()  mount()           │
  └──────────────────────┬──────────────────────────────┘
                         │ 系统调用
  ┌──────────────────────▼──────────────────────────────┐
  │                  VFS 层                              │
  │  sys_open → do_filp_open → link_path_walk           │
  │  dcache(目录项缓存,RCU 保护,哈希表)                │
  │  inode cache(inode_hashtable)                      │
  └──────┬───────────────────────────────────┬──────────┘
         │ f_op / inode_ops                  │ address_space
  ┌──────▼──────────────────┐  ┌────────────▼──────────┐
  │    具体文件系统          │  │      页缓存            │
  │  ext4 / btrfs / xfs     │  │  (page cache)          │
  │  tmpfs / nfs / ceph     │  │  folio / struct page   │
  └──────┬──────────────────┘  └────────────┬──────────┘
         │                                  │
  ┌──────▼──────────────────┐  ┌────────────▼──────────┐
  │      块设备层            │  │    回写(writeback)   │
  │  bio / request queue    │  │  pdflush / bdi-default │
  │  I/O 调度器(mq-deadline│  └────────────────────────┘
  │  / kyber / bfq)        │
  └──────┬──────────────────┘
         │
  ┌──────▼──────────────────┐
  │   块设备驱动             │
  │  NVMe / SATA / virtio   │
  └─────────────────────────┘

第 10 章:网络子系统架构

10.1 收包路径

  ┌──────────────────────────────────────────────────────────────┐
  │  物理层:数据帧到达网卡(DMA 到 RX ring buffer)               │
  └─────────────────────────┬────────────────────────────────────┘
                            │ 硬件中断
  ┌─────────────────────────▼────────────────────────────────────┐
  │  网卡驱动(igb/ixgbe/mlx5 等)                                │
  │  硬中断处理:禁用中断,调度 NAPI poll                          │
  └─────────────────────────┬────────────────────────────────────┘
                            │
  ┌─────────────────────────▼────────────────────────────────────┐
  │  NET_RX_SOFTIRQ                                               │
  │  net_rx_action() → napi_poll() → driver->poll()              │
  │  批量收包(最多 budget=64 个包),减少中断开销                  │
  └─────────────────────────┬────────────────────────────────────┘
                            │
  ┌─────────────────────────▼────────────────────────────────────┐
  │  netif_receive_skb()                                          │
  │  ├── XDP(最早处理点,需驱动支持)                             │
  │  │   BPF 程序:XDP_PASS/XDP_DROP/XDP_TX/XDP_REDIRECT         │
  │  └── 协议分发(ptype_base 哈希表,按 ETH 类型)                │
  │       ├── ETH_P_IP  → ip_rcv()                               │
  │       ├── ETH_P_IPV6 → ipv6_rcv()                            │
  │       └── ETH_P_ARP → arp_rcv()                              │
  └─────────────────────────┬────────────────────────────────────┘
                            │
  ┌─────────────────────────▼────────────────────────────────────┐
  │  IP 层(net/ipv4/ip_input.c)                                 │
  │  ip_rcv()                                                    │
  │  → Netfilter NF_INET_PRE_ROUTING hook                        │
  │  → ip_rcv_finish()                                           │
  │  → ip_route_input()(路由查找,可能转发)                     │
  │  → Netfilter NF_INET_LOCAL_IN hook                           │
  │  → ip_local_deliver()                                        │
  └─────────────────────────┬────────────────────────────────────┘
                            │
  ┌─────────────────────────▼────────────────────────────────────┐
  │  传输层                                                        │
  │  tcp_v4_rcv()(TCP)  │  udp_rcv()(UDP)                     │
  │  TCP:三次握手处理、流量控制、重传、乱序重组                    │
  │  数据放入 sk->sk_receive_queue                                │
  └─────────────────────────┬────────────────────────────────────┘
                            │
  ┌─────────────────────────▼────────────────────────────────────┐
  │  Socket 层                                                    │
  │  sock_recvmsg() → 唤醒等待的用户进程                           │
  │  用户 recv()/read() → 从 sk_receive_queue 取数据              │
  └──────────────────────────────────────────────────────────────┘

10.2 发包路径

  用户 send()/write()
       │
  sock_sendmsg() → proto->sendmsg()(tcp_sendmsg)
       │ TCP 封装:添加 TCP 头、序号、校验和
  ip_queue_xmit() → ip_output()
       │ IP 封装:添加 IP 头
       ├── Netfilter NF_INET_LOCAL_OUT
       ├── 路由查找(fib_lookup,选出口接口)
       └── Netfilter NF_INET_POST_ROUTING
       │
  neighbor(ARP/NDP)地址解析
       │
  dev_queue_xmit()
       │ qdisc(Traffic Control 排队规则,如 fq、pfifo_fast)
  net/core/dev.c: __dev_queue_xmit()
       │
  网卡驱动 ndo_start_xmit()
       │ 写入 TX ring buffer descriptor
  DMA 到网卡硬件
       │
  物理发送(电/光信号)

10.3 Netfilter 五个 Hook 点

  数据包流经内核的 Netfilter hook 点:

  入站方向:
  NIC ──▶ [NF_INET_PRE_ROUTING] ──路由──▶
    ├── 本机目标 ──▶ [NF_INET_LOCAL_IN] ──▶ 传输层
    └── 转发    ──▶ [NF_INET_FORWARD]  ──▶ NF_INET_POST_ROUTING ──▶ NIC

  出站方向:
  本地进程 ──▶ [NF_INET_LOCAL_OUT] ──路由──▶ [NF_INET_POST_ROUTING] ──▶ NIC

  每个 hook 点上可注册多个回调(按优先级排序):
  - conntrack(连接跟踪)
  - NAT(地址转换)
  - iptables/nftables 规则
  - eBPF TC/XDP 程序

10.4 BPF 在网络路径中的插入点

  eBPF 程序类型及位置:

  NIC HW
    │
  XDP(BPF_PROG_TYPE_XDP)
    │  位置:驱动 poll,DMA 完成后立即处理
    │  无 sk_buff,最高性能(可处理 >10 Mpps/core)
    │  典型用途:DDoS 防护(DROP)、LB(REDIRECT)
    ▼
  TC(BPF_PROG_TYPE_SCHED_CLS)
    │  位置:ingress/egress qdisc
    │  有完整 sk_buff,可修改包内容(NAT、封装)
    ▼
  netif_receive_skb
    │
  Socket BPF(BPF_PROG_TYPE_SK_SKB)
    │  位置:socket 层,可重定向 socket 间流量
    ▼
  sock_recvmsg

10.5 完整网络路径 ASCII 图

  ┌──────────────────────────────────────────────────────────────────┐
  │ 收包路径                                                          │
  │ NIC HW ─DMA─▶ RX ring ─NAPI─▶ [XDP] ─▶ netif_receive_skb       │
  │ ─▶ [TC ingress] ─▶ ip_rcv ─▶ [NF PRE_ROUTING]                   │
  │ ─▶ route ─▶ [NF LOCAL_IN] ─▶ tcp_v4_rcv ─▶ recv_queue           │
  │ ─▶ recv()                                                        │
  └──────────────────────────────────────────────────────────────────┘

  ┌──────────────────────────────────────────────────────────────────┐
  │ 发包路径                                                          │
  │ send() ─▶ tcp_sendmsg ─▶ ip_queue_xmit                           │
  │ ─▶ [NF LOCAL_OUT] ─▶ fib_lookup ─▶ [NF POST_ROUTING]            │
  │ ─▶ [TC egress] ─▶ dev_queue_xmit ─▶ ndo_start_xmit             │
  │ ─▶ DMA ─▶ NIC HW                                                │
  └──────────────────────────────────────────────────────────────────┘

第 11 章:设备驱动模型

11.1 kobject/kset/ktype 层次

Linux 设备模型的基础是 kobject(内核对象),它为所有设备、驱动、总线提供统一的引用计数、生命周期管理和 sysfs 表示。

  kobject 层次:

  kset(kobject 集合,对应 sysfs 目录)
  ├── kobject A(对应 sysfs 子目录)
  │    ├── kref(引用计数)
  │    ├── *parent(父对象)
  │    ├── ktype(操作函数集:release, sysfs_ops)
  │    └── sysfs dirent
  ├── kobject B
  └── kobject C

  引用计数:
  kobject_get()  增加引用
  kobject_put()  减少引用,归零时调用 ktype->release()

11.2 bus/device/driver 三角绑定

  Bus(总线):PCI, USB, I2C, Platform, AMBA, ...
       │
       ├── Device:挂在总线上的设备实例
       │    - 设备注册:device_register() → device_add()
       │    - 总线遍历所有驱动,调用 match()
       │    - 匹配成功 → driver->probe(device)
       │
       └── Driver:声明支持哪些设备的驱动
            - 驱动注册:driver_register()
            - 总线遍历所有设备,调用 match()
            - 匹配成功 → driver->probe(device)

  match() 方式(以 PCI 为例):
  vendor:device ID 精确匹配 或 class 码匹配
  (pci_match_id() 遍历 pci_device_id 表)

11.3 probe/remove 生命周期

/* 典型的平台驱动注册 */
static int my_driver_probe(struct platform_device *pdev)
{
    struct my_dev *dev;

    /* devm_* 系列:设备管理资源,device 释放时自动回收 */
    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    dev->base = devm_platform_ioremap_resource(pdev, 0);
    dev->irq  = platform_get_irq(pdev, 0);

    devm_request_irq(&pdev->dev, dev->irq, my_irq_handler,
                     IRQF_SHARED, "my_dev", dev);

    platform_set_drvdata(pdev, dev);
    return 0;   /* 返回 0 表示绑定成功 */
}

static void my_driver_remove(struct platform_device *pdev)
{
    /* devm_* 资源由框架自动释放,通常无需手动释放 */
}

static struct platform_driver my_driver = {
    .probe  = my_driver_probe,
    .remove = my_driver_remove,
    .driver = { .name = "my_device", .of_match_table = my_dt_ids },
};
module_platform_driver(my_driver);   /* 展开 module_init/module_exit */

11.4 中断子系统

  ┌───────────────────────────────────────────────────────────┐
  │  用户可见 API                                              │
  │  request_irq() / request_threaded_irq() / free_irq()      │
  └──────────────────────────┬────────────────────────────────┘
                             │
  ┌──────────────────────────▼────────────────────────────────┐
  │  Generic IRQ Layer(kernel/irq/)                          │
  │  struct irq_desc[]  ← 每个 IRQ 号一个描述符               │
  │  irq_domain         ← 硬件 IRQ 号 ↔ Linux IRQ 号 映射     │
  │  irq_chip           ← 中断控制器操作                       │
  └──────────────────────────┬────────────────────────────────┘
                             │
  ┌──────────────────────────▼────────────────────────────────┐
  │  Platform 中断控制器驱动                                    │
  │  x86: LAPIC(本地)+ IOAPIC(I/O)                         │
  │  ARM: GIC v2/v3/v4(Generic Interrupt Controller)        │
  │  RISC-V: PLIC(Platform-Level Interrupt Controller)      │
  └────────────────────────────────────────────────────────────┘

中断线程化(推荐模式):

/* 硬中断处理(极短,只确认中断)*/
irqreturn_t my_handler_hard(int irq, void *data)
{
    ack_interrupt();
    return IRQ_WAKE_THREAD;   /* 唤醒线程处理函数 */
}

/* 线程化处理(可以睡眠)*/
irqreturn_t my_handler_thread(int irq, void *data)
{
    process_data();           /* 耗时处理在内核线程中完成 */
    return IRQ_HANDLED;
}

request_threaded_irq(irq, my_handler_hard, my_handler_thread, ...);

11.5 DMA 映射

驱动使用 DMA 时,需要处理 CPU 和设备的地址空间差异:

/* 一致性 DMA 映射(一次分配,CPU 和设备均可访问)*/
dma_addr_t dma_handle;
void *cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
/* cpu_addr: CPU 访问地址,dma_handle: 设备 DMA 地址 */

/* 流式 DMA 映射(已有缓冲区,临时映射)*/
dma_addr_t dma_addr = dma_map_single(dev, buf, len, DMA_TO_DEVICE);
/* DMA 传输 */
dma_unmap_single(dev, dma_addr, len, DMA_TO_DEVICE);

IOMMU 是 DMA 安全的保障:将设备 DMA 地址隔离于物理地址,防止恶意/bug 驱动 DMA 到任意物理内存。

11.6 设备模型 ASCII 图

  sysfs(/sys/)反映内核设备模型:

  /sys/
  ├── bus/
  │   ├── pci/
  │   │   ├── devices/ (符号链接 → /sys/devices/)
  │   │   └── drivers/ (注册的 PCI 驱动)
  │   ├── usb/
  │   └── platform/
  ├── devices/
  │   ├── pci0000:00/
  │   │   └── 0000:00:1f.2/   (PCIe SSD 控制器)
  │   │       ├── vendor, device, class (属性文件)
  │   │       ├── driver -> ../../bus/pci/drivers/nvme
  │   │       └── nvme0/ (子设备)
  │   └── platform/
  ├── class/
  │   ├── net/ → eth0, lo, wlan0
  │   ├── block/ → sda, nvme0n1
  │   └── input/ → mouse0, keyboard
  └── module/
      └── nvme/ → refcnt, parameters/

第 12 章:安全架构

12.1 Linux 安全模型:DAC → Capabilities → LSM

Linux 安全模型是分层的,依次执行:

  安全检查层次(按执行顺序):

  ① DAC(Discretionary Access Control)
     ─────────────────────────────────────────
     传统 UNIX 权限:user/group/other + rwx
     通过 inode.i_uid/i_gid/i_mode 检查
     最基础,粒度粗,任何 root 进程可绕过

  ② Capabilities(能力机制,Linux 2.2+)
     ─────────────────────────────────────────
     将 root 特权拆分为约 40 种独立能力:
       CAP_NET_ADMIN    修改网络配置、创建套接字
       CAP_SYS_ADMIN    系统管理(最广泛,避免滥用)
       CAP_DAC_OVERRIDE 绕过 DAC 权限检查
       CAP_KILL         向任意进程发送信号
       CAP_SYS_MODULE   加载/卸载内核模块
       CAP_NET_RAW      使用原始套接字(ping)
     允许以最小权限原则运行服务

  ③ LSM(Linux Security Modules,Linux 2.6+)
     ─────────────────────────────────────────
     DAC 和 Capabilities 通过后,执行 MAC:
     SELinux / AppArmor / Smack / BPF-LSM

12.2 LSM Hook 机制

LSM 框架通过在内核关键路径插入 hook 点,允许安全模块实施强制访问控制。

调用方式:所有 LSM hook 按注册顺序依次执行,任何模块拒绝即拒绝:

/* LSM hook 调用示例(内核内部,伪代码)*/
int security_inode_permission(struct inode *inode, int mask)
{
    /* 遍历所有注册的 LSM 模块的 inode_permission hook */
    hlist_for_each_entry(hp, &security_hook_heads.inode_permission, list) {
        rc = hp->hook.inode_permission(inode, mask);
        if (rc)
            return rc;   /* 任意模块拒绝即失败 */
    }
    return 0;   /* 所有模块允许才成功 */
}

主要 hook 点分布:

  文件操作:inode_create / mkdir / unlink / rename / permission
  进程管理:task_create / task_kill / task_setuid
  网络操作:socket_create / connect / sendmsg / recvmsg
  IPC:     msg_queue_msgsnd / shm_shmat
  系统能力:capable(CAP_* 检查的最后一关)

12.3 SELinux/AppArmor/BPF-LSM

SELinux(Security Enhanced Linux)

  • 基于类型强制(Type Enforcement)和角色访问控制(RBAC)
  • 每个对象(文件、进程、网络连接)都有安全标签(如 system_u:object_r:shadow_t:s0
  • 策略规则:allow httpd_t shadow_t : file read;(允许 httpd 读 shadow 文件)
  • Android 强制使用 SELinux,所有 app 有独立 domain

AppArmor

  • 基于路径的访问控制,比 SELinux 简单易用
  • 为每个程序定义 profile(可访问的路径和操作)
  • Ubuntu/Debian/SUSE 默认使用

BPF-LSM(Linux 5.7+)

  • 使用 BPF 程序实现自定义 LSM hook,无需重新编译内核
  • Google 用于 ChromeOS 和 Android 安全策略

12.4 命名空间隔离(8 种 Namespace)

Linux 通过 8 种命名空间实现轻量级进程隔离(容器的核心):

  命名空间       内核版本    隔离内容                  clone 标志
  ────────────────────────────────────────────────────────────────
  Mount (mnt)   2.4.19    文件系统挂载树               CLONE_NEWNS
  UTS           2.6.19    主机名、NIS 域名              CLONE_NEWUTS
  IPC           2.6.19    SysV IPC、POSIX mq           CLONE_NEWIPC
  PID           2.6.24    PID 编号空间(PID 1 重置)    CLONE_NEWPID
  Network (net) 2.6.24    网络栈(接口/路由/iptables)  CLONE_NEWNET
  User          3.8       UID/GID 映射                 CLONE_NEWUSER
  Cgroup        4.6       cgroup 根目录视图             CLONE_NEWCGROUP
  Time          5.6       单调时钟、启动时间偏移         CLONE_NEWTIME

命名空间通过 clone(CLONE_NEW*)unshare() 创建,与 cgroup 一起构成 Linux 容器技术(Docker/Kubernetes)的基础。

12.5 seccomp 过滤

seccomp 是容器和沙箱安全的最后一道防线,即使容器逃逸也无法调用危险系统调用:

  Docker 默认阻断的系统调用(部分):
  kexec_load   加载新内核(不允许容器替换内核)
  reboot       重启系统
  mount        挂载文件系统(默认拒绝,需 --privileged)
  ptrace       追踪其他进程
  personality  修改进程运行特性
  ...(共约 44 个高危系统调用被默认拒绝)

第 13 章:eBPF 可编程内核

13.1 eBPF 架构概述

eBPF(extended Berkeley Packet Filter)是 Linux 的可编程框架,允许在内核中安全运行用户提供的程序,无需修改内核源码或加载内核模块。

  eBPF 程序生命周期:

  用户空间                          内核空间
  ┌──────────────────┐
  │  BPF C 源码      │  Clang + llvm
  │  (CO-RE 风格)    │──────────────▶  BPF 字节码(ELF .o)
  └────────┬─────────┘
           │ bpf(BPF_PROG_LOAD) 系统调用
  ┌────────▼──────────────────────────────────────────────────┐
  │  BPF Verifier(kernel/bpf/verifier.c,约 30,000 行)      │
  │  ① CFG 分析:无无界循环,无不可达代码                        │
  │  ② 抽象解释:追踪寄存器类型和值范围                          │
  │  ③ 指针安全:所有内存访问在合法范围内                         │
  │  ④ 栈深度:不超过 512 字节                                  │
  │  ⑤ helper 调用参数类型检查                                  │
  └────────┬──────────────────────────────────────────────────┘
           │ 验证通过
  ┌────────▼──────────────────────────────────────────────────┐
  │  JIT 编译器                                                │
  │  BPF 字节码 → 原生机器码(x86_64/arm64/s390x...)           │
  │  性能接近内核原生 C 代码                                     │
  └────────┬──────────────────────────────────────────────────┘
           │ bpf_link_create() 挂载
  ┌────────▼──────────────────────────────────────────────────┐
  │  Attach 到 hook 点                                         │
  │  XDP / TC / kprobe / tracepoint / LSM / cgroup / socket   │
  └────────────────────────────────────────────────────────────┘

13.2 BPF 程序类型(20+ 种)

  分类            类型                              用途
  ─────────────────────────────────────────────────────────────────
  网络/数据平面   BPF_PROG_TYPE_XDP                最快速包处理(>10Mpps)
                 BPF_PROG_TYPE_SCHED_CLS           TC 分类器(ingress/egress)
                 BPF_PROG_TYPE_SK_SKB              socket 流量重定向
                 BPF_PROG_TYPE_SK_MSG              sendmsg 拦截
                 BPF_PROG_TYPE_LWT_IN/OUT/XMIT     轻量级隧道

  追踪/观测      BPF_PROG_TYPE_KPROBE              内核函数插桩(动态)
                 BPF_PROG_TYPE_TRACEPOINT          内核静态追踪点
                 BPF_PROG_TYPE_PERF_EVENT          perf 事件
                 BPF_PROG_TYPE_RAW_TRACEPOINT      原始追踪点(更低开销)
                 BPF_PROG_TYPE_FENTRY/FEXIT        函数入口/出口(ftrace-based)

  安全           BPF_PROG_TYPE_LSM                 LSM hook(MAC 策略)
                 (seccomp 使用 classic BPF)

  调度           BPF_PROG_TYPE_STRUCT_OPS          sched_ext 自定义调度器

  cgroup         BPF_PROG_TYPE_CGROUP_SKB          cgroup 级别包过滤
                 BPF_PROG_TYPE_CGROUP_SOCK_ADDR    cgroup socket 地址劫持
                 BPF_PROG_TYPE_CGROUP_DEVICE       cgroup 设备访问控制

13.3 Verifier 安全保证

BPF Verifier 位于 kernel/bpf/verifier.c,约 30,000 行代码,是 eBPF 安全性的核心。

验证算法(抽象解释)

  1. CFG 构建:分析所有指令,建立控制流图,确认无无界循环

  2. 寄存器类型追踪

    r0: 初始值 0(整数)
    r1: ctx 指针(类型:PTR_TO_CTX,只读)
    r2-r5: 函数参数
    ...
    每条指令执行后,更新所有寄存器的类型状态
    若类型不安全(如把整数当指针解引用),拒绝
    
  3. 值范围追踪:对整数寄存器追踪可能的值范围(min/max),确保数组访问不越界

  4. 安全限制

    • 栈 512 字节
    • 程序指令数复杂度限制(防止验证器超时)
    • 循环必须有有界迭代(5.3+ 通过 bpf_loop() 支持有限循环)

13.4 JIT 编译

  BPF 指令集(精简 RISC-like ISA):
  - 11 个 64 位寄存器(r0-r10)
  - r10 = 只读帧指针
  - r0 = 函数返回值
  - 64/32 位算术、逻辑、移位操作
  - 条件跳转(相比,向前/向后跳,但不能形成循环)
  - 内存 load/store(严格边界检查)
  - 调用 helper 函数(约 300 个)

  x86_64 JIT 示例(BPF add r0, r1 → x86 addq %r9, %rax):
  BPF 字节码(8 字节/指令)被 JIT 为 1-6 字节 x86 指令
  JIT 后性能:~1-3ns/指令(接近 C 代码)
  解释器执行:~100ns/指令(已废弃,CONFIG_BPF_JIT_ALWAYS_ON)

13.5 Map 数据结构

BPF Map 是 BPF 程序与用户空间之间交换数据的桥梁:

  Map 类型                      特点                    典型用途
  ───────────────────────────────────────────────────────────────
  BPF_MAP_TYPE_HASH             哈希表                  连接跟踪、IP 黑名单
  BPF_MAP_TYPE_ARRAY            数组(下标 → 值)        统计计数器(固定大小)
  BPF_MAP_TYPE_LRU_HASH         LRU 哈希(自动淘汰)     流量统计
  BPF_MAP_TYPE_PERCPU_HASH      per-CPU 哈希(无锁)     高性能无竞争计数
  BPF_MAP_TYPE_LPM_TRIE         最长前缀匹配树           路由/ACL/CIDR 匹配
  BPF_MAP_TYPE_RINGBUF          无锁环形缓冲区(新)      事件上报(替代 perf_event)
  BPF_MAP_TYPE_PROG_ARRAY       程序数组(尾调用)        可扩展包处理管道
  BPF_MAP_TYPE_SOCKMAP          Socket 映射              socket 重定向(TCP 代理)
  BPF_MAP_TYPE_STRUCT_OPS       结构体操作集              sched_ext 调度器注册
  BPF_MAP_TYPE_TASK_STORAGE     任务本地存储              per-task BPF 数据

13.6 BTF/CO-RE 可移植性

**BTF(BPF Type Format)**是 eBPF 可移植性的基础:

  传统 BPF(无 CO-RE):
  为每个目标内核版本单独编译 → 二进制不可通用

  CO-RE(Compile Once, Run Everywhere):
  1. 编译时:Clang 在 .BTF 节记录所有访问的字段信息
  2. 加载时:libbpf 对比目标内核的 BTF,自动重定位字段偏移
  3. 结果:一个 BPF 二进制可在 4.x ~ 6.x 等内核上运行

  获取内核 BTF:
  $ ls /sys/kernel/btf/vmlinux   # 存在则支持 CO-RE
  $ bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

13.7 eBPF 在内核各子系统中的应用图

  ┌──────────────────────────────────────────────────────────────┐
  │  网络(最成熟)                                               │
  │  XDP ──▶ 40Gbps DDoS 防护(Cloudflare)                      │
  │  TC BPF ──▶ Cilium CNI(Kubernetes 网络 + NetworkPolicy)     │
  │  SK_SKB ──▶ Facebook Katran(四层负载均衡)                   │
  ├──────────────────────────────────────────────────────────────┤
  │  可观测性(增长最快)                                          │
  │  kprobe/fentry ──▶ BCC/bpftrace 任意内核函数追踪             │
  │  tracepoint ──▶ 低开销系统调用/内核事件统计                   │
  │  perf_event ──▶ Parca/Pyroscope 持续性能分析(CPU profiling)│
  ├──────────────────────────────────────────────────────────────┤
  │  安全                                                          │
  │  LSM BPF ──▶ Google Sandbox2 / Falco(入侵检测)             │
  │  seccomp ──▶ Docker/Kubernetes 系统调用白名单                 │
  ├──────────────────────────────────────────────────────────────┤
  │  调度(最新)                                                  │
  │  sched_ext ──▶ Meta scx_layered(生产调度器,2024)           │
  │  sched_ext ──▶ scx_lavd(游戏低延迟调度)                     │
  └──────────────────────────────────────────────────────────────┘

第 14 章:性能关键设计

14.1 零拷贝(Zero-Copy)

传统文件发送路径(read + write)需要 4 次数据拷贝:

  传统路径(4 次拷贝,2 次系统调用):

  磁盘 ──DMA──▶ 内核页缓存
  内核页缓存 ──CPU copy──▶ 用户缓冲区
  用户缓冲区 ──CPU copy──▶ socket 发送缓冲区(内核)
  socket 缓冲区 ──DMA──▶ NIC

  CPU 参与了 2 次拷贝,且需要切换到用户空间。

sendfile()(2 次拷贝,1 次系统调用):

sendfile(out_fd, in_fd, &offset, count);
/* 数据路径:
   磁盘 ──DMA──▶ 内核页缓存
   内核页缓存 ──DMA scatter-gather──▶ NIC(CPU 无需参与!)
   注:需要 NIC 支持 scatter-gather DMA */

splice()(管道零拷贝):

/* 文件 → pipe */
splice(in_fd, NULL, pipe_fds[1], NULL, len, SPLICE_F_MOVE);
/* pipe → socket */
splice(pipe_fds[0], NULL, sock_fd, NULL, len, SPLICE_F_MOVE);
/* 数据始终在内核页缓存中,用户空间只有 file descriptor 操作 */

io_uring(Linux 5.1+,最先进的异步 I/O):

  io_uring 架构(用户空间与内核共享环形缓冲区):

  用户空间                      内核空间
  ┌──────────────────┐         ┌──────────────────────┐
  │  提交队列(SQ)   │──mmap──▶│  SQ(共享内存,无需复制)│
  │  完成队列(CQ)   │◀─mmap──│  CQ(共享内存)        │
  └──────────────────┘         │                      │
                               │  io_uring worker     │
  ① 用户批量填写 SQE            │  异步处理 I/O 请求   │
  ② io_uring_submit()          │  完成后写入 CQE       │
  ③ 内核异步处理                │                      │
  ④ 用户轮询/等待 CQE           │                      │
                               └──────────────────────┘

  SQPOLL 模式:内核线程轮询 SQ,用户无需任何系统调用!
  固定缓冲区(IORING_OP_READ_FIXED):注册缓冲区,减少地址翻译

14.2 RCU 对性能的贡献

RCU 是 Linux 内核中性能影响最大的并发原语,在以下场景有决定性作用:

  RCU 性能优势(典型测量):

  IPv4 路由查找(每个数据包需要一次):
    spinlock 保护:    ~200ns/查找(高并发时更差)
    RCU 保护:         ~10-20ns/查找(几乎无开销)

  进程列表遍历(ps 命令需要锁定进程列表):
    spinlock 方案:    ~1ms(其他操作被阻塞)
    RCU 方案:         ~100µs(并发读,互不阻塞)

  内核中主要 RCU 保护的热点数据结构:
  - IPv4/IPv6 路由表(fib_table,每数据包访问)
  - 进程列表(task_list,ps/top 频繁读取)
  - VFS dcache(路径查找,每次文件操作)
  - 网络设备列表(netdev_chain,数据包收发)
  - SELinux AVC 缓存(每次 LSM hook)
  - 内核模块列表(每次符号查找)

14.3 NUMA 感知分配

在 NUMA 架构上(多 CPU socket),访问本地内存比远端内存快 2-5 倍。内核通过以下机制优化:

/* 首选本地节点分配(默认行为)*/
page = alloc_pages_node(numa_node_id(), GFP_KERNEL, 0);

/* per-CPU 页分配缓存(page allocator 内置,避免跨节点)*/
/* 每个 CPU 维护 per_cpu_pages 结构,批量预取本地节点页 */

/* NUMA 调度均衡(scheduler 将进程迁移到数据所在节点)*/
/* task_numa_fault() 检测频繁跨节点访问,触发迁移 */

NUMA 性能陷阱

  线程固定在 Node 1,但分配的内存在 Node 0:
  每次访问 ≈ 150ns(vs 本地 ≈ 80ns),高负载时 CPU 停顿明显。

  解决方案:
  ① 手动绑定:numactl --membind=1 --cpunodebind=1 ./app
  ② 内核自动:NUMA balancing(/proc/sys/kernel/numa_balancing=1)
              定期扫描进程访问模式,将热页迁移到访问节点

14.4 per-CPU 数据避免缓存伪共享

伪共享(False Sharing)问题

  伪共享示例:

  struct stats {
      atomic_t cpu0_counter;   /* 偏移 0 */
      atomic_t cpu1_counter;   /* 偏移 4 */
  };
  /* cpu0_counter 和 cpu1_counter 在同一个 64 字节缓存行! */
  /* CPU0 修改 cpu0_counter 导致 CPU1 的缓存失效,反之亦然 */
  /* 性能损失:缓存行在 CPU 之间反复传输(~100ns/次)*/

  修正方案 1:缓存行对齐
  struct stats {
      atomic_t cpu0_counter __cacheline_aligned;
      atomic_t cpu1_counter __cacheline_aligned;
  };

  修正方案 2(最彻底):per-CPU 变量
  DEFINE_PER_CPU(int, my_counter);
  /* 每个 CPU 有自己独立的副本,天然无伪共享 */
  /* 无需原子操作(单 CPU 访问),性能极高 */

14.5 lockless 数据结构

现代内核大量使用无锁数据结构替换传统锁保护的结构:

XArray(eXtensible Array,替代 radix_tree)

  • 使用 per-node spinlock(比全局 spinlock 细粒度)
  • 通过标记(marks)实现高效的"脏页"查找
  • 用于页缓存(address_space)、proc_fs、BPF map

Maple Tree(VMA 管理,Linux 6.1+)

  • 替代 VMA 红黑树(mm_rb)+ 双向链表组合
  • B-tree 变体,专为范围查询优化(VMA 是地址范围集合)
  • 内置 RCU 读者无锁支持(MA_STATE + mas_find()
  • 减少了 VMA 操作(mmap/munmap/brk)的锁持有时间
/* Maple Tree 范围查找(内核内部)*/
MA_STATE(mas, &mm->mm_mt, addr, addr);
vma = mas_find(&mas, ULONG_MAX);   /* 查找包含 addr 的 VMA,O(log n) */

llist(lock-less list)

  • 使用 cmpxchg 原子操作的无锁单链表(只支持在头部插入)
  • 用于 per-CPU 延迟释放队列(kfree_rcu 批量释放)
  • 中断延迟工作、网络 NAPI 完成队列

第 15 章:内核扩展性机制对比

15.1 四种主要扩展机制

机制 适用场景 限制 安全性 典型示例
内核模块(.ko) 驱动、文件系统、网络协议 可导致 Oops/Panic;ABI 不稳定 需要 CAP_SYS_MODULE;可访问全部内核 API ext4.ko, nvidia.ko
eBPF 观测、网络、调度、安全策略 无界循环限制;512B 栈;100万指令 BPF verifier 严格验证;不能崩溃内核 cilium、XDP 程序、bpftrace
sched_ext 自定义调度策略替换 仅调度逻辑;通过 BPF 实现 BPF verifier 保护;调度错误降级到 CFS scx_simple, scx_lavd
Rust 驱动 内存安全的设备驱动 API 绑定不完整;部分内核宏不可用 Rust 类型系统防止 UAF/双重释放 Nova(NVIDIA GPU 驱动原型)

15.2 内核模块详解

内核模块是最传统的扩展方式,允许运行时加载/卸载代码:

  模块生命周期:

  insmod module.ko
       │
  内核 ELF 加载(module_layout 验证)
  符号解析(对 EXPORT_SYMBOL 符号的引用)
  版本魔数检查(vermagic:内核版本字符串)
       │
  module_init() 回调(= __initcall_6)
       │
  模块运行
       │
  rmmod
       │
  module_exit() 回调
  等待所有使用者引用计数归零
  释放模块内存

内核模块的安全风险

  • 加载模块需要 CAP_SYS_MODULE 权限
  • 模块 bug 可导致内核 Oops、内存损坏乃至 panic(无隔离)
  • 内核模块 ABI 不稳定:不同内核版本的 .ko 不兼容(vermagic 检查)
  • CONFIG_MODULE_SIG + CONFIG_MODULE_SIG_FORCE 要求模块必须有信任的签名

15.3 eBPF 与内核模块的边界

  eBPF 允许:
  ✓ 读取内核数据结构(通过 BTF 安全访问,CO-RE 可移植)
  ✓ 调用约 300 个预定义 helper 函数
  ✓ 通过 Map 与用户空间高效交换数据
  ✓ 调用受信任的内核函数(kfunc,需声明 BTF_SET_GPL)
  ✓ 程序链(tail call,最多 33 次)

  eBPF 不允许:
  ✗ 无界循环(5.3+ 有限循环,需 verifier 可证明有界)
  ✗ 超过 512 字节栈帧
  ✗ 超过 100 万条指令复杂度(防止 verifier 超时)
  ✗ 睡眠/阻塞(大多数程序类型)
  ✗ 任意内存写入(只能写 BPF stack、map value、ctx 中允许的字段)

  内核模块允许(eBPF 不允许的):
  ✓ 访问任意内核符号和内存
  ✓ 实现新的系统调用、文件系统、网络协议
  ✓ 使用内核所有 API(包括睡眠、内存分配、信号等)
  ✓ 无复杂度限制

15.4 Rust 内核驱动

Linux 6.1 合入 Rust 支持,Rust 内核驱动的核心优势:

  Rust 类型系统在内核开发中防止的错误类型:

  内存安全(编译期保证):
  ✓ use-after-free:Rust 所有权系统,生命周期约束
  ✓ double-free:同上
  ✓ buffer overflow:切片边界检查
  ✓ null pointer dereference:Option<T>,解引用前必须处理 None

  并发安全(Send/Sync trait 系统):
  ✓ data race:Send/Sync trait 约束 + 内核锁封装(Guard 模式)
  ✓ 锁定序违规:可在类型系统中编码锁的顺序约束

  当前状态(2024 年):
  - 基本框架合入(rust/ 目录)
  - 部分驱动子系统有绑定(GPIO、platform bus、PCI 部分)
  - Nova GPU 驱动(NVIDIA 开源,Rust 实现)进行中
  - 内核社区内部对 Rust 整合速度有争议

15.5 扩展机制选择指南

  需求场景                            推荐方案
  ────────────────────────────────────────────────────────────────
  观测内核行为(性能分析/调试)          eBPF fentry/kprobe/tracepoint
  高性能网络包处理(>1Mpps)            eBPF XDP
  Kubernetes 网络策略                  eBPF TC(Cilium)
  自定义调度策略(非侵入)              sched_ext BPF
  Linux 安全策略(无重编译)            eBPF LSM
  容器/沙箱系统调用过滤                 seccomp BPF
  添加设备驱动(一次性/快速原型)        内核模块(C)
  添加设备驱动(新项目,内存安全要求)   Rust 内核驱动(如有绑定)
  实现新文件系统(用户空间可维护)       FUSE(用户态文件系统)
  实现新文件系统(高性能,内核态)       内核模块(C)
  实现新网络协议                        内核模块(C)+ eBPF hook 点

附录 A:内核子系统依赖关系

  Linux 内核子系统依赖关系(简化):

  ┌──────────────────────────────────────────────────────────────┐
  │                    用户空间接口层                               │
  │         系统调用   /proc   /sys   netlink   ioctl              │
  └───────────────────────────┬──────────────────────────────────┘
                              │
  ┌───────┬──────────────────┬┴─────────────────────┬───────────┐
  │  VFS  │   网络栈          │    安全(LSM)         │   eBPF   │
  │(fs/)  │   (net/)         │    (security/)        │  (kernel/│
  │       │   TCP/IP/UDP     │    SELinux/AppArmor   │   bpf/)  │
  └───┬───┘        │         └──────────┬────────────┘     │    │
      │    ┌───────┴──────┐             │                  │    │
  ┌───┴────┤ Netfilter     │             │                  │    │
  │ 文件系  │ XDP/TC hook  │             │                  │    │
  │ ext4   └───────────────┘             │                  │    │
  │ btrfs  网络驱动层                    │                  │    │
  │ xfs                                 │                  │    │
  └───┬────────────────────────────────────────────────────┘    │
      │                                                          │
  ┌───┴──────────────────────────────────────────────────────┐  │
  │             进程管理(kernel/)                             │  │
  │   fork/exec   signal   调度器(CFS/RT/DL/sched_ext)     │◄─┘
  │   命名空间    cgroup   workqueue   RCU                    │
  └──────────────────────────┬──────────────────────────────┘
                             │
  ┌──────────────────────────┴──────────────────────────────┐
  │                  内存管理(mm/)                           │
  │   buddy(页分配)   SLUB(对象缓存)   vmalloc             │
  │   mmap/VMA(maple tree)   page cache   swap/OOM         │
  └──────────────────────────┬──────────────────────────────┘
                             │
  ┌──────────────────────────┴──────────────────────────────┐
  │                 架构支持(arch/)                          │
  │   x86: 页表/APIC/ACPI/系统调用/KASLR/PTI                 │
  │   arm64: 同类功能(ASID/GIC/PSCI)                       │
  └───────────────────────────────────────────────────────────┘

附录 B:关键源码文件速查

文件路径 核心内容 关键行号
init/main.c 内核 C 入口 start_kernel(),完整初始化序列 1007-1220
init/main.c rest_init(),创建 PID 1(kernel_init)和 PID 2(kthreadd) 714-760
init/main.c kernel_init(),执行 /sbin/init 1573-1646
arch/x86/entry/entry_64.S entry_SYSCALL_64,64 位系统调用汇编入口 87-170
arch/x86/entry/syscall_64.c do_syscall_64(),系统调用 C 层分发 87-141
arch/x86/entry/syscall_64.c do_syscall_x64(),调用系统调用实现 53-66
include/linux/sched.h task_struct 结构体定义 820
include/linux/sched.h 进程状态常量(TASK_RUNNING 等) 107-127
include/linux/mm_types.h struct page, struct folio 79, 401
include/linux/mm_types.h struct vm_area_struct 913
include/linux/mm_types.h struct mm_struct 1123
include/linux/fs.h struct inode 766
include/linux/fs.h struct file 1259
include/linux/fs.h struct file_operations 1926
include/linux/net.h struct socket, struct proto_ops 116, 160
include/linux/skbuff.h struct sk_buff 885
include/linux/init.h __init 宏,__initcall 优先级数组声明 45-46, 152-161
include/linux/init.h __define_initcall 宏定义 277
include/linux/rcupdate.h RCU 核心 API(call_rcu, synchronize_rcu) 51-53
kernel/sched/sched.h struct sched_class 调度类接口 2497
kernel/sched/sched.h struct cfs_rq CFS 运行队列 678
mm/memory.c handle_mm_fault() 缺页异常入口 6589
mm/memory.c __handle_mm_fault() 缺页处理核心 6355

由 Claude Code 分析生成