操作系统基础深度解析
从内核态/用户态出发,把进程、虚拟内存、文件系统、I/O 栈、系统调用与调度器一次讲透。每一节都给出可在终端验证的命令和量级数字。
打开一个终端,敲下 cat hello.txt。在你按下回车的那一瞬间,发生了至少七层下钻:bash 解析命令 -> fork+execve 启动 cat 进程 -> 进程的内存被分配虚拟地址空间 -> cat 调用 read() 触发 syscall -> CPU 切到内核态 -> VFS 转发到 ext4 -> 块层给 NVMe 排好请求 -> 硬件通过 DMA 把字节送回 -> 中断把控制权交还给 cat -> 字节经过页缓存复制到用户缓冲区 -> 屏幕上出现内容。
整个过程大概 100 微秒,你看到的只是最后那一帧。这篇文章的目标,是把这条路径上的每一层都拆开,让你在遇到性能问题、奇怪的权限问题、或者「为什么我的程序卡在 D 状态」时,能直接定位到是哪一层出了事。
读完你能做到
- 准确说出 monolithic 与 microkernel 的真实差异,以及为什么 Linux 至今没有走 microkernel 路线。
- 看到一个
ps输出里的D/S/R/Z,知道每个状态对应进程生命周期的哪一格、要等什么事件才能离开。 - 理解一次「进程访问一个变量」背后 MMU/TLB/页表/page fault/swap 的完整链路,并能算出每一级的数量级开销。
- 看明白
ls -li输出里 inode、nlink、blocks 三列分别在说什么,能解释rm后空间没释放的原因。 - 把一次
read()系统调用从 glibc 一路追到 NVMe 队列,并能定位「为什么我的 IO 慢」是在哪一层。 - 比较 FCFS / SJF / RR / CFS 四种调度策略的优劣,理解 Linux 为什么选 CFS 而不是 RR。
前置
- 至少写过一些 C 或者用过 Python 调过 OS 接口(
os.read、os.fork之类)。 - 用过 Linux,知道
top、ps、strace、/proc大致是干嘛的。 - 不要求看过《操作系统:三只小猪》,但看过会更顺。
1. 操作系统到底在做什么
操作系统(Operating System,OS)这个名字其实有点误导人——它听上去像一个「系统」,实际上是一组解决多人共用一台机器这件事的代码集合。如果一台机器上永远只跑一个程序、永远只有一个用户、硬件永远不会出错,OS 大部分代码都可以删掉。OS 之所以复杂,是因为现实里:
- CPU 只有几十个核,但要跑成百上千个进程 -> 需要调度器。
- 物理内存只有几十 GB,但每个进程都希望「我独占整台机器的地址空间」 -> 需要虚拟内存。
- 硬件型号上千种,但应用程序不想知道 NVMe 和 SATA 的区别 -> 需要驱动 + VFS 抽象。
- 多个进程要共享文件、内存、socket,但不能互相破坏 -> 需要权限 + 隔离。
把这四件事记住,下面所有章节都能挂在它们底下。我们会按「最贴近 CPU 的部分先讲」的顺序展开:内核架构 -> 进程 -> 内存 -> 文件 -> I/O -> 系统调用 -> 调度。
2. 内核架构:单内核 vs 微内核

