基于 Linux 6.x 主线内核源码的深度分析文档
- 内核概述与设计哲学
- 源码目录深度解析
- 内核启动流程(源码级)
- 系统调用机制(源码级)
- 核心数据结构体系
- 内核并发模型
- 内存管理架构
- 进程调度架构
- VFS 与文件系统架构
- 网络子系统架构
- 设备驱动模型
- 安全架构
- eBPF 可编程内核
- 性能关键设计
- 内核扩展性机制对比
Linux 内核是一个宏内核(Monolithic Kernel),自 1991 年 Linus Torvalds 发布 0.01 版本以来,已经演进为人类历史上代码规模最大、参与者最多的开源软件项目之一。
在哲学层面,Linux 的核心设计目标可以归纳为三点:
- UNIX 兼容性:遵循 POSIX 标准,提供稳定的用户空间 ABI(尤其是系统调用接口)
- 性能优先:在安全与性能的权衡中,内核倾向于选择性能,将安全责任部分交给用户空间
- 渐进式演化:新特性通过小增量合并,避免大规模重写
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 工作异步化,减少系统调用次数
理解 Linux 内核必须先理解它所处的执行环境约束,这些约束决定了内核的诸多设计选择:
内核代码不能使用 FPU(浮点处理单元)或 SIMD 指令,原因在于:
- 每次上下文切换需要保存/恢复 FPU 状态(x86 上
XSAVE区域可达 2.5KB) - 如果内核随意使用 FPU,则每次中断/异常都需要保存/恢复 FPU 状态,代价极高
- 解决方案:内核提供
kernel_fpu_begin()/kernel_fpu_end()显式区域,仅在必要时(如 AES-NI 加密)才启用
x86_64 上,每个进程的内核栈默认只有 16KB(THREAD_SIZE = PAGE_SIZE * 2 = 16384 字节)。这导致:
- 内核函数不能使用大的栈局部变量(通常应 < 1KB)
- 递归调用深度受严格限制
- 不能在栈上分配大型数据结构,应使用
kmalloc()等堆分配 - 内核提供
CONFIG_FRAME_WARN编译时警告超大栈帧
在硬中断(hardirq)、软中断(softirq)、tasklet 上下文中:
- 不能调用任何可能休眠的函数(
mutex_lock、kmalloc(GFP_KERNEL)等) - 原因:中断可能在任意进程上下文中触发,如果中断处理程序阻塞,系统将死锁
- 只能使用
spinlock、kmalloc(GFP_ATOMIC)等非阻塞原语
持有 spinlock 时,内核是不可抢占的(或者说,内核抢占被禁用)。这意味着:
spinlock保护的临界区必须尽量短暂- 不能在持有
spinlock时进行任何可能阻塞或耗时的操作
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)
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 通用代码)
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/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/ 的主要组件:
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 进程退出
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 等所有主要子系统。
这是一个重要的架构决策,直接关系到内核 ABI 的稳定性。
include/
├── linux/ # 内核内部头文件(可以随时改变)
├── uapi/ # 用户空间 API(必须保持稳定)
│ ├── linux/ # 系统调用参数、ioctl 结构体
│ └── asm/ # 架构相关的用户空间接口
└── asm-generic/ # 通用架构实现(可被覆盖)
include/uapi/ 的 ABI 稳定性承诺:
- 一旦合入主线,
uapi/中的接口永远不能破坏性修改 - 这是 Linux 的核心承诺:任何在旧内核上编译的用户程序,必须能在更新的内核上运行
- 具体例子:
struct stat的布局自 1992 年以来从未改变(通过stat64、statx扩展而不是修改)
include/linux/ 的自由度:
- 内核内部接口可以随时修改,不需要保持向后兼容
- 这也是为什么内核不提供稳定的内核内部 ABI,加载模块必须针对特定内核版本编译
完整 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)
└─────────────────────────────┘
GRUB2 将 bzImage(压缩的内核镜像)加载到内存后,首先执行的是 arch/x86/boot/header.S 中的 16 位实模式代码。这段代码负责:
- 内存探测:通过 INT 15h/E820 探测可用物理内存区域
- 视频模式设置:配置 VGA/VESA 模式
- CPU 能力检测:确认支持 64 位模式
- 解压准备:调用
arch/x86/boot/compressed/misc.c中的extract_kernel()解压内核
解压后的 vmlinux ELF 文件被加载到高地址(CONFIG_PHYSICAL_START = 0x1000000,即 16MB 处),然后跳转到 arch/x86/kernel/head_64.S 中的 startup_64 标签。
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 语言世界。
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()__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 内存。
init/main.c:714-760 的 rest_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.");
}系统调用是用户空间进入内核的唯一正规途径(除了异常/中断)。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│ │ 保存寄存器,切换栈 │
└──────────────────┘ └─────────────────────────────────┘
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:170arch/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);
}
}某些系统调用(clock_gettime、gettimeofday、time)调用极其频繁,但不需要实际进入内核。Linux 通过 vDSO(virtual Dynamic Shared Object) 机制解决这个问题。
工作原理:
- 内核在进程地址空间中映射一个特殊的共享库
vdso.so(约 4KB) - 这个库包含特定系统调用的用户空间实现
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 保证时间数据读取的一致性。
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 │ 通知 │
└───────────────────────────────┘
用户空间
══════════════════════════════════════════════════════════════════
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 = 返回值
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 时间(纳秒)*/ │
└─────────────────────────────────────────────────────────────────┘
}
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
└─────────────┘
mm_struct(include/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 私有数据 */
};struct file(include/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_operations(include/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 个函数指针
};struct socket(include/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_ops(include/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);
// ...
};struct sk_buff(include/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))
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 红黑树 │
└────────────────────────────────────────────────────────────┘
内核代码在不同的执行上下文中运行,不同上下文有不同的约束:
执行上下文层次(中断优先级从高到低):
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()等可睡眠函数
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(单处理器)上退化为关中断。
struct mutex my_mutex;
mutex_init(&my_mutex);
mutex_lock(&my_mutex); /* 如果锁被持有,当前进程睡眠等待 */
/* 临界区 - 可以睡眠 */
mutex_unlock(&my_mutex);何时用 mutex:
- 进程上下文(syscall、kthread)
- 临界区可能需要较长时间(毫秒级)
- 持有锁时需要调用可能睡眠的函数
当数据以读为主、偶尔写时,使用读写锁可以提高并发度:
struct rw_semaphore sem;
/* 读者(并发访问)*/
down_read(&sem);
/* 读临界区 */
up_read(&sem);
/* 写者(独占访问)*/
down_write(&sem);
/* 写临界区 */
up_write(&sem);内核中 mmap_lock(mm_struct.mmap_lock,mm_types.h:1196)就是典型的 rw_semaphore:大多数内存访问只需读锁,只有 mmap/munmap 等修改操作需要写锁。
RCU(Read-Copy-Update)是 Linux 内核中最重要的并发原语之一,专为读多写少的数据结构设计(如路由表、进程列表、文件系统缓存)。
核心思想:
- 读者完全无锁:
rcu_read_lock()只是禁止抢占(不是真正的锁) - 写者拷贝修改:写者先复制数据副本,修改副本,然后原子替换指针
- 宽限期:等待所有读者离开临界区后,才释放旧数据
/* 读者路径(极低开销)*/
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,极高频率)
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 访问自己的副本,无需锁,无缓存伪共享)
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 指令。
需要保护共享数据?
│
┌────▼────────────────────────────────────────────┐
│ 能不能用 per-CPU 变量?(每个 CPU 独立副本) │
│ → 计数器、缓存、运行队列等 │
└────┬────────────────────────────────────────────┘
│ 不能
┌────▼────────────────────────────────────────────┐
│ 读多写少?(>90% 是读操作) │
│ → 路由表、进程列表、文件系统缓存 │
└────┬────────────────────────────────────────────┘
是 ↓ │ 否(读写均衡)
使用 RCU │
┌────▼──────────────────────────────────────┐
│ 需要在中断上下文中持有锁? │
└────┬──────────────────────────────────────┘
是 ↓ │ 否
spinlock │ ┌────▼────────────────────────────────────┐
(irqsave) │ │ 临界区会睡眠(调用可阻塞函数)? │
│ └────┬────────────────────────────────────┘
│ 是 ↓ │ 否
│ mutex│ ┌────▼──────────────────────────────┐
│ (可睡) │ │ 读写分离有意义? │
│ │ └────┬──────────────────────────────┘
│ 是 ↓ │ 否
│ rw_semaphore spinlock(短临界区)
│ mutex(长临界区)
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 page(include/linux/mm_types.h:79)是物理页的描述符:
- 每个 4KB 物理页对应一个
struct page(约 64 字节) - 64GB RAM = 16M 个页 = 约 1GB
struct page数组 - 使用 union 节省空间(同一页在不同用途下有不同的字段含义)
- 现代内核引入
struct folio(mm_types.h:401)作为新的多页单位抽象
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 └──────────────────────────────┘
内存分配器层次(从高到低):
用户 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
当进程访问没有映射物理页的虚拟地址时,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)
│
返回用户空间,重新执行触发缺页的指令
当物理内存不足时,内核需要回收页面:
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 时的性能。
┌─────────────────────────────────────────────────────────────┐
│ 用户空间 │
│ 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│
└──────────────────────────┘ └────────────────────────────────┘
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+)
CFS(Completely Fair Scheduler,完全公平调度器)是 Linux 的默认调度器。
核心思想:用虚拟时间(vruntime)代替真实时间衡量公平性。每个进程维护 vruntime,值越小表示历史上获得的 CPU 时间越少,应该优先调度。
kernel/sched/sched.h:678(struct 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,在下一个调度点抢占
多 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)
SCHED_FIFO 和 SCHED_RR 是硬实时调度策略:
- SCHED_FIFO:严格按优先级(1-99,99 最高),同优先级 FIFO,无时间片限制
- SCHED_RR:同 SCHED_FIFO 加时间片(默认 100ms),同优先级轮转
实时进程优先级高于所有 CFS 进程,意味着一个实时进程可以 100% 占用 CPU(除非主动放弃或 RT throttling 介入)。
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 调度器取得显著性能提升。
时钟中断(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() 返回处继续执行
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 读写锁 */
// ...
};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 保护允许无锁并发读取。
页缓存是内核中文件数据的缓冲区,所有文件读写都经过它(除非 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)
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(容器特定目录)
两个视图共享同一个内核,但互不干扰。
┌─────────────────────────────────────────────────────┐
│ 用户空间 │
│ 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 │
└─────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ 物理层:数据帧到达网卡(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 取数据 │
└──────────────────────────────────────────────────────────────┘
用户 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 到网卡硬件
│
物理发送(电/光信号)
数据包流经内核的 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 程序
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
┌──────────────────────────────────────────────────────────────────┐
│ 收包路径 │
│ 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 │
└──────────────────────────────────────────────────────────────────┘
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()
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 表)
/* 典型的平台驱动注册 */
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 */ ┌───────────────────────────────────────────────────────────┐
│ 用户可见 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, ...);驱动使用 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 到任意物理内存。
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/
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
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_* 检查的最后一关)
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 安全策略
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)的基础。
seccomp 是容器和沙箱安全的最后一道防线,即使容器逃逸也无法调用危险系统调用:
Docker 默认阻断的系统调用(部分):
kexec_load 加载新内核(不允许容器替换内核)
reboot 重启系统
mount 挂载文件系统(默认拒绝,需 --privileged)
ptrace 追踪其他进程
personality 修改进程运行特性
...(共约 44 个高危系统调用被默认拒绝)
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 │
└────────────────────────────────────────────────────────────┘
分类 类型 用途
─────────────────────────────────────────────────────────────────
网络/数据平面 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 设备访问控制
BPF Verifier 位于 kernel/bpf/verifier.c,约 30,000 行代码,是 eBPF 安全性的核心。
验证算法(抽象解释):
-
CFG 构建:分析所有指令,建立控制流图,确认无无界循环
-
寄存器类型追踪:
r0: 初始值 0(整数) r1: ctx 指针(类型:PTR_TO_CTX,只读) r2-r5: 函数参数 ... 每条指令执行后,更新所有寄存器的类型状态 若类型不安全(如把整数当指针解引用),拒绝 -
值范围追踪:对整数寄存器追踪可能的值范围(min/max),确保数组访问不越界
-
安全限制:
- 栈 512 字节
- 程序指令数复杂度限制(防止验证器超时)
- 循环必须有有界迭代(5.3+ 通过
bpf_loop()支持有限循环)
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)
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 数据
**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
┌──────────────────────────────────────────────────────────────┐
│ 网络(最成熟) │
│ 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(游戏低延迟调度) │
└──────────────────────────────────────────────────────────────┘
传统文件发送路径(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):注册缓冲区,减少地址翻译
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)
- 内核模块列表(每次符号查找)
在 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)
定期扫描进程访问模式,将热页迁移到访问节点
伪共享(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 访问),性能极高 */
现代内核大量使用无锁数据结构替换传统锁保护的结构:
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 完成队列
| 机制 | 适用场景 | 限制 | 安全性 | 典型示例 |
|---|---|---|---|---|
| 内核模块(.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 驱动原型) |
内核模块是最传统的扩展方式,允许运行时加载/卸载代码:
模块生命周期:
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要求模块必须有信任的签名
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(包括睡眠、内存分配、信号等)
✓ 无复杂度限制
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 整合速度有争议
需求场景 推荐方案
────────────────────────────────────────────────────────────────
观测内核行为(性能分析/调试) 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 点
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) │
└───────────────────────────────────────────────────────────┘
| 文件路径 | 核心内容 | 关键行号 |
|---|---|---|
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 分析生成