操作系统学习笔记 · 第 29 课 · 虚拟化与容器原理
现代系统几乎都跑在云上、容器里。你可能天天用 Docker,却未必想过:Docker 凭什么能做到"秒级启动、互相隔离"? 它和虚拟机有什么本质区别?这一课,我们把 Docker 的"黑魔法"拆开看——你会发现,它调用的全是 Linux 内核早就有的能力。这是第八阶段(虚拟化与容器)的唯一一课,也是全书倒数第三课。
从"一栋楼 vs 一套房"说起
要把"一台机器当多台用",有两条路:
- 虚拟机(VM):在真机器上,用软件模拟出一台完整的电脑,再在上面装操作系统;
- 容器(Container):不模拟硬件,而是在同一个内核里,划出几个"互相看不见的小房间"。
生活类比:
虚拟机 = 一栋楼里装多套带独立水电表的房子(每套都有自己的水电表、自己的内核);容器 = 同一套房子里,用隔断分出多个工位(共享一套水电,只是彼此看不见)。
这就是两者的本质分野。
29.1 虚拟化 vs 容器
| 虚拟化(VM) | 容器 | |
|---|---|---|
| 隔离层 | 模拟整台硬件 | 共享内核,隔离进程视图 |
| 开销 | 大(每个 VM 一套 OS) | 小(秒级启动、几乎无损耗) |
| 代表 | VMware、KVM | Docker、Podman |
一句话理解:虚拟机是"软件模拟出一台真电脑"(每个 VM 都有自己的内核);容器是"同一个内核里划出多个互相看不见的小房间"。
- 虚拟机:用 hypervisor(虚拟机监视器) 模拟 CPU、内存、磁盘等整套硬件,每个 VM 里跑一个完整的操作系统内核。隔离最彻底,但开销巨大——每个 VM 都要一份 OS,启动慢、占资源;
- 容器:共享宿主机的内核,只是通过内核的 namespace/cgroup 让进程"以为"自己独享一台机器。启动秒级、开销几乎为零。
一句话:VM 隔离强但重,容器轻但共享内核。 这也是为什么现代互联网大量用容器——能在一个内核上跑成百上千个"小房间"。
29.2 namespace——"隔离"
一句话理解:namespace 让进程看到不同的世界——每个 namespace 里的进程以为自己拥有独立的 PID、网络、文件系统、用户,其实底层都共享同一个内核。
容器的"隔离"靠 namespace。它给进程提供一套独立的"视图":进程在 namespace 里,只能看到属于自己的那部分资源。
常见的七种 namespace:
| namespace | 隔离什么 |
|---|---|
| PID | 进程号(容器里 PID 从 1 开始) |
| NET | 网络栈(独立 IP、端口) |
| MNT | 挂载点(独立文件系统视图) |
| UTS | 主机名 |
| USER | 用户/权限 |
| IPC | 进程间通信资源 |
| Cgroup | 资源配额的视图 |
例子:在容器里 ps,只能看到自己那少得可怜的进程,PID 从 1 开始——因为 PID namespace 把它隔离了。它看不到宿主机上其他进程。一句话:namespace 管"隔离"——让进程以为自己独占一个世界。
29.3 cgroup——"限资源"
一句话理解:隔离只解决"看不见",cgroup 解决"别乱抢"——限制一组进程最多用多少 CPU、内存、磁盘 I/O。
namespace 让进程"看不见别人",但没解决"抢资源"——一个容器里的进程照样能把整台机器的 CPU 吃满,影响所有容器。
cgroup(control group,控制组) 就是干这个的:
- 把进程分组;
- 给某组设
cpu.max、memory.max等配额; - 超了就被限速,甚至触发 OOM(内存超限被杀)。
一句话:namespace 管"隔离",cgroup 管"配额",两者合起来就是容器的两个基石。
29.4 镜像与分层文件系统
一句话理解:Docker 镜像是一层层"只读"文件系统叠起来的(OverlayFS),每层只记录差异,共享底层镜像,所以省空间、秒级启动。
Docker 镜像为什么能秒级启动、还特别省空间?靠分层文件系统:
- 镜像由多层只读文件系统叠成(底层技术叫 OverlayFS);
- 每一层只记录和上一层的差异;
- 多个镜像可以共享同一层(比如都用 ubuntu 基础镜像,那这一层只存一份)。
例子:docker image ls 看到的镜像大小,远小于各层之和——因为层是复用、去重的。10 个镜像可能共享同一个 70MB 的 ubuntu 底层,只各自多一层几十 KB 的差异。29.5 Docker 的内核依赖清单
一句话理解:Docker 不是魔法,它调用的全是 Linux 内核既有能力——clone带 namespace 标志创建容器进程、cgroup限额、OverlayFS做镜像层、chroot/pivot_root换根目录。
把 Docker 拆开,它底下就四样东西:
clone+ namespace 标志:创建容器进程时,带上各种 namespace 标志,让它"进自己的小房间";- cgroup:给这个容器进程组设 CPU/内存配额;
- OverlayFS:把镜像层叠起来;
chroot/pivot_root:把容器的根目录切到镜像里,让它只看到镜像里的文件系统。
一句话:Docker = namespace(隔离)+ cgroup(限额)+ OverlayFS(镜像)+ chroot(换根)。它没有发明任何新内核技术,只是把这套繁琐的流程自动化、标准化了。
动手实验:亲手"造"一个容器
# 1. 用 unshare 造一个"独立世界"(PID + 挂载 + 主机名隔离)
sudo unshare --pid --fork --mount --uts --mount-proc bash
hostname my-container # 改主机名,不影响宿主机
ps -ef # 只看到容器里的进程
exit
# 2. 用 cgroup 限制 CPU(v2)
sudo mkdir /sys/fs/cgroup/demo
echo "50000 100000" | sudo tee /sys/fs/cgroup/demo/cpu.max # 最多用 50% CPU
echo $$ | sudo tee /sys/fs/cgroup/demo/cgroup.procs
# 跑个死循环,观察 CPU 被压在 50%
yes > /dev/null &
top -bn1 | head -5
# 3. 限制内存
echo "100000000" | sudo tee /sys/fs/cgroup/demo/memory.max # 100MB一句话理解:unshare 演示了"隔离"(容器前半段),写 cgroup 文件演示了"限额"(容器后半段)——Docker 无非是把这些自动化了。- 第一条
unshare命令,让你进到一个独立的 PID/挂载/主机名世界里,ps只能看到自己——这就是 namespace 的隔离效果; - 后面写
/sys/fs/cgroup/demo/cpu.max,把某个进程组的 CPU 上限压到 50%——这就是 cgroup 的限额效果。
体会一下:你刚才手动做了一遍 Docker 干的事。Docker 只是把这些步骤包装成了 docker run。深入点:两个进阶话题
① 容器的安全边界弱于 VM
容器共享内核,一旦内核被攻破,所有容器沦陷;VM 有硬件级隔离,更安全。所以高安全等级场景仍用 VM,或两者结合(VM 里跑容器)。
这也解释了为什么"多租户高隔离"场景(比如不同公司共用一台物理机)更倾向 VM,而"同一家公司内部跑微服务"用容器就够了。
② OCI 与 runc
Docker 底层用containerd+runc,runc本质就是帮你把unshare/cgroup 那套流程打包成标准(OCI 规范),Kubernetes 时代这些组件已经标准化。
OCI(Open Container Initiative)是一套容器标准,runc 是它的一个参考实现。这保证了不同厂商的容器工具能互通——Kubernetes 能调度 Docker、containerd、Podman 等不同运行时,靠的就是这套标准。
小结与思考题
这一课,我们拆开了 Docker 的"黑魔法":
- 虚拟化 vs 容器:VM 模拟整台硬件(重、隔离强),容器共享内核(轻、隔离弱);
- namespace:管"隔离",让进程以为独占一个世界;
- cgroup:管"配额",限制 CPU/内存/IO;
- 分层文件系统(OverlayFS):层叠只读镜像,省空间、秒级启动;
- Docker 内核依赖清单:namespace + cgroup + OverlayFS + chroot。
留三个问题:
- 虚拟机和容器最本质的区别?
- namespace 和 cgroup 各解决什么问题?
- Docker 依赖内核的哪些特性?
到这里,第八阶段(虚拟化与容器,29 课)完成。至此,你从"进程怎么跑"一路学到了"容器怎么隔离",理论已经齐了。但真正的高手,不只是"懂原理",更要会动手排障——系统卡了、内存满了、程序挂了,你怎么定位问题在哪?下一课,我们进入第九阶段:实战收束,先学一套"用命令解剖操作系统"的排障功夫,最后亲手写一个迷你 shell,把全书知识合流。