「内核态」和「用户态」是 CPU 提供的硬件特性(x86 上是 ring 0 和 ring 3)。在内核态可以执行特权指令、直接访问任意物理内存、控制中断。用户态不行——你的浏览器、数据库、bash,全部跑在 ring 3,想访问硬件只能通过内核借。
这一刀切下去之后,内核里到底应该塞多少东西?这就是单内核(monolithic)和微内核(microkernel)的核心分歧。
2.1 单内核:什么都塞进去
Linux、FreeBSD、Windows NT(实际上是 hybrid)都属于这一派。调度器、内存管理、文件系统、TCP/IP 栈、设备驱动全部住在内核地址空间里,互相直接调函数,不需要走任何隔离机制。
- 优点:快。 文件系统模块要给网络栈传数据,就是一次普通的函数调用,没有任何边界开销。
- 缺点:脆弱。 任何一个驱动崩溃 = 整个内核 panic。Linux 内核里 70% 的代码是驱动,新硬件就是新风险。
- 缓解办法:模块化。 Linux 把驱动做成可加载模块(
*.ko),运行时insmod/rmmod。但模块本身仍然在内核地址空间里,bug 一样能搞死内核——只是降低了编译和发布耦合。
2.2 微内核:只留必需的
Mach、L4、seL4、QNX 走的是另一条路。内核里只留进程间通信(IPC)、调度、最低限度的内存管理,文件系统、网络栈、驱动全都做成普通用户态进程。
- 优点:隔离。 网卡驱动崩溃 = 一个用户进程崩溃,重启它即可,内核不动。seL4 甚至做了形式化证明:在它的规约下,内核保证不会崩溃。
- 缺点:慢。 一次「打开文件」原本是一个函数调用,现在变成「应用 -> 内核 IPC -> 文件服务进程 -> 内核 IPC -> 块设备服务进程」,每一跳都是一次上下文切换。
- 现实使用: QNX 在汽车、医疗、工业控制里跑了几十年;macOS / iOS 的 XNU 是 Mach 微内核 + BSD 单内核拼出来的「混合内核」;纯微内核在通用桌面 / 服务器上仍然是少数派。
2.3 为什么 Linux 没换成微内核
1990 年 Tanenbaum 和 Linus 的著名论战里,Tanenbaum 说「Linux 用单内核已经过时了」。三十多年过去,Linux 仍然是单内核。原因不浪漫:
- 性能数字说话。 微内核的 IPC 开销(即使在 L4 这种极致优化的实现上)也比函数调用大一个数量级。
- 生态惯性。 全世界几十万个 Linux 内核驱动重写一遍是不可能的。
- 「足够安全」的中间路线。 内核地址空间随机化(KASLR)、模块签名、eBPF 把不可信代码沙箱化、用户态文件系统(FUSE)等手段,让单内核也能在大部分场景下做到「驱动出 bug 不直接 panic」。
记住这张图后,每次看到内核 oops 或 BSOD,你就知道:那是单内核的代价。每次看到一个驱动崩溃却没拖垮系统(比如手机 wifi 模块自己重启),那很可能是微内核思想在某个层面被借用了。
3. 进程与状态机
进程是 OS 给「正在运行的程序」记账的单位。每个进程都有自己的:地址空间、文件描述符表、信号处理表、CPU 上下文(寄存器)、调度状态。当 OS 决定让一个进程下 CPU、让另一个进程上 CPU 的时候,就是把前者的寄存器存到内存里、把后者的寄存器从内存里恢复——这叫上下文切换,每次大概 1~5 微秒。
3.1 五状态生命周期

