第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,逐条实现指令译码执行(包括特权指令),就能在上面跑一个真正的操作系统。真正难的是性能:
- 在 NEMU 上,虚拟机和真机的性能比大约是 1/10——「大部分同学的仙剑奇侠传应该没办法满帧流畅运行」;
- 但也有同学 JIT 优化得很好,仙剑奇侠传、Galgame 都能跑到 60 帧,「非常丝滑」。
JIT(Just-In-Time)的原理:
- 基本块(basic block):从一条指令开始,连续取到下一个跳转/条件跳转/
return之前——「这些指令一定按顺序执行」; - 你可以把这个基本块生成一段 C 代码(
CPU_state通过宏展开成寄存器),丢给 GCC 编译成.so,再dlopen加载进来直接调用——「立即数全部硬编码,不用再 decode,性能好很多」; - 但不能每个基本块都调用外部 GCC(一次可能 100ms)——所以 JIT 的工作方式:启动时先解释执行,为每个基本块计数,超过阈值(1 万/10 万次)才在后台编译,下次直接跳转到编译后的代码。Java VM、Node.js 都遵循这个循环。
📷 NEMU 的 JIT 优化:把基本块编译成本地代码,性能大幅提升
回到原视频 (00:01:00)
📷 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,代价极低
回到原视频 (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 理念被重新实现
回到原视频 (00:16:30)
需求来自 dot-com 时代:那时去 ISP 租一台机器跑 BBS/邮件/Web 服务,CPU 空闲率常常 80%–90%——于是「黑心商人」可以做这样一件事:在一台物理机上跑 5–10 个虚拟机,以物理机一半的价格卖给你。
- 「他不告诉你我有多少台物理机,悄悄一台当十台卖」;
- 而且超卖(over-subscribe)——「云厂商承诺你有多少算力,但一般至少卖到 120% 甚至 140%,因为他赌你不会把算力一直拉满」(讲师自己的课程网站 CPU 占用长期几乎为 0%);
- 「他发现了财富自由的机会,顺手把 paper 作者和几个博士生也带走了」——于是有了 VMware(上市时估值「仅次于 Google」)。
📷 Disco(SOSP'97):在一台物理机上运行多个完整的操作系统
回到原视频 (00:19:00)
编者拓展|Disco 的「影子页表」与 ring-1 hack:真正的困难是 ring0 的特权指令(关中断、IO、改
CR3)。VMware 的绝妙 hack:
- 把操作系统内核放到 ring1——「这只有真正懂技术的人才做得出来」;ring1 没有权限,所有特权指令都会产生异常 trap 出来,交给 VMM 处理;
- Shadow Page Table(影子页表):客户机认为自己的物理地址 1/2/3 映射到物理页 1/2/3,实际上 VMM 才是物理内存拥有者——它维护真正的页表(把客户机物理页映射到真实物理页),每次
move CR3时悄悄把影子页表换上去;- 结果是:操作系统 99% 的代码以本地速度执行,只有特权指令/IO 才 trap 出来。
📷 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)把特权指令与影子页表都放进硬件
回到原视频 (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」,隔离出一个操作系统里的多个「操作系统」
回到原视频 (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`:把进程的文件系统视图限制到某个子目录——容器隔离的雏形
回到原视频 (01:03:00)
📷 `lsns`:列出系统里的 namespace——容器隔离的基础设施
回到原视频 (01:06:30)
📷 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)
光隔开还不够——黑心商人还要能控制用量:
- 原始操作系统的进程调度器对所有进程公平执行;有了 OSID 后,你希望同一 OSID 的所有进程共享一个 CPU 配额;
- 于是有了 cgroups(control groups):把进程分组,为每组设置资源访问策略(CPU、内存、文件系统带宽、网络带宽……);cgroups v2 是一个树状结构。
📷 cgroups:把进程分组,为每组设置资源策略(与 namespace 正交的机制)
回到原视频 (01:11:00)
编者拓展|容器 = namespaces + cgroups + overlayfs:
- namespaces 提供隔离;
- cgroups 提供资源限制;
- overlayfs(上节课讲的分层镜像)让你轻量地创建同一个系统的多个实例——「最底下那一层是一个操作系统镜像,一台机器上放 1000 个实例,只要一个副本、只浪费一份存储」。
「这就是黑心资本家最喜欢的——你已经发明了 Docker。」
📷 namespaces + cgroups + overlayfs = 容器:分层镜像让一台机器放 1000 个实例只浪费一份存储
回到原视频 (01:11:30)
从 jail 到 Docker:FreeBSD 2000 年就有 jail;Linux 2006 年才有 cgroups + namespaces(Google 工程师推动);2008 年第一个流行的 LXC;再后来 Docker 成为云原生的标准答案。
二、容器 v.s. 虚拟机
(参考时间: 01:12)
虚拟机相比容器有一个显著优点:安全性——因为容器共享同一个内核,内核有漏洞就惨了:
- 越权:拿到一个全局指针就能访问别人的容器,偷走机密信息;
- DoS:如果 cgroups 资源控制没做好,「不停地创建文件把系统文件描述符表耗满,别人想打开文件也打不开了」。
「但虚拟机太浪费了——需要一个模拟出来的物理内存、一个 shadow page table,还把软件和硬件都卷了进来。也许更轻量的方案更好。」
📷 容器共享内核的代价:一个全局指针就能越权访问别人的容器(以及 cgroups 没做好导致的 DoS)
回到原视频 (01:13:00)
编者拓展|The Bitter Lesson:讲师又把 Sutton 的「苦涩教训」搬了出来——「要做那些能随算力规模化的 meta mechanism。强化学习、大模型都是,某种程度上人类/生命也是:DNA 可以复制,底下更简单的是化学公理。」
Kubernetes:声明式编排:容器需要管理——VMware 有 ESX/vSphere(需要在服务器上装软件、甚至带外控制信号);容器上做同样的事容易多了:
- 声明式:「我要这个名字的服务,在每个数据中心复制 100 份」——K8s 自动调度,容器被杀掉了就在另一台机器上再起一个(因为 Docker 是 hash 构建的 deterministic build);
- 见缝插针:「哪个机器余量比较大就插到哪」——这是一个背包问题(NP-hard 的优化问题):「算法做得越好,为黑心资本家省下的机器就越多」。
📷 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 了」。
总结
- 虚拟化的两条路线:
- 虚拟机(full-system virtualization):Disco/VMware——ring1 hack + shadow page table;硬件虚拟化(Intel VT 2005)、EPT(2008)、virtio/vGPU;黄金时代(VMware 估值数百亿);
- 容器:OSID(给每个对象加一个 OS ID)→ Linux namespaces(PID/mount/IPC/net/user/UTS/time)+ cgroups(资源策略)+ overlayfs(分层镜像)→ Docker / Kubernetes;
- 性能与安全:JIT(基本块、阈值编译)、QEMU 的模板拼接;虚拟机隔离更好但更重,容器更轻但共享内核(有越权和 DoS 风险);
- Takeaway:「容器和虚拟机在云上其实是完全一样的」——只是开销量级不同。「技术也是可以改变世界的——你也许可以不要那么在意分数,然后可以去试一试。」
编者说明:本讲(及上一讲)是补充内容,不考试。讲师最后的落点是「品味」与「见识更大的世界」——「一门课我讲再多知识都是没有用的,你们可能需要意识到还有更多『竟然还可以这样』的东西。」
附:官方参考与延伸阅读
本讲的讲义见课程主页,以下链接按书中出现顺序整理:
阅读材料:读 Disco(SOSP'97)与 QEMU 的 dynamic translator;动手用 unshare/chroot/cgroups 拼一个「最小容器」。
版权说明:本讲讲义与幻灯片作者为蒋炎岩,采用 Creative Commons BY-NC 4.0 许可,来源于 https://jyywiki.cn/OS/2026/。视频画面与字幕版权归原作者所有。本书为课堂内容的学习性重构。