Hang Zhengyang

Linux 内核:定义、职责、心智模型与结构

前置:进程、fd 与文件、线程、状态切换、权限与安全、页表。前五篇用 xv6 讲清了单个机制,这一篇把视角拉远,看一个真实的、生产级的内核整体是什么样子。

学习路线:OVERVIEW 的「主题地图」把每个 xv6 lab 对应到 Linux 的子系统和读码任务。

本篇的数字来自两处,都是 2026-10-09 实测的:

  • 本机源码树 ~/repo/kernel-lab/linux:主线 v7.3-rc6(提交 47324d3a5b3a)。
  • 本机正在运行的内核:Ubuntu 的 7.0.0-38-generic,x86-64,16 个逻辑 CPU,已经开机约 40 小时。

先猜再看(命令都在宿主机上跑):

uname -srm; cat /proc/cmdline         # 我是哪个内核、开机时带了什么参数
lsmod | wc -l                          # 加载了多少个模块
ps -eo ppid,comm | awk '$1==2'         # 内核线程(kthreadd 的孩子)
grep -E '^(ctxt|processes)' /proc/stat # 开机以来的上下文切换次数、fork 次数
strace -c ls /etc > /dev/null          # 一个 ls 发了多少个系统调用

0. 一张总图

Linux 内核是一个常驻内存、以 CPU 最高特权级运行的大程序。它独占硬件,把硬件包装成进程、文件、地址空间、socket 这些抽象,通过约 390 个系统调用提供给用户程序;它本身不"主动运行",而是被系统调用、异常、中断这三种事件唤起,再加上一批自己创建的内核线程做后台工作。

 ┌────────────────────────────── 用户空间(U 态 / ring 3)──────────────────────────────┐
 │  bash   python   nginx   systemd(pid 1)   ……         glibc / musl:把系统调用包成函数 │
 └──────────────────┬───────────────────────────────────────────────▲──────────────────┘
       系统调用(syscall / ecall)  缺页等异常                       │ 返回值、信号、数据
 ═══════════════════╪════════════ 特权级边界 ════════════════════════╪══════════════════
 ┌──────────────────▼───────────────────── 内核空间(S 态 / ring 0)──┴──────────────────┐
 │  系统调用入口:arch/x86/entry/  →  do_syscall_64()  →  sys_call_table[nr]             │
 │ ┌─────────────┬──────────────┬──────────────┬──────────────┬──────────────────────┐ │
 │ │ 进程 & 调度  │ 内存管理      │ VFS 文件系统  │ 网络栈        │ IPC、安全、时间、BPF…  │ │
 │ │ kernel/     │ mm/          │ fs/          │ net/         │ ipc/ security/ …      │ │
 │ └──────┬──────┴──────┬───────┴──────┬───────┴──────┬───────┴──────────────────────┘ │
 │        │       块设备层 block/   字符设备、TTY…    网络设备层                            │
 │ ┌──────▼─────────────▼──────────────▼──────────────▼─────────────────────────────┐  │
 │ │ 设备驱动 drivers/(源码的 69%):NVMe、GPU、网卡、USB、Wi-Fi……                     │  │
 │ └────────────────────────────────────────┬─────────────────────────────────────┘  │
 │ 体系结构相关 arch/x86、arch/riscv:陷入入口、页表格式、上下文切换、中断控制器          │
 └───────────────────────────────────────────┼───────────────────────────────────────┘
               中断 ▲                         ▼ 读写设备寄存器、DMA
 ┌────────────────────────────────────── 硬件 ────────────────────────────────────────┐
 │  CPU(多核)   内存   磁盘   网卡   GPU   定时器   中断控制器                            │
 └───────────────────────────────────────────────────────────────────────────────────┘

1. 定义:从八个角度说"内核是什么"

同一个东西,站的位置不同,看到的样子就不同。