任何时刻,每个进程都恰好处于这五个状态之一:
| 状态 | Linux ps 列里的字母 | 含义 | 离开的事件 |
|---|---|---|---|
| NEW | (短暂) | 正在创建(fork 和 execve 之间) | 内核完成进程结构,转 READY |
| READY | R (Runnable) | 万事俱备,等 CPU | 调度器选中 -> RUNNING |
| RUNNING | R (实际占着 CPU) | 正在执行用户代码或内核代码 | 时间片用完 / 调用阻塞 syscall / 退出 |
| BLOCKED | S (可中断) / D (不可中断) | 在等某个事件(I/O、锁、子进程) | 事件到达 -> READY |
| TERMINATED | Z (Zombie) | 跑完了但 PCB 还在等父进程 wait() | 父进程读走退出码 -> 完全消失 |
值得专门记住两件事:
- READY 和 RUNNING 在 ps 里都是
R。 Linux 把「能跑」和「正在跑」合并显示,因为时间分辨率太短,区分意义不大。 D状态杀不掉。kill -9都没用。这是因为进程正在内核里等一个不能取消的硬件操作(比如 NFS 读写卡死)。看到大量D进程,第一个怀疑对象是底层存储 / 网络。
3.2 上下文切换的真实开销
| |
一次切换的直接成本约 1~5 微秒(保存/恢复寄存器、切页表、更新调度统计),间接成本是 缓存污染——新进程的工作集要重新加载到 L1/L2 cache,可能再多花几微秒到几十微秒。所以线程数远多于核心数几乎一定会变慢,不是因为 CPU 算不过来,而是因为切换在烧时间。
3.3 进程 vs 线程 vs 协程
这三个词每天都在被混用。一句话区分:
- 进程:独立地址空间。隔离最好,创建最贵(fork 要复制页表)。
- 线程:同一地址空间。共享内存最方便,但要自己处理同步。Linux 上线程其实就是「共享更多东西的进程」,由
clone()系统调用控制共享什么。 - 协程:用户态调度。OS 完全看不到,没有 syscall 开销,但需要程序自己交出 CPU(
await)。一台机器上可以轻松跑几十万个协程。
| 维度 | 进程 | 线程 | 协程 |
|---|---|---|---|
| 调度者 | OS 内核 | OS 内核 | 用户态运行时 |
| 切换成本 | ~5 us | ~3 us | ~100 ns |
| 内存隔离 | 强 | 无(同进程内) | 无 |
| 适合场景 | 多服务、安全边界 | 计算密集 + 共享内存 | 高并发 I/O |
4. 虚拟内存:每个进程都以为自己独占机器

直接让进程访问物理地址有三个无解的问题:
- 冲突。 两个程序都想用 0x1000,谁让谁?
- 隔离。 进程 A 有没有办法读到进程 B 的密码?
- 超额。 32 个进程每个想用 4 GB,机器只有 16 GB,怎么办?
虚拟内存把所有问题一起解决:每个进程拿到一份自己的虚拟地址空间(64 位下名义上 256 TB),由 MMU 硬件 + 内核维护的页表翻译成物理地址。
4.1 翻译流程
地址按页(page,通常 4 KiB)切分。一个虚拟地址 = [VPN | offset],VPN 经过页表映射成 PFN(physical frame number),物理地址 = [PFN | offset]。
- TLB(Translation Lookaside Buffer): MMU 上的小缓存,通常几百到几千项。命中是 1 个 cycle,没命中要走页表(4 级页表 = 4 次内存访问 ~= 100 cycles)。
- 多级页表: 64 位下如果用单级页表,光页表本身就要占数 PB。Linux 现在用 5 级页表(PML5),按需展开,只为真正使用的虚拟地址范围分配页表项。
- 页表项里的标志位: R/W/X(权限)、Present(是否在物理内存里)、Dirty(写过没)、Accessed(最近访问过没)、User/Supervisor。这些位是所有内存安全和性能优化的基础——COW、mmap、swap、KSM 全都靠它们。
4.2 page fault 路径
如果 PTE 的 Present 位是 0,CPU 直接抛 page fault 异常,跳到内核处理。三种典型情况:
- Minor fault(按需分配): 进程
malloc了 1 GB 但还没真正写过——内核此刻才真给它分配物理页,并把 PTE 写好。 - Major fault(从 swap 调入): 那一页之前被换到了磁盘上的 swap 空间,要把它读回内存。这是慢动作,10 ms 量级。
- Segfault(非法访问): 那一页根本不属于这个进程的合法地址范围 -> 发 SIGSEGV,进程多半挂掉。
| |
maj_flt 持续增长 = 在频繁换页,性能会塌方。这是「机器在 swap」的硬指标,比看 free 列直观得多。
4.3 一些每天都用、但你可能没意识到是虚拟内存在干活的功能
fork()的 Copy-on-Write: fork 不真的复制 4 GB,而是把父子的所有页都标记为只读 + 共享,谁先写谁触发 fault,到时再复制那一页。mmap()把文件映射成内存: 你以为在读内存,其实 OS 在按需读磁盘。/usr/bin/python这种共享库,所有 Python 进程都映射到同一份物理页上。- OOM killer: 当物理 + swap 也不够用时,内核挑一个「分数最高」的进程杀掉。
/proc/<pid>/oom_score决定谁倒霉。 - 大页(huge pages): 把页大小从 4 KiB 改到 2 MiB 或 1 GiB,TLB 覆盖范围放大 512 倍。数据库(PostgreSQL、Redis)经常配。
数量级要记住:
| 事件 | 时间 | 相对慢多少 |
|---|---|---|
| L1 cache 命中 | 1 ns | 1x |
| TLB 命中 + L1 命中 | ~1 ns | 1x |
| TLB miss + 页表遍历 | ~30 ns | 30x |
| RAM 访问 | ~100 ns | 100x |
| page fault(minor,无 I/O) | ~1 us | 1,000x |
| page fault(major,从 SSD swap) | ~80 us | 80,000x |
| page fault(major,从 HDD swap) | ~10 ms | 10,000,000x |
「慢」是有量级的。性能优化的第一步永远是判断你在跟哪个量级打交道。
5. 文件系统:名字、元数据、数据是三件不同的事

