<!-- title: 容器不是虚拟机：内核如何骗进程 -->
<!-- domain: programming -->
<!-- video: https://www.youtube.com/watch?v=LTlkYpvPlJo -->

# 容器不是虚拟机：内核如何骗进程

## 导语：一个被普遍误解的技术

几乎所有开发者都"用过"容器——`docker run`、Kubernetes、云原生、CI/CD——但你若问一句"容器到底是个什么东西"，十个人里能有八个会答错。最主流的错误答案是：**容器就是轻量级的虚拟机**。

这个误解如此普遍，以至于它本身成了理解一切云原生技术的最大障碍。今天我们要拆解的这支视频——**《How Docker Actually Works: Namespaces & Cgroups Explained》**（作者 Doogal，资深软件工程师）——要传递的核心反常识只有一句话：**容器里根本没有虚拟机，它甚至没有一个新的操作系统内核。容器只是一群普通的 Linux 进程，只不过内核骗了它们，让它们以为自己是世界上唯一的进程。**

这不是一个"科普向"的边角知识。当你真正理解"容器 = 受约束的普通进程"之后，你就能瞬间想通一长串原本只能死记硬背的问题：为什么容器启动只要毫秒级？为什么同一个内核漏洞可以让一台宿主机上所有容器一起沦陷？为什么容器里的 root 和宿主机上的 root 不是一回事？为什么 `docker run` 启动的程序会变成 PID 1，而 PID 1 在信号处理上有那么多坑？为什么 gVisor、Kata Containers 这些"给容器套一层虚拟机内核"的方案在 2026 年突然成了高安全场景的标配？

本文先还原视频的核心论证，再补齐视频来不及展开的内核机制、安全模型和实战陷阱。读完之后，你对"容器"的认知会从"一种打包工具"升级为"一组 Linux 内核特性的组合产物"。

## 视频背景卡片

| 项目 | 内容 |
|------|------|
| 视频标题 | How Docker Actually Works: Namespaces & Cgroups Explained |
| 频道 | Doogal \| Senior Software Engineer |
| 平台 | YouTube |
| 核心主题 | 剥离 Docker 的"魔法外壳"，揭示容器在 Linux 内核层面的真实构成：namespaces + cgroups |
| 一句话定位 | 把"容器"从黑盒变成可被源码级理解的一组系统调用 |

## 核心内容拆解

### 一、最关键的认知翻转：容器 ≠ 虚拟机

视频的第一块基石，是先把"容器是轻量虚拟机"这个图景彻底砸碎。

**虚拟机（VM）走的是硬件级虚拟化路线**：在物理机之上跑一个 hypervisor（如 KVM、VMware ESXi），它截获并模拟出一套完整的虚拟硬件——虚拟 CPU、虚拟网卡、虚拟磁盘。每个 VM 里都装有一份**完整的客户操作系统内核（guest kernel）**，自己调度进程、自己管内存页表、自己跑驱动。代价是巨大的：每个 VM 至少要占用几百 MB 到几 GB 的内存来养那份 guest OS，冷启动要等内核整个引导流程走一遍，常常是几十秒。

**容器走的是完全不同的操作系统级虚拟化路线**。这里没有 hypervisor，没有虚拟硬件，更没有第二个内核。所有容器和宿主机**共享同一个 Linux 内核**。容器里跑的那个程序，本质上和你在宿主机终端里直接敲 `./myapp` 启动的进程，是同一种东西——都是内核调度器眼里的 task_struct，都排队等同一个 CPU 时间片。区别只在于：内核用一些"障眼法"，让这个进程**看不到**其他进程、**看不到**真实的网络拓扑、**看不到**真实的文件系统挂载点。

> **视频原话主旨**：Docker 并没有发明任何新东西，它只是把 Linux 内核早就在用的两个特性——namespaces 和 cgroups——打包成了一个易用的工具。容器不是新物种，是旧积木的新组合。

这个翻转的含金量极高。一旦接受"容器就是普通进程"，后续所有现象都能用进程语义解释：容器秒级启动，是因为启动一个进程本来就只要毫秒，根本不用等内核引导；容器内存占用低，是因为省掉了 guest OS 那一大坨常驻开销；而所谓"容器逃逸（container escape）"，本质上不是攻破了一台虚拟机，而是**让一个进程骗过内核的隔离，重新看到了宿主机的世界**——这是内核漏洞利用问题，和"VM 越界"是完全不同等级的威胁。

### 二、Namespaces：给进程造一个"楚门的世界"

