你每天都在用操作系统,但你大概说不清它到底在做什么。
这不怪你。“管理硬件资源"这种说法等于没说——它把一个非常具体、非常聪明的东西,说成了一句听不出内容的口号。
这门课换一种说法。操作系统只做三件事:
把一个 CPU 变成很多个——你的机器有 10 个核,却能同时跑几百个进程,每个进程都以为 CPU 是自己的。
让很多人同时动同一份东西而不打架——两个线程同时给一个计数器加一,结果必须是加了二。
把断电就消失的东西留下来——内存断电即空,你却指望文件明天还在。
这就是虚拟化、并发、持久化三支柱。这门课的三大部分,就是这三句话各自的展开。
一个反复出现的手法
三支柱看起来是三件不相干的事。但操作系统解决它们,用的是同一招——这一招你会在这门课里见到十几次:
加一层中间人,然后在这一层里撒一个善意的谎。
虚拟内存这一层告诉每个进程"这块内存是你的”(谎言:它是共享的,随时可能被换到磁盘上)。进程这一层告诉每段程序"CPU 是你的"(谎言:它每几毫秒就被抢走一次)。文件这一层告诉你"你在往硬盘写"(谎言:数据还躺在内存里,一断电就没了)。
这些谎言不是缺陷,是设计。没有它们,你写的每一行代码都得知道机器上还有谁在跑。
但每个谎言都要付两笔钱,这门课有一半篇幅在讲这两笔:
- 中间人本身要花时间。 每次访问内存都要查页表,这个开销必须被消掉——第 13 篇的 TLB 就是干这个的。
- 谎言总会在某个地方漏出来。 你的程序变慢了、缓存失效了、断电后文件坏了——都是同一个谎在某处漏了。
⭐ 这门课真正教的,是识别每一层的谎言,以及知道它在哪里漏。 记住机制的名字是最容易的部分。
这门课不讲什么
- 不罗列考点。 不会有"进程与线程的五点区别"这种表格。
- 不讲某个具体系统的用法。 不是 Linux 运维手册,
systemctl一次都不会出现。 - 不假装每件事都有标准答案。 调度、置换、一致性这些地方,工业界至今在换方案。哪里是真的没定论,我会直说。
实验环境
正文里的每一段代码都真的跑过,而且是在真实的 Linux 内核上跑的——不是我写的模拟器。这一点在这门课里格外重要:“我的程序输出了这个"和"操作系统就是这样工作的"是两件事,只有让真内核当裁判,前者才能推出后者。
为什么必须用容器
如果你和我一样用 Apple Silicon 的 Mac,直接在 macOS 上跑这门课的代码会处处对不上:
| macOS(Apple Silicon) | 容器里的 Linux | |
|---|---|---|
| 页大小 | 16384 字节 | 4096 字节(教材里所有数字的前提) |
/proc/self/maps |
不存在 | 有——能直接看到你的地址空间长什么样 |
strace |
没有(dtruss 受 SIP 限制) |
有 |
页大小这一条尤其致命:讲分页时算出来的每一个数都会差四倍。所以这门课统一在容器里跑。
一次性准备
新建一个目录,放一个 Dockerfile:
FROM debian:13-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 gcc libc6-dev strace procps util-linux time \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /lab
CMD ["bash"]
构建一次(只需一次):
docker build -t oslab .
之后每一篇的代码,都在这个环境里跑:
docker run --rm -it --privileged -v "$PWD:/lab" oslab
--privileged 是为了让 strace 和后面几篇的调度、cgroup 实验能用;-v "$PWD:/lab" 把当前目录挂进去,你在宿主机上编辑、在容器里运行。
代码以 Python 3 为主——它的 os 模块几乎把系统调用原样暴露出来(os.fork、os.pipe、os.open),不用编译,复制即跑。只有在必须看见内存布局的地方(第 9 篇的指针与 malloc、第 72/73 号实验)才落到 C。
每篇怎么读
每篇都走同一条线:
问题从哪来(一个你能想象的场景)
→ 系统怎么解(机制本身)
→ 跑一遍看看(能运行的代码,真内核裁判)
→ 代价与取舍(它放弃了什么)
看到 ⭐ 的地方是这一篇的核心断言,看到 ⚠️ 的地方是常见的错误理解。
第零部分 · 起点
第一部分 · 虚拟化 CPU
一个核,几百个进程,每个都以为核是自己的。这是怎么做到的,以及为什么"公平"这个词比看上去难得多。
- 第 2 篇:进程——把一个 CPU 变成很多个
- 第 3 篇:进程 API——为什么创建进程要 fork 和 exec 两步
- 第 4 篇:受限直接执行——内核怎么把控制权拿回来
- 第 5 篇:调度——周转时间和响应时间不能同时最优
- 第 6 篇:多级反馈队列——不问你是谁,只看你干过什么
- 第 7 篇:多核与公平份额——CFS 到底「公平」在哪
第二部分 · 虚拟化内存
每个进程都以为自己独占整个内存。这一层谎撒得最大,机制也最精巧。
- 第 8 篇:地址空间——每个进程都以为自己独占内存
- 第 9 篇:内存 API——malloc/free 和那些经典错法
- 第 10 篇:地址转换——从基址界限到分段
- 第 11 篇:空闲空间管理——碎片是怎么攒出来的
- 第 12 篇:分页——把「随便切」改成「切成一样大」
- 第 13 篇:TLB——页表的代价,和缓存怎么救它
- 第 14 篇:更小的页表——多级页表
- 第 15 篇:交换与页面置换——内存不够时先扔谁
第三部分 · 并发
前两部分是操作系统骗你,这一部分是你自己骗自己:你以为一行代码是一步做完的。
- 第 16 篇:线程——共享地址空间的代价
- 第 17 篇:锁——从一条原子指令建起来
- 第 18 篇:并发数据结构——加锁容易,加对锁难
- 第 19 篇:条件变量——等待一件事发生
- 第 20 篇:信号量——一个原语顶两个
- 第 21 篇:死锁与并发 bug——不死锁的那些 bug 更常见
- 第 22 篇:事件驱动与异步——从 epoll 到协程到 io_uring
第四部分 · 持久化
内存断电即空。文件系统要在一个随时可能断电的设备上,维持一个"永远是对的"的结构。
- 第 23 篇:I/O 设备——轮询、中断、DMA
- 第 24 篇:磁盘与 SSD——寻道、FTL、写放大
- 第 25 篇:文件与目录——inode、文件描述符、link 到底是什么
- 第 26 篇:文件系统实现——在一块裸盘上摆出结构
- 第 27 篇:崩溃一致性——断电之后,怎么还能是对的
- 第 28 篇:现代文件系统——日志结构、写时复制、校验和
第五部分 · 隔离
把三支柱再做一遍,这次是为了把人和人隔开。
动手实验
六个实验,全部在上面那个容器里跑。每个实验都有一个不由我裁判的验收标准——要么真内核认,要么它不认。
- 实验一:写一个 shell——
fork/exec/wait/管道/重定向 - 实验二:调度器——自己实现,再和真实的 Linux CFS 对照
- 实验三:内存分配器——自己写
malloc,用/proc/self/maps看它要到了哪块地 - 实验四:并发——线程安全哈希表,用 ThreadSanitizer 抓自己的竞态
- 实验五:迷你文件系统——在一个普通文件上做出 inode 和目录
- 实验六:崩溃一致性——模拟断电,验证有日志能恢复、没日志不能