很多人以为「文件 = 一段字节 + 一个名字」。这是用户视角。在内核里它分成三层:
- 目录项(dirent): 名字 -> inode 号的映射。目录就是「特殊的文件」,里面装着一堆 dirent。
- inode: 文件的元数据 + 指向数据块的指针。没有名字。
- 数据块(block): 真正存字节的地方,通常 4 KiB 一块。
把这三层分开,是 Unix 文件系统的设计精髓。它直接解释了一堆原本看着诡异的现象:
5.1 为什么 mv 在同一个分区里是瞬时的?
mv a.txt b.txt:只是把目录里 a.txt -> ino 42 这条 dirent 改成 b.txt -> ino 42。inode 没动,数据块更没动。零字节复制。跨分区就不行了,因为另一个文件系统的 inode 表是独立的,必须真复制。
5.2 为什么 rm 不一定真删数据?
rm 做的是 (1) 删 dirent (2) nlink--。当 nlink 减到 0 且 没有任何进程还打开着这个文件时,inode 才真正被释放,数据块才被标记为 free。
| |
这是一个常见的「磁盘满了但找不到大文件」的根因:日志被 logrotate 删了,但应用还开着 fd 在写。重启那个应用 = 释放空间。
5.3 硬链接 vs 软链接
- 硬链接 (
ln a b): 在同一个 inode 上挂多个名字。nlink加一。删任一个名字其他都还在。不能跨分区(inode 号在分区内才唯一)、不能链接目录(避免成环)。 - 软链接 (
ln -s a b): 一个独立的 inode,里面只存目标的路径字符串。可以跨分区、可以指向不存在的目标(dangling link)、被指目标删了它就坏了。
| |
5.4 块指针:为什么 inode 大小固定,但能存超大文件?
经典 Unix inode 里有 12 个直接块指针(直接指数据块)+ 1 个一级间接 + 1 个二级间接 + 1 个三级间接。块大小 4 KiB 时:
- 直接:12 * 4 KiB = 48 KiB
- 一级间接:(4 KiB / 4 B) * 4 KiB = 4 MiB
- 二级间接:~4 GiB
- 三级间接:~4 TiB
绝大多数文件 < 48 KiB,所以小文件直接命中、零额外读。大文件付出 1~3 次额外 I/O 就能定位到任意 4 KiB。这是「为常见情况优化」最经典的例子之一。
现代文件系统(ext4、xfs、btrfs、ZFS)改用 extent(连续区间)替代「指针指向单块」,进一步减少元数据开销,但「inode 是元数据 + 块定位结构」的核心不变。
5.5 VFS:把所有「文件状的东西」都叫文件
Linux 在文件系统之上加了一层 VFS(Virtual File System),让不同的文件系统、网络挂载、伪文件系统都暴露同一套 open/read/write/seek/close 接口。这就是为什么你可以:
cat /proc/cpuinfo读 CPU 信息(procfs,纯虚构的文件)cat /sys/class/net/eth0/address读 MAC 地址(sysfs)echo mem > /sys/power/state让机器睡眠- 用 FUSE 把 SSH 远端目录挂载成本地路径
「一切皆文件」不是哲学口号,是 VFS 把它实现出来的工程结果。
6. I/O 子系统:一次 read() 下钻七层