角度 内核是…… 本机实测 / 例子
一个程序 一个 ELF 可执行文件,开机时被加载进内存,之后一直待在那里 本机编译出的 vmlinux 436MB(带调试信息),压缩后的 bzImage 15MB;运行中的内核有 383,745 个符号(/proc/kallsyms)
硬件的主人 唯一运行在最高特权级、能直接碰硬件的软件。别人想用硬件都得经过它 x86 的 ring 0、RISC-V 的 S 态;ls /sys/bus 列出 53 种总线
一个服务端 用户程序是客户端,系统调用是请求,fd、pid 是句柄(见 01) x86-64 约 390 个系统调用;ls /etc 一次就发了 150 个
资源的复用器和仲裁者 把一份硬件分给很多程序:CPU 按时间分,内存按空间分,并决定谁先谁后 开机 40 小时上下文切换 2.87 亿次,平均每秒约 2000 次
抽象的提供者 把杂乱的硬件变成整齐的抽象:磁盘块变成文件,网卡帧变成 socket,物理内存变成地址空间 /proc/filesystems 里 31 种文件系统,用户都用同一个 read 读
事件处理器 不是一个在后台循环的进程,而是一堆被事件调用的代码:系统调用、异常、中断进来就执行,处理完就返回 开机以来中断 2.46 亿次(/proc/stat 的 intr)
一个框架 核心定义接口(一张张函数指针表),驱动和文件系统是插进来的实现,很多还能运行时加载 本机加载了 176 个模块,最大的是 amdgpu(21MB)
一个工程和社区 全世界最大的协作软件项目之一,有自己的发布节奏、维护者层级和规矩 3,826 万行代码;v7.1 → v7.2 九周里 2,737 位作者提交 16,418 个补丁

内核不是什么

  • 不是"操作系统"的全部。 Linux 严格来说只是内核。你用的 Ubuntu = Linux 内核 + glibc + systemd + shell + 各种工具。Android 用的也是 Linux 内核,但用户空间完全不同。
  • 不是 libc。 printf、malloc、fopen 是 C 库在用户态实现的,最后才落到 write、mmap、openat 这些系统调用上。
  • (基本上)不是一个进程。 内核代码大多是"在某个进程的身份下"或"在中断里"执行的,没有一个叫"内核"的进程在排队等 CPU。例外是内核线程(第 3.2 节)。
  • 不是 pid 1。 pid 1 是用户空间的 init(本机是 systemd),由内核启动的第一个用户进程。

2. 职责:内核替用户程序管哪些事

每一项职责都可以用同一组问题来看:给用户的抽象是什么、状态存在哪个结构里、代码在哪、xv6 里对应什么。

职责 给用户的抽象 核心状态(结构体) 源码 xv6 对应 / lab
进程管理 进程、线程、信号、fork/exec/exit/wait struct task_struct(include/linux/sched.h:835,定义一千多行) kernel/fork.c、exit.c、signal.c struct proc,util lab
CPU 调度 "我的程序在跑";优先级、nice、实时策略 每个 CPU 一个运行队列 struct rq;调度类 struct sched_class kernel/sched/:fair.c(EEVDF)、rt.c、deadline.c、ext/(BPF 写调度器) scheduler() 轮转,lock lab
内存管理 私有地址空间、mmap、brk、共享内存 struct mm_struct、vm_area_struct、struct folio/page、页表 mm/:memory.c(缺页)、page_alloc.c(伙伴系统)、slub.c、vmscan.c(回收) vm.c、kalloc.c,pgtbl、cow、mmap lab
文件系统 文件、目录、路径、权限、挂载 VFS 四大对象:super_block、inode、dentry、file fs/ 公共层 + fs/ext4/、fs/btrfs/… fs.c、file.c,fs lab
块设备 I/O (对用户透明)磁盘读写、I/O 调度 struct bio、请求队列 block/ bio.c、virtio_disk.c
网络 socket、TCP/UDP、路由、防火墙 struct sock、struct sk_buff、net_device net/:ipv4/、core/、netfilter/ net lab(只有驱动和简单协议)
设备驱动 /dev 下的设备、显卡、声卡、USB…… struct device、struct device_driver、各种 *_ops drivers/ uart.c、virtio_disk.c,net lab
进程间通信 管道、信号、共享内存、消息队列、Unix socket pipe_inode_info、ipc_namespace fs/pipe.c、ipc/、net/unix/ pipe.c
安全 用户和权限、capability、SELinux/AppArmor、seccomp struct cred、LSM 钩子 security/、kernel/seccomp.c 只有 U/S 和 PTE_U,syscall lab 的沙箱
时间 时钟、定时器、sleep、超时 hrtimer、timekeeper、jiffies kernel/time/ ticks、clockintr
资源隔离与容器 namespace(看到什么)、cgroup(用多少) nsproxy、css_set kernel/nsproxy.c、kernel/cgroup/ 无
虚拟化 KVM:让用户进程当虚拟机 struct kvm、kvm_vcpu virt/kvm/、arch/x86/kvm/ 无
可观测性 /proc、/sys、ftrace、perf、eBPF tracepoint、kprobe、BPF 验证器 kernel/trace/、kernel/bpf/、fs/proc/ procdump()(Ctrl-P)
电源管理 睡眠、休眠、调频 cpufreq_policy、dev_pm_ops kernel/power/、drivers/cpufreq/ 无