视频用大量篇幅讲 namespaces，这是"骗进程"的主力武器。namespaces 的机制可以用一个比喻概括：**它给每个进程戴上一副特制眼镜，让进程看到的"系统全局状态"是一份被精心裁剪过的副本。** 进程以为自己看到了全世界，其实看到的是楚门的世界。

Linux 内核目前提供了**8 种 namespace**（视频重点讲了前几种，这里补齐全部，并标注哪些是后期加入的），每一种负责隔离一类"系统资源视图"：

- **PID namespace**：让进程拥有自己独立的进程号树。容器里的主进程看到自己是 **PID 1**，看不到宿主机上其他几万个进程。这也是为什么 `docker exec` 进去敲 `ps` 只能看到容器内的进程。
- **NET namespace**：独立的网络栈——独立的网卡接口（比如那个经典的 `eth0`）、独立的路由表、独立的 iptables 规则、独立的 loopback。容器能有一个 IP 地址，本质就是 NET namespace 给它配了一张虚拟网卡（veth pair 的一端）。
- **MNT namespace**：独立的文件系统挂载点视图。这是容器看起来"有自己的根文件系统（rootfs）"的根源——内核并没有真的换一个硬盘，只是把另一套挂载树藏起来，给这个进程看一张新的挂载表。
- **UTS namespace**：隔离 hostname 和 domain name，所以每个容器可以有不一样的名字（你在 `docker run` 里设的 `--hostname` 就是改这个）。
- **IPC namespace**：隔离 System V 和 POSIX 消息队列、共享内存等进程间通信对象。
- **USER namespace**：可能是最巧妙的一种——**UID/GID 映射**。容器里你以为自己是 root（UID 0），但映射到宿主机上可能是一个毫无权限的普通用户（比如 UID 100000）。这正是 rootless container 的内核基础。
- **Cgroup namespace**：让容器看到的是自己的 cgroup 层级视图，而不是宿主机整个 cgroup 树。
- **Time namespace**（内核 5.6 引入，属于较新的补充）：允许容器有独立于宿主机的时间偏移，主要用于容器迁移/检查点恢复（CRIU）场景。

**技术内核（延伸补充）**：从系统调用层面看，创建和进入 namespace 只有三个 API——`clone()`（在 fork 时顺便创建新 namespace）、`unshare()`（让已存在进程"脱离"当前 namespace）、`setns()`（让进程加入一个已有的 namespace）。所谓"启动一个容器"，落到最底层就是带着 `CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | ...` 这串标志位去调用 `clone()`。理解了这一点，你就明白容器技术没有任何"魔法内核模块"——它就是几个 flag。

### 三、Cgroups：给资源画一道"围栏"

视频强调，namespaces 解决的是"能不能看见"的问题，但**光隔离是不够的**。一个容器即使看不见别的进程，如果它发起一个死循环把 CPU 占满，或者疯狂 `malloc` 把内存吃光，宿主机上所有其他容器照样被拖死。于是需要第二根支柱：**cgroups（control groups）**，它解决的是"能用多少"的问题——做资源计量（accounting）和资源限制（limiting）。

cgroups 把资源切成几个控制器（controller），视频提到的核心几类包括：

- **CPU 控制器**：可以设 cpu.shares（相对权重）或 cpu.max（绝对配额，比如"最多用 1.5 个核"）。超出配额就会被节流（throttle）。
- **memory 控制器**：设内存上限，一旦进程组触及上限，内核会触发 **OOM killer** 把这个组里的进程杀掉——这就是你在 Kubernetes 里常见的 `OOMKilled` 退出原因的内核根源。
- **IO/blkio 控制器**：限制磁盘读写带宽和 IOPS，防止一个容器把存储 IO 打满。
- **pids 控制器**：限制一个 cgroup 里最多能 fork 出多少进程，专门防 fork bomb。

> **延伸（视频未细讲的历史包袱）**：cgroup 有两个大版本。**v1** 是每个控制器一棵独立的层级树（hierarchy），灵活但难用、容易配置混乱；**v2**（约内核 4.5 起逐步成熟）统一成**单一层级树**，所有控制器挂在同一棵树上，语义更一致。如今 Docker/Kubernetes 默认已切到 cgroup v2。理解 v1/v2 的差异，是排查很多"为什么我配的限制没生效"问题的前提。