read(fd, buf, 4096) 看起来是一行代码,背后是一个七层下钻 + 一次中断回调。我们按层走一遍:
- 应用层。 调 glibc 的
read()包装函数。 - 系统调用接口。 包装函数把寄存器准备好,执行
syscall指令陷入内核。CPU 切到 ring 0,跳到entry_SYSCALL_64入口。 - VFS。 内核根据 fd 找到
struct file *,按它指向的file_operations.read派发到具体文件系统。 - 具体文件系统(ext4)。 把文件偏移翻译成块号(走 inode 的 extent 树)。
- 页缓存(page cache)。 先看这块是不是已经在内存里。命中就直接
copy_to_user(buf)返回,整个流程在几微秒内结束。这是 99% 的热数据走的路径。 - 块层。 没命中,构造一个 BIO(块 I/O 描述符),交给块层调度。块层会合并相邻 BIO(电梯算法 / mq-deadline / kyber),减少机械硬盘的寻道。
- 设备驱动。 NVMe 驱动把 BIO 翻译成 NVMe 命令,提交到硬件队列(submission queue)。
- 硬件。 SSD 控制器执行命令,通过 DMA 把数据直接写到内核分配好的物理页里(不走 CPU),完成后触发中断通知 CPU。
中断处理把数据复制到用户 buf,唤醒之前 sleep 的进程。控制权回到 read() 调用者。
6.1 为什么这么多层
每一层都对应一个抽象:
| 层 | 抽象的是 |
|---|---|
| 系统调用接口 | 用户态/内核态权限边界 |
| VFS | 不同文件系统的 API 差异 |
| 文件系统 | 字节流 -> 磁盘块的映射 |
| 页缓存 | 慢设备 + 快内存的速度差 |
| 块层 | 重排请求以优化设备 |
| 驱动 | 不同硬件的命令集差异 |
少了哪一层,应用程序都要自己解决那个抽象。这就是为什么直接绕过 OS 写一个数据库引擎并不会比走 OS 快——OS 的这些优化都是几十年沉淀下来的。
6.2 同步、异步、io_uring
经典 read() 是阻塞同步:调用者一直挂着等数据。这对一个进程一次只做一件 I/O 的程序很合适,但对要同时管 1 万个连接的服务器来说就挂掉了——每个连接一个线程?10000 个线程的上下文切换会把 CPU 跑冒烟。
历代解法:
select/poll/epoll: 一个线程同时盯多个 fd,谁就绪了通知。只能告诉你「现在可以发 read 了」,read 本身仍然是同步。aio: Linux 早年的真正异步 I/O,但只支持 direct I/O,bug 很多,没真正流行起来。io_uring(Linux 5.1+): 现代答案。用户态和内核共享两个环形队列(提交 + 完成),单次 syscall 可以提交几十个 I/O,内核完成后写回完成队列,用户态自己来取。系统调用次数指数级下降,是高性能 I/O 库(QUIC 服务器、新一代数据库)的首选。
6.3 「慢 IO」的诊断顺序
| |
从应用追到设备,定位「慢」是哪一层带来的。常见误区:看到 CPU 不忙就以为系统闲着——其实可能整台机器都在等 I/O,vmstat 的 wa 列才是真相。
7. 系统调用:用户态唯一合法的求救信号