一句话:xv6 实现了前 8 行的最小版本,Linux 把每一行都做到了生产级,又加上了后 6 行。

3. 心智模型

单靠一个模型看不全。下面七个模型各回答一类问题,遇到新东西时挑合适的那个去套。

3.1 服务端模型:用户只拿得到句柄

(01 已经讲过,这里只总结。)

  • 状态在内核里,用户手里只有编号:fd、pid、socket 都是。用户想改状态只能发请求。
  • 每个请求内核都要校验:这个 fd 是不是你的、这个指针是不是你的内存(04)。
  • 用它回答:"用户程序能不能直接做 X?"答案几乎总是"不能,只能请内核做"。

3.2 事件驱动模型:内核什么时候在执行

内核没有自己的 main 循环。CPU 进入内核只有三种方式,外加内核自己创建的线程:

入口 谁触发 同步还是异步 代表谁执行 能不能睡眠
系统调用 用户程序主动发起 同步 当前进程(进程上下文) 能(比如等磁盘)
异常(缺页、除零) 当前指令出错 同步 当前进程 缺页处理能睡眠
中断 设备,和当前进程无关 异步 谁也不代表(中断上下文) 不能
内核线程 内核自己创建 — 它自己(没有用户地址空间) 能

"能不能睡眠"是内核编程的第一条规矩。 睡眠就是让出 CPU 等别人唤醒;中断打断的是一个无关的进程,在里面睡眠就等于让无辜的进程替你等。所以中断处理程序只做最紧急的事(确认设备、拷走数据),剩下的推给"下半部":

  • 软中断(softirq):中断返回前或在 ksoftirqd 线程里执行,网络收包主要靠它;
  • 工作队列(workqueue):交给 kworker 内核线程在进程上下文里做,可以睡眠。

本机实测:kthreadd(pid 2)下面有 246 个内核线程,其中 kworker 135 个;migration、ksoftirqd、cpuhp、idle_inject 各 16 个,每个 CPU 一个。

用这个模型回答:"这段内核代码现在是替谁在跑?能不能调用会阻塞的函数?"

3.3 对象模型:内核是一堆带引用计数的对象

把内核看成一个内存里的数据库:表是各种结构体,系统调用是对它们的事务。

对象 代表 被谁引用
task_struct 一个线程 运行队列、父子链表、pid 哈希表
mm_struct 一个地址空间 同一进程的所有线程
files_struct fd 表 同一进程的线程;fork 时复制
struct file 一次 open 的结果(含偏移量) fd 表里的项;fork 后父子共享
inode / dentry 一个文件 / 一个路径分量 file、目录缓存
folio / page 一块物理内存 页表、page cache、各种缓冲区
sk_buff 一个网络包 协议栈各层依次传递