值得注意的是一个常被忽略的设计哲学：**cgroups 和 namespaces 是正交的两套机制**。你可以只隔离不限资源（进程能看见的很少，但能把机器吃垮），也可以只限资源不隔离（进程什么都看得见，但最多用 1 核）。**"容器"只是把这两者同时拉满的约定俗成的组合**，内核里并不存在一个叫"container"的对象。

### 四、安全三件套：capabilities、seccomp、LSM

容器在 namespaces+cgroups 之上，还叠加了第三层——**安全策略**，因为前两层解决不了"容器内的 root 还是太危险"。视频在这块点到为止，这里展开补充，因为它是理解"为什么容器安全"的关键。

- **Linux Capabilities**：传统 Unix 里 root 拥有一切权限，非 root 几乎啥也干不了，粒度太粗。Capabilities 把 root 的特权**拆成了约 40 个可独立开关的 bit**——比如 `CAP_NET_BIND_SERVICE`（绑定 1024 以下端口）、`CAP_SYS_ADMIN`（几乎万能的危险位）、`CAP_NET_RAW`（能发原始网络包，常被攻击者用来做 ARP 欺骗/扫描）。Docker 默认只给容器一个小白名单（drop 绝大部分，只保留十几个）。所以**容器里的 root ≠ 宿主机 root**，它是一个被阉割过的 root。
- **seccomp（secure computing mode）**：默认 profile 直接**屏蔽掉几百个几乎用不到的危险系统调用**（比如过去多次出过内核漏洞的 `keyctl`、`bpf`、某些 `ptrace` 用法）。攻击者就算在容器里拿到 shell，能调用的攻击面也大幅缩小。
- **LSM（Linux Security Modules）**：AppArmor / SELinux 这类强制访问控制（MAC），按预先写好的策略文件，对"这个进程能不能读这个文件"做更细的裁决。

把这三层和前两层叠起来，才是一个生产级容器真正的防御纵深。**延伸观点**：这也是为什么高安全场景在 2026 年流行 **gVisor**（Google）和 **Kata Containers**——它们思路相反：既然共享内核是最大风险点，那就**给每个容器配一个专用的轻量级 guest 内核**（gVisor 用用户态实现的兼容内核拦截系统调用，Kata 直接套一个极简 VM）。等于把容器和虚拟机的优点缝合——这恰恰反证了"容器 = 共享内核的进程"这个本质。

### 五、OverlayFS 与镜像分层：Copy-on-Write 的艺术

视频还涉及一个常被忽略但至关重要的机制——**镜像（image）是怎么存的、容器怎么从镜像里"跑"起来**。

Docker 镜像不是一个单一的大文件，而是**一堆只读层（read-only layer）的堆叠**，每执行一条 `Dockerfile` 指令（`RUN`、`COPY`）就新增一层。启动容器时，内核用 **OverlayFS**（联合挂载文件系统，overlay/overlay2）把这些只读层当成 **lowerdir**，在上面叠一层薄薄的**可写层（upperdir）**，再统一挂成一个 merged 视图给容器看。

这套设计的精髓是 **copy-on-write（COW）**：

- 多个容器基于同一个镜像启动时，它们**共享所有只读层**，只有各自的可写层是独立的。这就是为什么 10 个基于同一个 Ubuntu 镜像的容器，磁盘占用几乎还是一份镜像的大小——密度惊人。
- 容器要修改某个文件时，OverlayFS 先把这个文件从下层**复制**到可写层，再在副本上改，原始只读层永远不动。这也是为什么"容器里改的内容一停就没了"——可写层随容器销毁而消失，持久化必须挂卷（volume）。

> **延伸**：这个分层模型直接决定了 Dockerfile 的最佳实践——把变化频率低的层放前面（基础镜像、装依赖），变化频率高的放后面（拷业务代码），这样 layer cache 命中率最高，构建飞快。"为什么 Dockerfile 顺序影响构建速度"——根因就是 COW 分层。

### 六、runc 与 OCI：真正干活的那几行系统调用

视频反复强调的一个观点，也是 2015 年容器生态一次重要标准化运动的产物：**Docker 只是门面，真正"造容器"的是一个叫 runc 的小程序。**

早期 Docker 把"管理镜像""管理网络""创建容器"全塞进一个巨型 daemon，既臃肿又不利于生态。后来社区拆出了 **OCI（Open Container Initiative）** 标准，把容器的定义拆成三块：镜像格式标准（image spec）、运行时标准（runtime spec）、分发标准（distribution spec）。**runc** 是 reference runtime——它做的事情极其纯粹：**读一份 runtime spec 配置（一个 JSON），然后忠实地调用 `clone()/unshare()/setns()` 去创建 namespaces，写 cgroup 文件去设限制，挂载 rootfs，最后 `exec` 用户指定的进程。**