用户态程序想做任何「特权」操作(读文件、发包、申请新内存、创建进程),都必须通过系统调用。这是操作系统能保证安全的根本机制——CPU 在 ring 3 时根本执行不了某些指令,唯一切到 ring 0 的方式就是 syscall / int 0x80 / sysenter 这类陷阱指令。
7.1 一次 syscall 的步骤
- C 库包装。 应用代码里的
read(fd, buf, n)是 glibc 的普通函数。 - 寄存器准备。 glibc 把 syscall 号放进
rax(Linux x86_64 约定),参数放进rdi、rsi、rdx、r10、r8、r9。 syscall指令。 CPU 把当前 RIP/RSP/EFLAGS 保存到 MSR,切换到 ring 0,跳到内核早就注册好的入口entry_SYSCALL_64。- 派发。 内核以
rax为下标查sys_call_table,跳到对应的内核函数(如sys_read)。 - 执行。 走 VFS、文件系统、块层 ……(见上一章)。
- 返回。 结果写到
rax,执行sysret切回 ring 3。
7.2 syscall 的真实开销
一次「空 syscall」(什么都不做的 getpid())在现代 CPU 上大约 100 ns。看上去不多,但相对来说:
- 普通函数调用: ~1 ns。syscall 比函数调用慢 100 倍。
- 缓存影响: mode 切换会污染 L1/L2,下面几百个指令的 IPC 会下降。
- Spectre/Meltdown 缓解(KPTI): 切换时要刷 TLB,开销又被推高 30~50%。
这就是为什么所有高性能 I/O 库都在批量化 syscall:sendmmsg 一次发多个包、io_uring 一次提交多个 I/O、vmsplice 直接搬页表。本质都是「摊薄那 100 ns」。
7.3 用 strace 看 syscall
| |
写性能问题的诊断里,strace -c 通常 30 秒内告诉你瓶颈在哪——如果 read 占 80%,问题在 I/O;futex 占 80%,是锁争用;epoll_wait 占 80%,那大概率是健康的(在等事件)。
8. 调度器:四种策略,同一组任务