三条通用规律:

  1. 对象之间用指针连成图,系统调用就是在图上走一条路。read(fd):task_struct → files_struct → file → inode → page cache 里的 folio。
  2. 共享就用引用计数:fork 后父子的 fd 指向同一个 struct file,引用计数变成 2。close 只是减 1,减到 0 才真正释放。xv6 的 filedup/fileclose 是同一个模式。
  3. 对象有生命周期:分配、初始化、发布(让别人能找到)、使用、撤销、等所有人用完、释放。很多内核 bug(use-after-free、泄漏)就是这条链断了一环。

用它回答:"这个状态存在哪?谁还在用它?什么时候能释放?"

3.4 面向对象的 C:函数指针表

内核用 C 写,却到处是"接口"和"多态":一个结构体里装满函数指针,不同的实现填不同的函数。

接口(函数指针表) 定义在 谁来实现 统一了什么
struct file_operations include/linux/fs.h:1919 每种文件系统、每个字符设备驱动 普通文件、管道、socket、/dev/null,用户都用同一个 read
struct inode_operations include/linux/fs.h:1994 每种文件系统 查找、创建、删除、改名
struct net_device_ops include/linux/netdevice.h:1458 每个网卡驱动 协议栈不用关心是哪块网卡
struct sched_class kernel/sched/sched.h:2623 fair、rt、deadline、ext、idle 调度器核心不用关心具体策略

追一次 read,就能看到多态在哪里发生:

read(fd, buf, n)                                   用户态
  └→ SYSCALL_DEFINE3(read, …)      fs/read_write.c:723
     └→ vfs_read()                 fs/read_write.c:554     公共逻辑:权限、偏移量
        └→ file->f_op->read_iter()                         ← 多态:按文件类型分派
           ├→ ext4_file_read_iter()    普通文件:查 page cache,没有就发块 I/O
           ├→ anon_pipe_read()         管道:从环形缓冲区拷
           └→ sock_read_iter()         socket:从接收队列拷

xv6 里同样的分派写成 fileread() 里的 if (f->type == FD_PIPE) … else if (f->type == FD_INODE) …(02)。Linux 把这个 if 换成了函数指针,于是新增一种文件类型不用改公共代码。"一切皆文件"在实现上就是这张表。

3.5 分层与"机制和策略分离"

数据从用户走到硬件,要穿过好几层,每一层只和上下邻居说话:

写文件:  write → VFS → ext4(文件块怎么排) → page cache → 块层(合并、排序 I/O) → NVMe 驱动 → 磁盘
发网络包:send  → socket → TCP(可靠、拥塞控制) → IP(路由) → 网络设备层(排队) → 网卡驱动 → 网卡

每层之间是第 3.4 节的函数指针接口,所以换一个文件系统、换一块网卡,上下层都不用动。

机制和策略分离是同一个思想的另一面:内核提供"怎么做"的机制,把"做哪个"的决定留成可替换的部分。

  • 调度:切换上下文是机制;选哪个任务是策略(sched_class,现在甚至可以用 BPF 写,即 sched_ext)。
  • 内存回收:换出页是机制;换哪页是策略(LRU 列表、swappiness)。
  • 安全:LSM 钩子是机制;SELinux、AppArmor 是策略。
  • 网络:收发包是机制;防火墙规则(netfilter / nftables)、拥塞控制算法(tcp_congestion_control)是策略。

3.6 缓存模型:内核是一个巨大的缓存管理器

很多内核内存其实是缓存,用来把慢的东西变快:

缓存 缓存的是 省掉的是
page cache 文件内容 磁盘读写
dentry / inode cache 路径查找结果、文件元数据 一层层读目录
slab / SLUB 常用大小的对象 每次从伙伴系统切页
TLB(硬件,内核管一致性) 地址翻译 走页表(05 §8)
每 CPU 的空闲页列表 刚释放的页 抢全局锁

free -h 里的 buff/cache 就是这些。Linux 的做法是"空闲内存就是浪费的内存":尽量都拿来当缓存,需要时再回收。用这个模型回答:"为什么第二次读同一个文件快得多?""为什么 free 显示内存快满了,系统却不卡?"

3.7 并发模型:很多 CPU 同时在内核里