于是一个完整的调用栈是：`docker` CLI → `dockerd`（daemon）→ `containerd`（高层运行时，管镜像、生命周期）→ `runc`（底层运行时，真正发系统调用）。你甚至可以完全不用 Docker，直接拿 runc + 一份手写的 JSON 就能起一个容器。

> 这个事实极其重要：**容器没有任何专利技术、没有任何神秘内核模块。它是 Linux 内核 + 一组标准化的 CLI 工具。** 正因如此，containerd、CRI-O、podman、runc 才能无缝互操作——大家都遵守同一份 OCI spec。

### 七、为什么这套设计统治了云（以及它的命门）

最后，视频隐含了一个值得点透的结论：容器之所以从 2013 年一个 dotCloud 公司的副业，长成今天云计算的事实标准，靠的是上面这套机制带来的**结构性优势**：

1. **极致密度**：没有 guest OS 开销，一台宿主机可以塞几十到几百个容器，而 VM 时代的密度通常是个位数。
2. **毫秒级弹性**：启动一个进程 = 启动一个容器，扩缩容近乎瞬时，这是 Kubernetes 能做秒级 autoscaling 的物理基础。
3. **不可变基础设施（immutable infrastructure）**：镜像是只读、版本化、可复现的制品，"在我机器上能跑"的问题被根除。
4. **可组合性**：基于 OCI 标准，构建、传输、运行可以被不同厂商的不同工具各司其职。

**但硬币的另一面是这套设计固有的命门**——视频没有展开，这里补上：**共享内核意味着"共享命运"（shared fate）。** 所有容器趴在同一个内核上，一旦出现一个能绕过 namespaces/capabilities 的内核提权漏洞（历史上 CVE-2019-5736 runc、CVE-2022-0185 heap overflow 等都让容器逃逸成为现实），整个宿主机一锅端。这就是为什么多租户公有云必须用 VM 做硬隔离（如 AWS Nitro、GCP Shielded VM），而容器更适合"信任边界之内"或"每租户独占一台 VM"。**理解了"容器是进程而非 VM"，你自然就理解了它的安全边界画在哪里。**

## 知识结构图

```mermaid
flowchart TD
    A["容器 = 受约束的普通 Linux 进程<br/>共享同一个内核，无 hypervisor"] --> B["Namespaces<br/>隔离'能不能看见'<br/>给进程造楚门世界"]
    A --> C["Cgroups<br/>限制'能用多少'<br/>给资源画围栏"]
    A --> D["安全三件套<br/>缩小攻击面"]
    A --> E["OverlayFS<br/>镜像分层 + Copy-on-Write"]

    B --> B1["PID — 独立进程树，主进程=PID1"]
    B --> B2["NET — 独立网卡/路由/IP"]
    B --> B3["MNT — 独立挂载视图/rootfs"]
    B --> B4["USER — UID映射，容器root≠宿主root"]
    B --> B5["UTS/IPC/Cgroup/Time"]

    C --> C1["CPU — shares/max 节流"]
    C --> C2["memory — 上限→OOMKilled"]
    C --> C3["IO/pids — 防IO霸占与fork炸弹"]
    C --> C4["v1多树 / v2统一树"]

    D --> D1["Capabilities — 拆root为~40个bit"]
    D --> D2["seccomp — 屏蔽危险系统调用"]
    D --> D3["AppArmor/SELinux — MAC策略"]

    E --> E1["lowerdir 只读镜像层(共享)"]
    E --> E2["upperdir 可写层(每容器独立)"]
    E --> E3["merged 联合视图 → COW"]

    B & C & D & E --> F["OCI runtime: runc<br/>clone/unshare/setns + 写cgroup"]
    F --> G["docker → containerd → runc<br/>CLI 栈"]
    G --> H["统治云的优势:密度/毫秒弹性/不可变/可组合"]
    H --> I["命门:共享内核=共享命运<br/>→ 公有云仍靠VM硬隔离"]
```

## 实战启示

把上面这套机制装进脑子，你在工程实践中会获得一组可以直接兑现的能力：

**第一，看懂容器"出问题"的真正原因。** Kubernetes 里 `OOMKilled` 不是你的程序 bug，是 cgroup memory 控制器按你设的 limit 主动杀人；`CPUThrottling` 是 cpu.max 在节流。**调试的第一步永远是分清"我的代码慢"还是"我被内核限流了"。** 学会读 `/sys/fs/cgroup/` 下的计数文件，能让你在监控告警里一眼定位根因。