调度器决定「下一刻 CPU 让谁跑」。用同样的三个任务(P1 burst=8、P2 burst=4、P3 burst=2,依次到达)跑一遍四种经典策略,对比直观:
8.1 FCFS — 先来先服务
谁先来谁先跑,跑完才让位。实现一行代码。问题叫护航效应(convoy effect):一个 CPU-bound 长任务卡在前面,后面所有短任务都饿着。上图里 P3 等到 t=12 才跑,平均等待时间 5.67。
适用场景:批处理后台、单任务系统。绝不用于交互式系统。
8.2 SJF — 最短作业优先
每次从就绪队列里挑剩余 burst 最短的。理论上平均等待时间最优。问题有两个:
- 谁知道未来 burst 多长? 不知道。只能用历史平均估计(Linux 早年的 nice + 老化算法)。
- 饥饿。 长任务可能永远等不到——只要短任务源源不断进来。
8.3 Round Robin — 时间片轮转
每个进程跑固定时长(quantum,通常 10~100 ms),到点就强制换下一个。没有饥饿、响应均匀——这就是早期分时操作系统能让二十个人共用一台机器的原因。
调参的核心是 quantum:
- 太小(< 1 ms):上下文切换占比过高,吞吐塌方。
- 太大(> 1 s):交互响应卡顿,长任务又把 CPU 霸占了,退化成 FCFS。
上图 quantum=2 时平均等待 4.33。
8.4 CFS — Linux 现役
Linux 从 2.6.23 开始用 CFS(Completely Fair Scheduler)。核心思想:给每个进程记一个 vruntime(虚拟运行时间),永远调度 vruntime 最小的那个。vruntime 按 nice 值加权——nice 高的进程 vruntime 涨得快,于是被调度的频率低。
实现细节:
- 用红黑树按 vruntime 排序,pick 最小是 O(log n)。
- 没有固定 quantum,而是「目标时延 / 进程数」动态分配。8 个 runnable 进程、目标 6 ms -> 每人跑 0.75 ms。
- I/O-bound 进程因为大部分时间在 sleep,vruntime 涨得慢,自动获得高优先级——这正是「交互式优先」的天然实现,不需要专门分类。
上图 CFS 的等待时间最低(4.0)且最均匀。短任务 P3 1 单位等待就完成了。
2024 年 Linux 6.6 引入了 EEVDF(Earliest Eligible Virtual Deadline First),是 CFS 的改进版,更适合现代 NUMA + 大量小任务的工作负载。思想一脉相承,工程细节不同。
8.5 实时调度
CFS 是「公平调度」,对实时任务(音视频、机器人控制、汽车)不够。Linux 还有:
SCHED_FIFO: 严格优先级,最高优先级的实时任务一上来就霸占 CPU 直到自己 sleep / 让出。SCHED_RR: 同优先级用 RR 切换。SCHED_DEADLINE(3.14+): 给任务声明 (runtime, deadline, period),内核保证在 deadline 前给到 runtime。基于 EDF 算法。
| |
9. 把 7 张图串成一次完整的 cat hello.txt
回到开头那个 cat hello.txt,用学过的所有零件复述一遍:
- bash 解析命令 -> 调用
fork():内核给新进程分配 PCB、复制虚拟地址空间(COW,所以瞬时完成)。父子都从 fork 返回。 - 子进程调
execve("/bin/cat", ...):清掉自己当前的虚拟地址空间,重新 mmap/bin/cat的代码段、加载动态链接器、启动 main()。这一步发生了 N 次 minor page fault——cat 的代码页是按需分配的。 - cat 调用
open("hello.txt")-> syscall(用户态切内核态,约 100 ns) -> VFS -> ext4 -> 找到 dirent 「hello.txt -> ino 1234」 -> 读 inode 1234 进内存 -> 返回 fd。 - cat 调用
read(fd, buf, 4096)-> syscall -> VFS -> ext4:把文件偏移 0 翻译成 LBA 19528 -> 查页缓存:未命中 -> 构造 BIO -> 块层调度 -> NVMe 驱动 -> 硬件 DMA 把 4096 字节搬到内核页。 - 中断回调 -> 唤醒 cat(从 BLOCKED 切回 READY) -> 调度器(CFS)选中它 -> 上下文切换 -> cat 在内核里执行
copy_to_user(buf, page, 4096)-> 返回用户态。 - cat 调用
write(1, buf, n)-> syscall -> VFS -> /dev/pts/0(伪终端) -> tty 子系统 -> 终端模拟器 -> 屏幕。 - cat 调用
_exit(0)-> syscall -> 内核回收资源、把退出码塞 PCB、转 ZOMBIE -> bashwait()收尸。
整个过程涉及:4 次主要 syscall、若干次 page fault、一次 NVMe 完整 I/O、若干次上下文切换、CFS 几次调度决策。每一步都对应了一张图。 这就是「理解操作系统」的真正含义——不是背概念,而是看一个具体动作,能说出底层每一步在干什么。
10. 接下来读什么
按「越来越底」的顺序:
- 入门: Operating Systems: Three Easy Pieces(OSTEP,免费在线,强烈推荐)。
- Linux 内核细节: Understanding the Linux Kernel(旧但经典);新一点用 Linux Kernel Development (Robert Love)。
- 性能视角: Brendan Gregg 的 Systems Performance 和他的网站,是当代性能工程的圣经。
- 微内核 / 形式化: seL4 的论文 seL4: Formal Verification of an OS Kernel。
- 想动手: xv6(MIT 6.S081 的教学 OS),代码 1 万行就能跑起来一个真正的多进程内核。
- Linux 子系统专题: 内核源码树里
Documentation/是被严重低估的资料库;/proc和/sys是活的文档。
「操作系统」这个领域的好处是它的概念五十年没大变。你今天学的进程模型、虚拟内存、文件系统抽象,回到 1985 年的 BSD 同样适用,到 2050 年大概率还在。投入回报极高的一笔学习。