RING · PROCESS · MEMORY · FILE · 一台维持骗局的机器

独占

O P E R A T I N G S Y S T E M

你的程序有一个从没被质疑过的信念:这台机器是我的。CPU 是我的——我写的循环从第一条指令跑到最后一条,中间没有断过。内存是我的——我的 main0x400000,我的栈在 0x7fff 那一带,从来没人跟我抢。

这些全部是假的。你的循环每秒被打断上千次,只是每次都被完好地放了回去,所以你察觉不到。你那个 0x400000 和我机器上那个 0x400000,在物理内存里隔着十万八千里——而你这辈子一个真实的物理地址都没碰过

操作系统的全部工作,就是维持这个骗局。这本书把它一层层拆开:那道线(凭什么有些指令你不能执行)、分身(你以为你独占 CPU)、幻境(你以为你独占内存)、万物(你以为你在读文件)。最后交给你一张价目表——每一次跨越那道线,到底要付多少钱。

而且它是活的:书里四台招牌引擎——四级页表地址翻译器真 malloc 分配器CFS 调度器fork 解释器——是用 JavaScript 真写出来、就在这个网页里跑的。你输一个虚拟地址,它真的按 9|9|9|9|12 切开、一级级走下去;你改一行 C 代码,它真的解析、真的分裂出进程树。正文里每一个数字,都是这些引擎当场算的。

24
6
22个真引擎 Demo
4台招牌引擎:页表 · 分配器 · 调度器 · fork

这本书为什么不从「什么是进程」讲起

因为那句话你早就背过了——「进程是资源分配的基本单位,线程是 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 核数」。每一章末尾都有一节 ↑ 回到应用层,把这一章的机制接回一个你可能真的遇到过的现象。

这本书写给

  • 写了多年应用层(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 微秒。

更重要的是,你会开始看见那道线。它一直在那儿,你的每一行代码都活在它上面。