第29讲 · 容器和虚拟化

2026 南京大学《操作系统原理》第二十九讲 · 课堂实录重构

主讲 蒋炎岩(Yanyan Jiang)
视频 https://www.bilibili.com/video/BV1uaEB66EAH/
课程主页 https://jyywiki.cn/OS/2026/
本讲讲义 https://jyywiki.cn/OS/2026/lect29.md
讲义 / 幻灯片作者 蒋炎岩,https://jyywiki.cn/
讲义 / 幻灯片许可 CC BY-NC 4.0
图片来源 书中插图为课堂演示与讲义渲染图;演示类截图为视频画面

对应讲次:第29讲(南京大学 操作系统原理)

制作模型:deepseek-flash(coding-agent / pi;据 ~/.pi/agent/settings.json)

编者说明:本书由课堂录像的语音字幕重构而成,术语、章节骨架与参考链接已按课程官方讲义(lect29.md)校准。讲师的口语风格、类比与案例被尽量保留;编辑性的补充一律以「编者说明」或「编者拓展」标出。本讲为补充内容,不在考试范围。


引子:从 NEMU 到 JIT

(参考时间: 00:00)

虚拟化(full-system emulation)概念上很简单——你们做过 NEMU,逐条实现指令译码执行(包括特权指令),就能在上面跑一个真正的操作系统。真正难的是性能:

JIT(Just-In-Time)的原理:

NEMU 的 JIT 优化:把基本块编译成本地代码,性能大幅提升

📷 NEMU 的 JIT 优化:把基本块编译成本地代码,性能大幅提升
回到原视频 (00:01:00)

JIT 的工作循环:解释执行 → 计数 → 超过阈值则编译 → 跳转到本地代码

📷 JIT 的工作循环:解释执行 → 计数 → 超过阈值则编译 → 跳转到本地代码
回到原视频 (00:05:00)

编者拓展|QEMU 的 dynamic translator:一个介于「完全解释执行」和「JIT 编译」之间的巧妙方案——模板拼接 + 重定位。对一条 ADDI 指令,预先生成好的 12 字节模板直接 memcpy 过来;带立即数的指令则拆成三条朴素指令(把寄存器搬进临时寄存器、运算、搬回),写成可编译但不可链接的 C 函数(立即数处留 4 字节占位),最后把立即数 patch 进去。「每一条指令只需要一次 memory copy + 一两个数字的 patch。」见 QEMU 2005 年的短文 Fast and Portable Dynamic Translator。

QEMU 的模板拼接:预编译好的小段汇编 + 重定位 patch,代价极低

📷 QEMU 的模板拼接:预编译好的小段汇编 + 重定位 patch,代价极低
回到原视频 (00:12:00)


一、虚拟机的黄金时代

1.1 Disco 与 VMware

(参考时间: 00:16)

起点:Mendel Rosenblum 带着博士生在 SOSP'97 发表的 Disco——「Bring back an idea popular in the 1970s」(virtual machine monitor)。

Disco / VMware:1970 年代的 virtual machine monitor 理念被重新实现

📷 Disco / VMware:1970 年代的 virtual machine monitor 理念被重新实现
回到原视频 (00:16:30)

需求来自 dot-com 时代:那时去 ISP 租一台机器跑 BBS/邮件/Web 服务,CPU 空闲率常常 80%–90%——于是「黑心商人」可以做这样一件事:在一台物理机上跑 5–10 个虚拟机,以物理机一半的价格卖给你。

