容器底层原理:和虚拟机到底差在哪

6 阅读6分钟

零、这篇文章讲什么

第 01 篇说了 Docker 是"应用 + 环境"打包,image / container / registry 三件套。但当时我留了个疑问:容器到底是靠什么机制,能让一堆互不干扰的服务挤在同一台机器上跑的? 很多人(包括我)一开始以为容器就是"轻量虚拟机"——这篇就是要打破这个误会。

读完你会得到三样东西:

  1. 一个明确结论:容器不是 Linux 系统,它只是被"骗"了的 Linux 进程
  2. 三大支柱机制:namespace(隔离)、cgroup(限资源)、overlayfs(分层)
  3. 一张对比表:容器和虚拟机到底差在哪,以及一个残酷的安全现实

前置知识:第 01 篇的 image / container 概念。不需要系统编程经验。

一、容器到底是不是 Linux?

Docker 不是操作系统,容器也不是。 把话说死:

  • Docker 是一个跑在 Linux 上的程序(守护进程 + CLI)
  • 容器是运行在宿主机上的普通 Linux 进程

关键在"共享内核"这四个字。所有容器和宿主机共用同一个 Linux 内核,容器里没有自己的内核——这就是它和虚拟机最根本的区别,也是它轻量的来源。

顺带解答一个常见困惑:你在 Windows / Mac 上用 Docker Desktop,底层其实是起了一个轻量 Linux 虚拟机(WSL2),Docker 跑在那里面。所以容器最终还是在 Linux 上跑的——只是这层对你透明了。

二、先看一个被"骗"的进程

假设你在宿主机上 ps aux,会看到成千上万个进程。但容器里的世界不是这样。进到容器里执行:

docker exec -it node-app bash   # 进到容器内部
ps -ef                          # 容器里看进程
hostname                        # 容器里看主机名

你会发现容器里的进程号从 1 开始、主机名是容器的名字——容器里的进程以为自己生活在一台独立的服务器上,完全不知道自己其实只是宿主机众多进程之一。

这就是容器隔离的本质:不是真的给你一台机器,而是"骗"你,让每个容器都以为自己独占了一台机器。 骗术由三个内核机制组成。

三、三大支柱机制

1. namespace:负责"骗"——隔离

namespace(命名空间)是 Linux 内核的功能,它把系统资源"复制"成多份视图,每个容器看到自己的那一份。隔离的对象是"全局资源",让进程以为这些资源是独占的

namespace隔离什么效果
PID进程号容器里 PID 1,宿主机上是几千
Network网络栈每个容器有自己的 eth0、iptables
Mount挂载点每个容器有一套自己的文件系统
UTS主机名容器里 hostname 是自己的名字
IPC进程间通信容器之间的消息队列互不相通
User用户容器里的 root 不等于宿主机 root

一句话:namespace 负责"看不见"——容器 A 看不见容器 B 的进程、网络、文件系统。

2. cgroup:负责"管"——限资源

namespace 解决"看得见看不见",但它管不了一个问题:资源占用。如果一个容器里的进程狂吃内存,不管 namespace 怎么隔,都会拖垮宿主机。

cgroup(Control Groups,控制组)就是补这块的——限制每个容器能用多少 CPU、内存、磁盘 IO。它是"限速器",保证:

  • 一个容器最坏只能吃掉它分到的配额
  • 不会出现"一个容器把宿主机搞挂,所有容器陪葬"

一句话:cgroup 负责"不许抢"——给每个容器划好资源上限。

3. overlayfs:负责"省"——分层复用

第 01 篇讲过镜像分层。分层是怎么实现的?靠 overlayfs(联合文件系统):多个只读层叠起来,上面加一个可写层,容器只在这一层写。

image 的只读层们(多个容器共享)
        └── 容器专属的可写层(每次改动写在这)
  • 多个容器共享底层镜像 → 省磁盘
  • 改动只进可写层 → 镜像不可变
  • 删容器 = 扔可写层 → 一切回到最初

一句话:overlayfs 负责"省"——用写时复制(copy-on-write)实现镜像复用与容器可写。

四、和虚拟机对比:隔离的不是一个级别

有了三个机制,就能看清容器和虚拟机的本质区别了:

传统虚拟机:
┌────────────── 宿主机 ──────────────┐
│ ┌────────┐ ┌────────┐ ┌────────┐  │
│ │VM1     │ │VM2     │ │VM3     │  │
│ │完整Guest│ │完整Guest│ │完整Guest│  │  ← 每个都自带内核
│ │OS+内核 │ │OS+内核 │ │OS+内核 │  │
│ └────┬───┘ └────┬───┘ └────┬───┘  │
│   Hypervisor(虚拟硬件层)          │
└──────────────┬───────────────────┘
               物理机

Docker 容器:
┌────────────── 宿主机 ──────────────┐
│  ┌────┐ ┌────┐ ┌────┐            │
│  │C1  │ │C2  │ │C3  │  ← 只有 app+依赖   │
│  │app │ │app │ │app │    没有内核        │
│  └─┬──┘ └─┬──┘ └─┬──┘            │
│   └─ Linux 内核(共享)─┘           │
└──────────────┬───────────────────┘
               物理机
维度虚拟机容器
虚拟化对象虚拟硬件虚拟操作系统接口(namespace)
每个实例完整内核 + 用户态只有用户态,共享宿主内核
启动速度分钟级秒级
体积GB 级MB 级
资源开销每台固定分配,有浪费按需,利用率高
隔离强度(内核完全独立)(共享一个内核)

一句话:虚拟机隔离的是"内核",容器隔离的是"进程"。

五、残酷的安全现实:容器隔离 ≠ 安全沙箱

这是最容易踩的认知坑。很多人以为容器带"隔离",就能当沙箱隔离不可信代码——不能

因为所有容器共享同一个内核

  • namespace 挡的是"普通进程看不看得见",不是"攻击者攻不攻得破"
  • 一旦宿主机内核有漏洞,攻击者可以从容器里逃逸(container escape),宿主上所有容器全部沦陷
  • 虚拟机没有这个问题:Guest OS 被攻破也只影响那一个 VM,Hypervisor 是独立安全边界

所以安全等级上:虚拟机 > 容器。如果跑的是不可信的第三方代码、或者做多租户隔离,业界会用"安全容器"方案:

方案思路代表
gVisor在容器外加一层用户态内核拦截系统调用Google
Kata Containers每个容器套一个轻量 VMOpenStack 社区
Firecracker微虚拟化,毫秒级启动的轻量 VMAWS(Lambda / Fargate 就是它)

六、小结

三大支柱 + 一条安全警告,一句话各一个:

机制负责什么一句话
namespace隔离骗进程"我独占一台机器",负责"看不见"
cgroup限资源管住每个容器的 CPU/内存上限,负责"不许抢"
overlayfs分层复用镜像共享 + 容器可写,负责"省"

容器用"隔离强度"换来了"轻量高效";共享内核是它的本钱,也是它最大的安全软肋。