xv6 只有几个 CPU,用一把大锁也能凑合。Linux 要在几百个 CPU 上同时运行内核代码,并发是设计的核心:

手段 什么时候用 例子
自旋锁(spinlock) 临界区很短,不能睡眠(包括中断里) 运行队列 rq->lock
互斥锁(mutex)、信号量 临界区可能睡眠 inode 的 i_rwsem
每 CPU 数据 根本不共享,就不用锁 每 CPU 的运行队列、统计计数
RCU 读多写少:读者完全不加锁,写者复制一份改完再换指针,等所有老读者走完才释放旧的 路由表、进程列表、dentry 查找
原子操作、内存屏障 单个计数器、标志位 引用计数 refcount_t

抢占让问题更复杂:内核代码执行到一半也可能被换下 CPU。Linux 提供几种抢占模式(PREEMPT_NONE、VOLUNTARY、PREEMPT),实时补丁 PREEMPT_RT 在 6.12 进入主线,让几乎所有内核代码都可以被抢占,换取可预测的延迟。xv6 的 lock lab 是这一节的入门。

3.8 宏内核,但是模块化、可编程

架构 驱动、文件系统在哪运行 代表 优点 代价
宏内核(monolithic) 全部在内核空间,共享一个地址空间 Linux、xv6、FreeBSD 子系统之间直接调用函数,快 任何一个驱动的 bug 都能让整个系统崩溃
微内核(microkernel) 只有调度、IPC、地址空间在内核,其余是用户态服务 seL4、QNX、MINIX 3 驱动崩了重启它就行;内核小到可以形式化验证 一次文件读要好几次 IPC,慢

Linux 是宏内核,但有两个方向在"软化"它:

  • 可加载模块:驱动编译成 .ko,运行时 insmod / modprobe 加载。本机加载了 176 个。注意模块加载进来后和内核完全平等,并不更安全。
  • eBPF:用户写的小程序经过验证器检查(不能死循环、不能越界访问)之后,挂到内核的钩子上运行:抓包、跟踪、安全策略,现在连调度器(sched_ext)都能写。这是"在宏内核里安全地运行扩展代码"。

4. 结构

4.1 源码树地图

v7.3-rc6,统计 .c .h .S .rs 文件:

目录 文件数 行数 占比 里面是什么
drivers/ 34,375 2,642 万 69% 设备驱动,148 个子目录
arch/ 10,370 253 万 6.6% 21 个体系结构目录(含用户态的 um):陷入入口、页表、启动、上下文切换
sound/ 2,721 168 万 4.4% 声卡驱动和音频框架
fs/ 2,174 165 万 4.3% VFS 公共层 + 几十种文件系统
include/ 6,724 148 万 3.9% 头文件;include/linux/ 是内核内部接口,include/uapi/ 是给用户空间的接口
tools/ 5,159 145 万 3.8% perf、bpftool、selftests 等用户态工具
net/ 1,742 130 万 3.4% 网络协议栈
kernel/ 663 57 万 1.5% 进程、调度、信号、时间、BPF、tracing、cgroup
lib/ 831 33 万 0.9% 通用库:字符串、红黑树、压缩、校验和
mm/ 196 23 万 0.6% 内存管理
rust/ 442 16 万 0.4% Rust 支持和内核抽象(6.1 起)
security/ 269 13 万 0.3% LSM 框架、SELinux、AppArmor…
crypto/ 166 10 万 0.3% 加密算法
block/ 98 7 万 0.2% 块设备层、I/O 调度
io_uring/ 87 2.8 万 — 异步 I/O 接口
virt/、ipc/、init/ 41 2.6 万 — KVM 公共部分、System V IPC、启动
合计 66,397 3,826 万 xv6 的 kernel/ 是 6,694 行,约为它的 1/5700

几点观察:

  • 驱动占了七成。 "Linux 内核"的大部分是在支持各种硬件。真正的"核心"(kernel/、mm/、fs/ 的公共部分、net/ 的核心、block/、ipc/、init/)只有几百万行。
  • mm/ 只有 23 万行,却是最难的部分之一:代码密度高,并发细节多。
  • 读 Linux 的现实策略:不要试图读完,只沿着一条路径读(第 7 节)。

