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 |
一个网络包 | 协议栈各层依次传递 |
三条通用规律:
- 对象之间用指针连成图,系统调用就是在图上走一条路。
read(fd):task_struct→files_struct→file→inode→ page cache 里的folio。 - 共享就用引用计数:
fork后父子的 fd 指向同一个struct file,引用计数变成 2。close只是减 1,减到 0 才真正释放。xv6 的filedup/fileclose是同一个模式。 - 对象有生命周期:分配、初始化、发布(让别人能找到)、使用、撤销、等所有人用完、释放。很多内核 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是体系结构推荐的默认配置。
- 本机给 vng 编的测试内核只开了 1,668 个
- 产物:
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. 怎么读这么大的代码
- 从一个具体问题出发,只追一条路径。 比如"
read()怎么拿到文件数据",沿着第 3.4 节那条调用链往下走,旁边的分支先不看。 - 先找 xv6 的对应代码当地图(OVERVIEW 的主题地图里有每个 lab 的对照)。
- 工具:
- 在线交叉引用:elixir.bootlin.com,点一个符号就能看所有定义和引用;
- 本地:
make compile_commands.json(或scripts/clang-tools/gen_compile_commands.py)之后用 clangd 跳转; git log -p 文件、git blame:看一段代码为什么写成这样,提交说明往往比注释更有用。
- 让它跑起来看:用 vng / QEMU 起本机编出的内核,gdb 加载
vmlinux打断点;或者在宿主机上用 ftrace(/sys/kernel/tracing)、bpftrace、perf观察真实调用(都需要 root)。 - 读
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 周一个版本,按子系统分层维护,补丁走邮件列表 |
自测:不看笔记能不能做到
- 从八个角度里挑三个,各用一句话说内核是什么。
- 列出 CPU 进入内核的三种方式,并说出哪一种里不能睡眠、为什么。
- 画出
read(fd)从系统调用到具体文件系统的调用链,指出多态发生在哪一步。 fork和pthread_create在task_struct层面的区别是什么?- 说出两个"机制和策略分离"的例子。
思考题(先自己答)
- 中断处理程序为什么不能调用
mutex_lock(),却可以调用spin_lock()?如果中断处理程序要拿的自旋锁,正好被它打断的那段代码拿着,会发生什么?内核用什么办法避免? - 本机有 135 个
kworker线程,却只有 16 个 CPU。为什么需要这么多? - 一个可加载模块和编进内核的代码,在权限上有区别吗?那模块签名(
CONFIG_MODULE_SIG)防的是什么? - eBPF 程序也在内核里运行,为什么它就"安全",而模块不安全?验证器拒绝了哪些东西?
- 为什么说
/proc/self/maps里的每一行,对应的是内核里的一个vm_area_struct,而不是一组页表项?两者的关系是什么?(提示:05 的懒分配。) - 微内核里一次
read()文件大概要几次 IPC?宏内核里呢?