独占
你的程序有一个从没被质疑过的信念:这台机器是我的。CPU 是我的——我写的循环从第一条指令跑到最后一条,中间没有断过。内存是我的——我的 main 在 0x400000,我的栈在 0x7fff 那一带,从来没人跟我抢。
这些全部是假的。你的循环每秒被打断上千次,只是每次都被完好地放了回去,所以你察觉不到。你那个 0x400000 和我机器上那个 0x400000,在物理内存里隔着十万八千里——而你这辈子一个真实的物理地址都没碰过。
操作系统的全部工作,就是维持这个骗局。这本书把它一层层拆开:那道线(凭什么有些指令你不能执行)、分身(你以为你独占 CPU)、幻境(你以为你独占内存)、万物(你以为你在读文件)。最后交给你一张价目表——每一次跨越那道线,到底要付多少钱。
而且它是活的:书里四台招牌引擎——四级页表地址翻译器、真 malloc 分配器、CFS 调度器、fork 解释器——是用 JavaScript 真写出来、就在这个网页里跑的。你输一个虚拟地址,它真的按 9|9|9|9|12 切开、一级级走下去;你改一行 C 代码,它真的解析、真的分裂出进程树。正文里每一个数字,都是这些引擎当场算的。
这本书为什么不从「什么是进程」讲起
因为那句话你早就背过了——「进程是资源分配的基本单位,线程是 CPU 调度的基本单位」。背得出来,然后呢?然后你依然说不清下面这些事:
- 为什么服务器 load average 是 15,
top里 CPU 却几乎空闲? - 为什么
rm掉那个巨大的日志文件之后,df显示磁盘一点没变空? - 为什么
malloc申请 100 GB 会成功,而进程却在后面某一行写内存时被杀掉? - 为什么有的进程
kill -9都杀不掉? - 为什么限制了 0.5 核的容器里,JVM 兴高采烈地开了 16 个 GC 线程?
- 为什么同一段代码,有时候 1 微秒,有时候 3 毫秒?
这些问题看起来互不相干,属于运维、属于调优、属于「玄学」。但它们全都是同一件事的不同侧面——而这本书讲的就是那件事。
操作系统的全部工作,是维持一个骗局:让每个进程都以为自己独占整台机器。
这个骗局有三重。时间上:你以为你独占 CPU,其实你被切成了片,每秒被抢走上千次控制权(卷 II)。空间上:你以为你独占内存,其实你写的每一个地址都是假的,要经过四级页表翻译才落到真的物理内存上(卷 III)。接口上:你以为你在读一个文件,其实你只是拿到了一个号码,真正的活儿全在线的另一边(卷 IV)。
而维持骗局是要花钱的。每一次你跨过那道线——一次系统调用、一次缺页、一次上下文切换——都在付费。这本书的最后一章,是一张完整的价目表。读懂了它,上面那六个问题会同时变得显然。
三件这本书坚持做到的事
颜色分两侧,一眼看见你在哪一边
全书守一条视觉规则:用户态 = 蓝,内核态 = 砖红。这不只是装饰——正文里每一段 C 代码,系统调用都会被自动染成砖红。于是你扫一眼就知道:这一行是在自己家里算数,还是要敲门下去求人。
int fd = open("data.txt", O_RDONLY); // 砖红 = 要过线
size_t n = strlen(buf); // 普通色 = 纯用户态,内核不知道
write(1, buf, n); // 砖红 = 又过一次线
这个习惯一旦养成,你看任何代码都会自动估出它的「过线次数」——而那往往就是性能的第一决定因素。
每台机器都是真的
这本书里的关键机制,不是画个示意图给你看,是用真代码写出来在浏览器里跑的:
- 四级页表地址翻译器(第 11 章):你输一个虚拟地址,它用 BigInt 真的按
9|9|9|9|12切开,逐级查表,检查 W/U/NX 权限位,走 TLB 的 LRU 淘汰。缺页时它真的分配一页再重试。 - 真 malloc 分配器(第 14 章):真的维护空闲链表、真的按 16 字节对齐、真的分裂与合并。同一段分配序列,first-fit 要向内核要两次内存、利用率 30.5%;best-fit 只要一次、利用率 61.0%——这两个数字是它当场算的,不是我编的。
- CFS 调度器(第 8 章):用的是 Linux 内核
sched_prio_to_weight[]里一模一样的 40 个权重值。跑完你会看到 nice 差 1 的两个任务拿到 55.5% 和 44.5%——比值 1.249,正是内核那张表设计的 1.25 倍。 - fork 解释器(第 6 章):一个 C 子集的词法分析器 + 递归下降解析器 + 会分裂的求值器。代码框可以改——把循环从 3 次改成 4 次,进程数从 8 变成 16。
每一章都回到你每天写的代码
讲完页表要能解释「为什么 Redis 的 bgsave 内存会翻倍」,讲完 inode 要能解释「为什么删了日志磁盘没变空」,讲完 namespace 要能解释「为什么容器里的 JVM 数错了 CPU 核数」。每一章末尾都有一节 ↑ 回到应用层,把这一章的机制接回一个你可能真的遇到过的现象。
《同时》讲并发编程怎么写——协程、事件循环、锁、异步模型;这本书讲那台跑着它们的机器。两本书在「调度」和「epoll」上会碰头,各讲一侧:这里讲内核的数据结构,那里讲你的编程模型。
《开门》讲插件系统的沙箱与信任边界;这本书第 2 章讲的 ring 边界,是同一个问题在硬件上的最原始答案——第 22 章的容器则是它在今天的样子。
《当真》讲 PostgreSQL 怎么保证数据不丢;这本书第 18 章会告诉你,它靠的那个 fsync 到底贵在哪里。
这本书写给
- 写了多年应用层(Android/服务端/前端),但「进程、虚拟内存、系统调用」始终停在面试背诵阶段的人
- 能查出「是 IO 慢」,但说不清慢在哪一层、该往哪儿使劲的人
- 用 Docker 用得很熟,却从没搞懂它凭什么这么轻的人
- 想读《Operating Systems: Three Easy Pieces》或 CSAPP,但需要有人先把地图画出来的人
这本书不打算
- 教你写一个内核(那是 xv6 和 OSTEP 的活儿,读完这本再去正合适)
- 逐行讲 Linux 源码(这里讲机制与代价,不讲
struct task_struct有多少字段) - 覆盖每一个系统调用(讲透十几个,胜过列举三百个)
- 重讲并发编程模型(那是《同时》,这里只讲内核这一侧)
读完之后你会有的东西
一张价目表,和一个新习惯:看见任何一段代码,先问「它在这张表的第几行」。
这张表从 L1 缓存命中的 1 纳秒,一直排到机械盘寻道的 8 毫秒,跨了七个数量级——最上面那行和最下面那行的比例,相当于一秒钟和四个月。当你知道一次系统调用值 100 纳秒、一次上下文切换值 3 微秒、一次 fsync 值 1 毫秒,很多「性能玄学」会当场失去神秘感:你优化掉一万次纯计算省下的 10 微秒,会被随手多写的一次 fsync 一口吃光,还倒欠 990 微秒。
更重要的是,你会开始看见那道线。它一直在那儿,你的每一行代码都活在它上面。