4.2 一个进程在内核里长什么样

struct task_struct 的定义从 include/linux/sched.h:835 到 1995 行。按 03 线程 里"资源 + 执行流"的拆法归类:

部分 关键字段 xv6 struct proc 对应
执行流 __state、thread(保存的寄存器)、stack(内核栈)、on_cpu state、context、kstack
调度 prio、policy、se(CFS/EEVDF 实体)、rt、dl、sched_class 无(轮转)
地址空间 mm(指向 mm_struct,线程之间共享) pagetable、sz
打开的文件 files(指向 files_struct) ofile[NOFILE]
文件系统上下文 fs(cwd、root) cwd
身份 pid、tgid(线程组 = 进程)、real_parent、children pid、parent
权限 cred(uid、gid、capability) 无
信号 signal、sighand、pending killed
隔离 nsproxy(各种 namespace)、cgroups 无

关键差别:xv6 把资源直接放在 struct proc 里;Linux 放的是指向资源的指针,所以多个 task_struct 可以共享同一个 mm、files。clone() 的各个 CLONE_* 标志决定共享哪些、复制哪些:全复制就是进程,全共享就是线程(03 §4)。

4.3 开机:从第一条指令到 pid 1

步骤 Linux(x86-64) xv6
固件 UEFI 初始化硬件,加载引导程序 QEMU 直接把内核放在 0x80000000
引导程序 GRUB 加载 bzImage 和 initramfs,传入命令行(/proc/cmdline) 无
解压与体系结构初始化 arch/x86/boot/、arch/x86/kernel/head_64.S:建临时页表、进 64 位模式 entry.S → start.c:设栈、从 M 态切到 S 态
start_kernel() init/main.c:987:初始化内存管理、调度、中断、定时器、控制台…… main.c 里的 main():kinit、kvminit、procinit……
第一批线程 rest_init() 创建 pid 1(kernel_init)和 pid 2(kthreadd) userinit() 创建第一个进程
内核初始化收尾 kernel_init()(init/main.c:1554):初始化驱动、挂载根文件系统(先用 initramfs) —
进入用户空间 kernel_init 执行 /sbin/init,也就是 systemd,pid 1 从内核线程变成用户进程 第一个进程 exec /init,它再启动 sh

4.4 构建与配置

  • Kconfig:全树一共 22,655 个配置项(git ls-files | grep Kconfig)。make menuconfig 选择编进哪些功能,结果写进 .config。每一项可以是 y(编进内核)、m(编成模块)或不选。
    • 本机给 vng 编的测试内核只开了 1,668 个 =y、16 个 =m;发行版内核通常有几千个模块。
    • make tinyconfig 能编出最小的内核,make defconfig 是体系结构推荐的默认配置。
  • 产物:vmlinux(未压缩的 ELF,调试和 gdb 用)、arch/x86/boot/bzImage(压缩后真正被引导的映像)、*.ko(模块)、System.map(符号表)。
  • Kbuild:每个目录的 Makefile 只写 obj-$(CONFIG_FOO) += foo.o,配置项直接决定编不编。

5. 在本机观察运行中的内核

先猜,再运行(2026-10-09,Ubuntu 7.0.0-38-generic,开机约 40 小时):

命令 看什么 本机结果 对应心智模型
uname -srm 内核版本、架构 Linux 7.0.0-38-generic x86_64 程序
cat /proc/cmdline 引导程序传给内核的参数 BOOT_IMAGE=/boot/vmlinuz-… root=UUID=… ro quiet splash … 开机
lsmod | wc -l 已加载模块 176 个;amdgpu 21MB、mac80211 1.9MB 框架、模块
ps -eo ppid,comm | awk '$1==2' 内核线程 246 个:kworker 135、每 CPU 线程各 16 事件驱动
grep ctxt /proc/stat 上下文切换总数 2.87 亿次,约 2000 次/秒 复用器
grep processes /proc/stat 开机以来 fork 的次数 516,056 进程管理
strace -c ls /etc 一个小程序发了多少系统调用 150 次 服务端
wc -l /proc/kallsyms 内核符号数 383,745(非 root 看到的地址都是 0) 程序
ls /sys/bus 内核认识的总线类型 53 种:pci、usb、i2c、virtio… 驱动模型
free -h buff/cache 一列 — 缓存模型
cat /proc/self/maps 一个进程的 VMA,包括 [vdso]、[vvar] 45 段(grep -c 自己) 内存管理(05)