**第二，理解 PID 1 的特殊责任，避免踩坑。** 容器的主进程就是 PID 1，而 PID 1 在 Linux 里有两条特殊使命——**转发信号**和**回收僵尸子进程（reaping zombies）**。很多直接把 Node.js / Java 程序塞进容器的人发现：`docker stop` 要等 10 秒才强杀、容器莫名积压僵尸进程，根因就是他们的运行时没有当好 PID 1。解法是用 `tini` 或 dumb-init 这种 init 进程包一层，或者在 Kubernetes 里正确配置 `restartPolicy` 和优雅关闭钩子。**这是"容器=进程"视角直接产出的一个高频生产知识点。**

**第三，资源设置要走 request/limit 两段式。** Kubernetes 的 `request`（调度依据，告诉调度器你要多少）和 `limit`（cgroup 硬上限，内核真的拦住你）是两回事。只设 limit 不设 request，会导致调度器以为节点很空、实则被 cgroup 频繁压制；反过来则容易 OOM。理解它们分别对应 cgroup 的哪条配置，才能调出健康的资源画像。

**第四，镜像与构建优化有了底层依据。** 既然镜像是 OverlayFS 分层 + COW，那"小镜像"（用 alpine/distroless）、"层复用"（指令排序、合并 RUN）、"多阶段构建"（编译镜像和运行镜像分离）就不是玄学，而是直接减少层数和每层大小。**安全上**，用非 root user 跑容器、最小化 capabilities（Kubernetes 的 `securityContext.runAsNonRoot`、`drop ALL`）、跑 seccomp 默认 profile，能把容器逃逸的攻击面压到最小。

**第五，学会在"进程视角"和"容器视角"间切换。** 进容器里 `top`、`netstat`、`ls /proc/1/root` 时，要意识到你看到的是 namespaces 裁剪过的视图；要诊断"宿主机层面到底发生了什么"，得用 `nsenter`、`crictl`、`docker inspect` 这些工具跳出 namespace 看"上帝视角"。**这一对视角的切换，是从"会跑容器"到"懂容器"的分水岭。**

## 延伸学习路径

- **Julia Evans 的 zine《How Containers Work》**——这位以手绘小册子闻名的内核专家，用几页漫画把 namespaces/cgroups 讲得比任何视频都透彻，是入门最佳读物。
- **Liz Rice 的演讲《A Container in 100 Lines of Go》**（Container Camp / YouTube）——从零手写一个迷你 Docker，几十行 Go 调用 `syscall.Clone` 起容器，看完你会彻底祛魅。
- **Linux man pages**：`man 7 namespaces`、`man 7 cgroups`、`man 7 capabilities`、`man 7 user_namespaces`——最权威的一手定义，常读常新。
- **内核文档**：`Documentation/admin-guide/cgroup-v2.rst`（源码树内）——理解 cgroup v2 统一层级语义的最佳出处。
- **OCI 官方规范** runtime-spec / image-spec——读完会发现容器真的"就是一个 JSON 配置 + 一组系统调用"。

## 一句话总结

> 容器没有任何魔法内核——它只是一群被 **namespaces** 蒙住双眼、被 **cgroups** 套上枷锁、再用 capabilities/seccomp 削掉獠牙的**普通 Linux 进程**；Docker 不过是这套古老内核特性的一个漂亮外壳。

## 参考来源

1. [How Docker Actually Works: Namespaces & Cgroups Explained](https://www.youtube.com/watch?v=LTlkYpvPlJo) — 本文分析的核心视频（频道：Doogal \| Senior Software Engineer）
2. [How Docker Actually Works: A Deep Dive into Namespaces and Cgroups — dev.to/Doogal](https://dev.to/doogal/how-docker-actually-works-a-deep-dive-into-namespaces-and-cgroups-5h3e) — 同作者配套长文，系统调用级补充
3. [namespaces(7) — Linux manual page](https://man7.org/linux/man-pages/man7/namespaces.7.html) — 8 种 namespace 的一手权威定义
4. [cgroup v2 — Linux kernel documentation](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html) — 统一层级语义官方文档
5. [Open Container Initiative — Runtime Specification](https://github.com/opencontainers/runtime-spec) — runc 所遵循的容器运行时标准

---
> 本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵**爱卫生**（微信ID: gprshome201101）维护，聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。
> 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