Disco(SOSP'97):在一台物理机上运行多个完整的操作系统

📷 Disco(SOSP'97):在一台物理机上运行多个完整的操作系统
回到原视频 (00:19:00)

编者拓展|Disco 的「影子页表」与 ring-1 hack:真正的困难是 ring0 的特权指令(关中断、IO、改 CR3)。VMware 的绝妙 hack:

ring1 hack + shadow page table:内核以 ring1 运行,影子页表让 99% 的代码全速执行

📷 ring1 hack + shadow page table:内核以 ring1 运行,影子页表让 99% 的代码全速执行
回到原视频 (00:46:30)

硬件虚拟化:VMware 的成功影响了硬件设计——Intel 2005 年推出硬件虚拟化(进入虚拟机模式,像 ring−1,很多特权指令可直接执行,只有 IO 才 VM Exit);2008 年的 EPT 把影子页表也用硬件实现(「四级页表 + 四级嵌套 = 八级」)。还有标准化的 virtio、vGPU 等。

硬件虚拟化:Intel VT(2005)与 EPT(2008)把特权指令与影子页表都放进硬件

📷 硬件虚拟化:Intel VT(2005)与 EPT(2008)把特权指令与影子页表都放进硬件
回到原视频 (00:50:00)

编者拓展|虚拟化的「疯狂想法」:VM logging & replay(只需记录 VM Exit 时发生了什么,就能重放整台计算机的执行历史——「打完游戏还能把游戏录像完整回放」)、live migration(把虚拟机当作一块内存,暂停、drop 网络连接、拷到另一台机器上启动)。「今天 Agent 和沙箱又很需要这些技术,所以它们又开始复活了。」

1.2 换个思路:操作系统为什么不能虚拟化自己?

(参考时间: 00:53)

「人真的很有意思——他们做了一整套技术,但其实从一开始就走上了错误的道路。」

编者说明:你真正想干的是「把一台机器当十台卖」——那为什么不能一个操作系统当十个操作系统来卖呢?「如果 1997 年是另一个人发表了 lightweight container,那 600 亿就不是 VMware 而是他了。」

OSID 的想法:操作系统能不能在启动时加一个参数,加载两份 initramfs?——把 struct task 数组多一维,加一个 OSID:

// 每个对象都有一个 OSID(默认都是 0)
// 遍历 /proc 时只看与自己 OSID 相同的进程
// kill 一个进程时,OSID 不同就看不到它

「就这么简单——完全可以在你的操作系统里给每个对象加一个 OSID,你就在一个操作系统里实现了两个操作系统。」而且资源池是共享的:「不管 OSID=1 还是 OSID=2,都去同一个 allocator 申请对象,申请过来打一个 OSID 标记。」——「对黑心资本家来说绝对愿意要:极致性能,只给每个对象加了四个字节。」

OSID:给操作系统里每个对象加一个「操作系统 ID」,隔离出一个操作系统里的多个「操作系统」

📷 OSID:给操作系统里每个对象加一个「操作系统 ID」,隔离出一个操作系统里的多个「操作系统」
回到原视频 (00:56:00)

编者拓展|PID 是个 leaky abstraction:PID 会被回收——「你先 ps 找到一个卡了很久的进程,但反射弧太长(3 年后才按回车)……在服务几亿人的服务器上,这中间大量进程被创建销毁,你就杀错人了。」所以有了 pidfd_open——「一个设计非常诡异的 API,但仔细一想很有道理:everything is a file,想引用一个 PID 也要 open,持有引用后引用计数 +1,这个 PID 就不能被释放。」

chroot(UNIX v7, 1979):一旦 chroot,进程的文件系统视图就被限制到某个子目录下——「这不就是一个虚拟机吗?」(虽然 PID 没隔离,但没权限就杀不掉别人的进程)。「只要你枚举操作系统里有什么对象,为每一类对象都实现命名空间的虚拟化,你就发明了今天的 Linux namespaces。」

$ chroot /tmp/xxx /bin/sh       # 需要先构建一个含 /bin/sh 的完整文件系统
$ lsns                            # 列出系统里的 namespace
$ ls /proc/<pid>/ns               # 每个进程的 namespace(ipc/mnt/net/pid/user/...)
`chroot`:把进程的文件系统视图限制到某个子目录——容器隔离的雏形

📷 `chroot`:把进程的文件系统视图限制到某个子目录——容器隔离的雏形
回到原视频 (01:03:00)

`lsns`:列出系统里的 namespace——容器隔离的基础设施

📷 `lsns`:列出系统里的 namespace——容器隔离的基础设施
回到原视频 (01:06:30)

Linux namespaces:为每一类操作系统对象实现命名空间虚拟化(PID/mount/IPC/network/user/UTS/time)

📷 Linux namespaces:为每一类操作系统对象实现命名空间虚拟化(PID/mount/IPC/network/user/UTS/time)
回到原视频 (01:08:00)

编者说明:PID=1 的 /bin/init(systemd)拥有全套十个命名空间(time/secret/pid/user/uts/ipc/net/mnt/cgroup/…),相当于一个「根容器」,198 个进程默认共享,但有些守护进程在隔离的 namespace 上运行。

1.3 cgroups 与容器

(参考时间: 01:09)

光隔开还不够——黑心商人还要能控制用量:

cgroups:把进程分组,为每组设置资源策略(与 namespace 正交的机制)

📷 cgroups:把进程分组,为每组设置资源策略(与 namespace 正交的机制)
回到原视频 (01:11:00)

编者拓展|容器 = namespaces + cgroups + overlayfs:

「这就是黑心资本家最喜欢的——你已经发明了 Docker。」

namespaces + cgroups + overlayfs = 容器:分层镜像让一台机器放 1000 个实例只浪费一份存储

📷 namespaces + cgroups + overlayfs = 容器:分层镜像让一台机器放 1000 个实例只浪费一份存储
回到原视频 (01:11:30)

从 jail 到 Docker:FreeBSD 2000 年就有 jail;Linux 2006 年才有 cgroups + namespaces(Google 工程师推动);2008 年第一个流行的 LXC;再后来 Docker 成为云原生的标准答案。


二、容器 v.s. 虚拟机

(参考时间: 01:12)

虚拟机相比容器有一个显著优点:安全性——因为容器共享同一个内核,内核有漏洞就惨了:

「但虚拟机太浪费了——需要一个模拟出来的物理内存、一个 shadow page table,还把软件和硬件都卷了进来。也许更轻量的方案更好。」

容器共享内核的代价:一个全局指针就能越权访问别人的容器(以及 cgroups 没做好导致的 DoS)

📷 容器共享内核的代价:一个全局指针就能越权访问别人的容器(以及 cgroups 没做好导致的 DoS)
回到原视频 (01:13:00)

编者拓展|The Bitter Lesson:讲师又把 Sutton 的「苦涩教训」搬了出来——「要做那些能随算力规模化的 meta mechanism。强化学习、大模型都是,某种程度上人类/生命也是:DNA 可以复制,底下更简单的是化学公理。」

Kubernetes:声明式编排:容器需要管理——VMware 有 ESX/vSphere(需要在服务器上装软件、甚至带外控制信号);容器上做同样的事容易多了:

Kubernetes:声明式编排,把容器调度到整个集群(本质是一个多维度背包问题)

📷 Kubernetes:声明式编排,把容器调度到整个集群(本质是一个多维度背包问题)
回到原视频 (01:18:30)


三、关于「品味」与科研

(参考时间: 00:28)

编者拓展|「发一篇真正论文的意义」:讲师有一段关于科研的诚恳批评——「现在进入了一个怪圈:老板有个题目,需要一帮『打手』……然后凑数进一个论文流水线工厂。结果做不出来就去 benchmark crime(隐藏、不 scale、cherry-pick)。」他指出:Disco 论文里那句「Bring back an idea popular in 1970s」——「这才是发一篇真正论文的意义所在」。

「好的论文是有传承的」:Mendel Rosenblum 还有很有名的 LFS(Log-structured File System)(OSTEP 有专门一章);他的学生 Stephen Harrold 毕业后去了 Transmeta(把 x86 翻译成 VLIW/RISC,性能比 x86 还高,但「跟 Intel 打了很多年官司,最后被 Intel 活活拖死」);还有 Amir Hel 等做了很多 systems 领域的好工作。

编者拓展|「品味是不会消失的」:讲师说,我们这一代「成了第一代知道什么是好的人,但没能把世界变得更好」——「20 年后我发现同学们还在 report hacking,还有同学私信问『最后两个实验考试会不会考』。」他希望大家见识更大的世界:「虚拟化、实现一个 Disco 这样的系统、甚至去想『要不要争取财富自由的机会』——如果你问自己这个问题的次数够多,某天考试不再作为评价标准时,你的人生才不会失去动力。」

他还举例:20 年前复旦实验室的几位同学在 VEE'06 发表了 Live Update: Updating Operating Systems Using Virtualization——这个实验室后来诞生了国内操作系统论文产出第一的组(IPADS),也是华为 OS 首席科学家所在的组。「品味是增长的——他们刚起步时也没见过什么是真正好的工作,后来去 MIT 访学一年,才发现『原来世界是这样的』。」

他还有一个课程联动的想法:把系统方向的实验和离散数学/数字电路课联动——用 form of proof 的形式化语言(比如 RISC-V 的一小部分)从形式上证明你的 CPU/模拟器是对的,「就不需要 differential testing 了」。


总结

编者说明:本讲(及上一讲)是补充内容,不考试。讲师最后的落点是「品味」与「见识更大的世界」——「一门课我讲再多知识都是没有用的,你们可能需要意识到还有更多『竟然还可以这样』的东西。」


附:官方参考与延伸阅读

本讲的讲义见课程主页,以下链接按书中出现顺序整理:

主题 链接
Bellard, Fast and Portable Dynamic Translator(QEMU, 2005) https://www.usenix.org/legacy/events/usenix05/tech/freenix/full_papers/bellard/bellard.pdf
Bugnion et al., Disco: Running Commodity Operating Systems on Scalable Multiprocessors(SOSP'97) https://dl.acm.org/doi/10.1145/268998.266672
VMware https://www.vmware.com/
Linux namespaces(namespaces(7)) https://man7.org/linux/man-pages/man7/namespaces.7.html
Linux cgroups https://docs.kernel.org/admin-guide/cgroup-v2.html
chroot(2) https://man7.org/linux/man-pages/man2/chroot.2.html
pidfd_open(2) https://man7.org/linux/man-pages/man2/pidfd_open.2.html
LXC https://linuxcontainers.org/
Docker / Kubernetes https://kubernetes.io/docs/concepts/
Rosenblum & Ousterhout, The Design and Implementation of a Log-Structured File System https://people.eecs.berkeley.edu/~brewer/cs262/LFS.pdf
Sutton, The Bitter Lesson http://www.incompleteideas.net/IncIdeas/BitterLesson.html
OSTEP(第 16–18 章:Virtualization;第 36 章 I/O Devices) https://pages.cs.wisc.edu/~remzi/OSTEP/

阅读材料:读 Disco(SOSP'97)与 QEMU 的 dynamic translator;动手用 unshare/chroot/cgroups 拼一个「最小容器」。

版权说明:本讲讲义与幻灯片作者为蒋炎岩,采用 Creative Commons BY-NC 4.0 许可,来源于 https://jyywiki.cn/OS/2026/。视频画面与字幕版权归原作者所有。本书为课堂内容的学习性重构。