/proc 和 /sys 本身就是第 3.4 节的产物:它们不是磁盘上的文件,而是内核用 file_operations 现场生成的内容。读 /proc/stat 就是调用一个内核函数。

6. xv6 与 Linux 对照

方面 xv6 Linux 差距从哪来
规模 6,694 行内核 3,826 万行 主要是驱动和体系结构
体系结构 RISC-V 21 个 arch/ 目录 arch/ 抽象出公共接口
系统调用 22 个(加上 lab 添加的) 约 390 个(x86-64) 线程、异步 I/O、namespace、BPF……
调度 一个循环,轮转 调度类、EEVDF、每 CPU 运行队列、负载均衡、cgroup 带宽控制 公平、延迟、多核扩展
内存分配 一条 4KB 空闲链表 伙伴系统 + SLUB + 每 CPU 缓存 + NUMA 碎片、大块连续内存、多核扩展
内存回收 无,内存不够就失败 LRU、swap、OOM killer 内存超卖
文件系统 一种,带日志 VFS + 几十种 函数指针表
缓存 30 个块的 buffer cache page cache、dentry/inode cache 读写性能
锁 自旋锁、睡眠锁 再加上 RCU、每 CPU 数据、seqlock、读写锁 几百个 CPU
驱动 UART、virtio 磁盘 数万个 真实世界的硬件
安全 U/S 隔离 用户和组、capability、LSM、seccomp、namespace 多用户、容器
可观测性 Ctrl-P 打印进程表 /proc、/sys、ftrace、perf、eBPF 生产环境调试

xv6 的价值:每个机制都只有最本质的部分,几十行就能看全。所以读 Linux 时,先在 xv6 里找到对应的那几十行,把它当地图。

7. 开发模型与社区(为参与贡献准备)

发布节奏

  • 每个版本大约 9 到 10 周:前 2 周是合并窗口,Linus 从各子系统维护者那里拉取新功能,然后发布 -rc1;之后每周一个 -rc,只修 bug,通常到 -rc7 或 -rc8,然后正式发布。
  • 本机树里的实例:v7.1(2026-06-14)到 v7.2(2026-08-16),九周,16,418 个非合并提交、2,737 位作者;v7.2 到 v7.3-rc1 的合并窗口里进了 15,267 个提交。
  • 稳定版(stable)把修复回移植到已发布版本;每年有一个版本被定为 LTS,维护好几年。Ubuntu 的 7.0.0-38 就是在 7.0 上叠加了发行版自己的补丁。

维护者层级

  • 补丁不是直接给 Linus,而是先给子系统维护者,维护者收进自己的树,合并窗口时再由 Linus 拉取。
  • MAINTAINERS 文件记录了每个文件归谁管:本机统计 3,321 个条目,状态分布是 Maintained 2,235、Supported 807(有公司付钱维护)、Orphan 145(没人管)。
  • scripts/get_maintainer.pl 补丁文件 告诉你补丁该发给谁、抄送哪些邮件列表。

提交补丁

  • 用邮件:git format-patch + git send-email 发到邮件列表,在 lore.kernel.org 上公开存档,评审意见也在邮件里来回。
  • 提交前跑 scripts/checkpatch.pl 检查代码风格(Documentation/process/coding-style.rst)。
  • Signed-off-by: 表示你签署了 DCO(Developer Certificate of Origin),声明你有权提交这段代码。
  • 入门文档:Documentation/process/ 下从 1.Intro.rst 到 8.Conclusion.rst 的系列,submitting-patches.rst,howto.rst。

关于 AI 工具的规定

本机树里的 Documentation/process/coding-assistants.rst 和 generated-content.rst 规定:

  • AI 不能加 Signed-off-by:。只有人能签署 DCO,提交者要审查所有 AI 生成的代码,并承担全部责任。
  • 用了 AI 工具要加 Assisted-by: 标签,格式是 Assisted-by: LLM [工具1] [工具2](工具指 coccinelle、sparse 这类分析工具)。
  • 用 AI 找 bug 要走完整流程:先读完开发流程文档,尽量写出复现程序,再写修复并验证。
  • 拼写修正、格式化、机械重命名这类琐碎工具不在规定范围内。

8. 怎么读这么大的代码

  1. 从一个具体问题出发,只追一条路径。 比如"read() 怎么拿到文件数据",沿着第 3.4 节那条调用链往下走,旁边的分支先不看。
  2. 先找 xv6 的对应代码当地图(OVERVIEW 的主题地图里有每个 lab 的对照)。
  3. 工具:
    • 在线交叉引用:elixir.bootlin.com,点一个符号就能看所有定义和引用;
    • 本地:make compile_commands.json(或 scripts/clang-tools/gen_compile_commands.py)之后用 clangd 跳转;
    • git log -p 文件、git blame:看一段代码为什么写成这样,提交说明往往比注释更有用。
  4. 让它跑起来看:用 vng / QEMU 起本机编出的内核,gdb 加载 vmlinux 打断点;或者在宿主机上用 ftrace(/sys/kernel/tracing)、bpftrace、perf 观察真实调用(都需要 root)。
  5. 读 Documentation/:每个子系统大多有设计文档,比如 Documentation/mm/、Documentation/scheduler/、Documentation/filesystems/vfs.rst。

9. 一张表

问题 答案
内核是什么 常驻内存、最高特权级运行的程序,独占硬件、提供抽象
它什么时候执行 系统调用、异常、中断进来时,以及内核线程被调度时
用户怎么使用它 约 390 个系统调用;手里只有 fd、pid 这样的句柄
状态存在哪 一张由结构体组成的图:task_struct、mm_struct、file、inode…,用引用计数管理生命周期
怎么支持这么多硬件和文件系统 函数指针表做接口,驱动和文件系统是实现,可以编成模块
怎么扛住几百个 CPU 每 CPU 数据、RCU、细粒度锁
为什么是宏内核 子系统之间直接调函数,快;代价是一个驱动 bug 能拖垮全系统
代码大部分是什么 驱动(69%)
怎么开发 9 到 10 周一个版本,按子系统分层维护,补丁走邮件列表

自测:不看笔记能不能做到

  1. 从八个角度里挑三个,各用一句话说内核是什么。
  2. 列出 CPU 进入内核的三种方式,并说出哪一种里不能睡眠、为什么。
  3. 画出 read(fd) 从系统调用到具体文件系统的调用链,指出多态发生在哪一步。
  4. fork 和 pthread_create 在 task_struct 层面的区别是什么?
  5. 说出两个"机制和策略分离"的例子。

思考题(先自己答)

  1. 中断处理程序为什么不能调用 mutex_lock(),却可以调用 spin_lock()?如果中断处理程序要拿的自旋锁,正好被它打断的那段代码拿着,会发生什么?内核用什么办法避免?
  2. 本机有 135 个 kworker 线程,却只有 16 个 CPU。为什么需要这么多?
  3. 一个可加载模块和编进内核的代码,在权限上有区别吗?那模块签名(CONFIG_MODULE_SIG)防的是什么?
  4. eBPF 程序也在内核里运行,为什么它就"安全",而模块不安全?验证器拒绝了哪些东西?
  5. 为什么说 /proc/self/maps 里的每一行,对应的是内核里的一个 vm_area_struct,而不是一组页表项?两者的关系是什么?(提示:05 的懒分配。)
  6. 微内核里一次 read() 文件大概要几次 IPC?宏内核里呢?