虚拟化与容器隔离:从 KVM/QEMU 到 namespace 与 cgroup

虚拟化与容器是现代基础设施的两大基石,但它们的隔离模型截然不同。本文从 KVM/QEMU 的硬件虚拟化原理出发,深入 VMX/SVM 指令、EPT 内存虚拟化与 virtio 半虚拟化设备,再剖析容器依赖的 namespace 视图隔离与 cgroup 资源限制,最后对比虚拟机与容器的隔离边界、性能差异与安全风险,帮你建立从 Hypervisor 到 runc 的完整认知链路。

虚拟化与容器是现代云基础设施的两大基石。无论是公有云的虚拟机实例,还是 Kubernetes 上的 Pod,其底层都依赖一套精妙的资源隔离与复用机制。但很多人并不清楚:虚拟机与容器的隔离哲学完全不同——前者靠硬件模拟出一台"完整机器",后者靠内核特性把"同一个 Linux"切割成互相看不见的视图。

本文从 KVM/QEMU 的硬件虚拟化原理出发,依次解剖 CPU 虚拟化(VMX/SVM)、内存虚拟化(EPT/NPT)、设备虚拟化(virtio),再转向容器侧,深入 namespace 的视图隔离与 cgroup 的资源控制,最后给出虚拟机与容器的对比结论与安全风险清单。相关内容可结合 https://plumephp.com/os-overview/(内核态/用户态特权级)与 https://plumephp.com/os-linux-memory/(cgroup 内存限制)阅读。


为什么需要虚拟化与容器?

先厘清一个根本问题:为什么要隔离?隔离的动机通常有三类——安全性(一个程序不能破坏另一个程序)、资源复用(多租户共享物理机)、环境一致性(应用不受宿主机差异影响)。

隔离的三种技术路线

历史上,隔离方案经历了三代演化:

方案隔离粒度代表技术隔离强度开销
进程隔离进程虚拟内存、用户态/内核态中(共享内核)极低
容器隔离进程组 + 视图namespace + cgroup中高(共享内核,但视图隔离)低
虚拟化隔离整台机器KVM/QEMU、Xen、VMware高(独立内核)较高

进程隔离依赖虚拟内存与特权级,其边界是"用户态进程不能访问其他进程的内存"。但这还不够——恶意进程依然可以通过系统调用攻击内核,进而影响同内核上的所有进程。容器把隔离又推进了一步:通过 namespace 让进程"看不见"主机上的其他进程、网络、文件系统;通过 cgroup 让进程"用不完"全部 CPU 与内存。虚拟化则更进一步,Guest 里运行的是完全独立的内核,与宿主机内核之间隔着一层硬件模拟的"虚拟机监视器(Hypervisor)"。

为什么要做成本比较?

一句话概括:虚拟机隔离的是"内核",容器隔离的是"视图"。共享内核的容器高效但风险同源,独占内核的虚拟机安全但开销更高。理解这个权衡,是选择架构的第一步。


KVM/QEMU 硬件虚拟化原理

KVM(Kernel-based Virtual Machine)是 Linux 内核内置的虚拟化模块。它把 CPU 的虚拟化扩展(Intel VT-x / AMD-V)包装成标准接口,让普通进程可以"变身"成一台虚拟机。

虚拟化的三种模式

  • 全虚拟化(Full Virtualization):无需修改 Guest OS,硬件辅助(VMX/SVM)直接执行敏感指令。现代 KVM 走的就是这条路。
  • 半虚拟化(Paravirtualization):Guest OS 知道自己运行在虚拟机上,主动调用 Hypervisor 提供的超调用(hypercall)接口,例如 Xen 的早期方案。virtio 设备本质上是半虚拟化的思路。
  • 容器虚拟化(OS-level):不虚拟硬件,仅在内核层做视图与资源隔离,即容器方案。

KVM 架构:一切皆文件

KVM 的内核接口非常简洁:它暴露了 /dev/kvm 字符设备,用户态程序(通常是 QEMU)通过 open、ioctl 与该设备交互。

int kvm_fd = open("/dev/kvm", O_RDWR);
/* 获取 KVM 版本 */
int version = ioctl(kvm_fd, KVM_GET_API_VERSION, 0);
/* 创建一虚拟机,返回 vm_fd */
int vm_fd = ioctl(kvm_fd, KVM_CREATE_VM, 0);
/* 为虚拟 CPU 分配资源,返回 vcpu_fd */
int vcpu_fd = ioctl(vm_fd, KVM_CREATE_VCPU, 0);
/* 设置寄存器、加载内存,然后进入虚拟执行 */
ioctl(vcpu_fd, KVM_RUN, 0);

关键点在于 KVM_RUN:调用后 CPU 切换到虚拟化模式运行 Guest 代码,直到遇到需要宿主机处理的"陷阱"(VM Exit)才返回用户态。整个 KVM 的运行时就是一个巨大的事件循环。

VMCS / VMX 操作:CPU 怎么"分身"?

Intel VT-x 引入了两种操作模式:VMX root operation(宿主机)与 VMX non-root operation(Guest)。从 root 进入 non-root 的指令是 VMLAUNCH / VMRESUME,从 non-root 退回 root 则发生 VM Exit。

Guest 的 CPU 状态被保存在 VMCS(Virtual Machine Control Structure) 中,包括通用寄存器、段寄存器、控制寄存器、以及控制 VM Exit 行为的"执行控制"字段。每次 VM Exit,CPU 自动把当前状态存入 VMCS,并从宿主机指定的入口点继续执行。

; 简化示意:进入 Guest
mov rax, [guest_rip]
mov rbx, [guest_cr3]
vmresume              ; 返回 non-root 模式,CPU 恢复 Guest 上下文

VM Exit 的原因(exit reason)五花八门:访问了未映射的物理页、执行了 I/O 指令、写控制寄存器、定时器到期等。QEMU 收到 exit reason 后分类处理:需要模拟的(如 PIO/MMIO 访问)交给设备模型,不需要的(如定时器)直接重新注入。

内存虚拟化:两级地址翻译

内存虚拟化是性能核心。Guest 有自己的虚拟地址(GVA)与物理地址(GPA),宿主机又有真实的物理地址(HPA)。传统方案用影子页表(Shadow Page Table)让 MMU 直接使用 GVA→HPA 的映射,但每次 Guest 改页表都要同步,代价高昂。

现代硬件提供 EPT(Intel Extended Page Tables) / NPT(AMD Nested Page Tables):MMU 直接做两级翻译——先查 Guest 页表(GVA→GPA),再查 EPT(GPA→HPA)。两次翻译都命中 TLB 的话开销很小,Guest 改页表也不再打扰宿主机。这也是为什么现代 KVM 性能可以接近裸机。

设备虚拟化:virtio 半虚拟化

早期 QEMU 用软件模拟真实网卡(如 e1000),每个包要经过设备模型,性能极差。virtio 的思路是:给 Guest 一个"虚拟总线",Guest 驱动知道自己在虚拟化环境中,直接与宿主机共享内存环形队列通信。

Guest virtio-net 驱动
        │  写队列(descriptor ring)
        ▼
   vring(共享内存)
        │  通知(kick / doorbell)
        ▼
QEMU vhost 后端 / 内核 vhost-net
        │  TX/RX
        ▼
       物理网卡

virtio 关键三要素是 virtqueue(共享环形队列)、virtio device(PCI 设备)与 notify(通知机制)。因为共享内存 + 减少中断,virtio 的吞吐可以逼近物理网卡。

嵌套虚拟化

现代公有云支持"虚拟机里的虚拟机",依赖 VMCS shadowing 与 EPT 的嵌套支持(nEPT)。Intel 用"VMCS 影子"让内层 Hypervisor 可以创建自己的 VMCS,同时硬件感知嵌套层数。性能仍有损耗,但功能上是完整的。


Namespace:进程的"视界"隔离

容器与虚拟机最大的不同是:容器里没有自己的内核。它靠 namespace 把一组进程放进一个"虚拟世界",这些进程看到的 PID、网络、挂载点、主机名,都只是这个世界里的映射。

八个 namespace

Linux 目前提供 8 类 namespace,分别隔离不同的内核资源:

namespace隔离内容clone flag
Mount(mnt)文件系统挂载点CLONE_NEWNS
UTS主机名 / domain nameCLONE_NEWUTS
IPCSystem V IPC / POSIX 消息队列CLONE_NEWIPC
PID进程号视图CLONE_NEWPID
Network(net)网络协议栈(接口/路由/iptables)CLONE_NEWNET
User用户与组 ID 映射CLONE_NEWUSER
Cgroupcgroup 根目录视图CLONE_NEWCGROUP
Time系统时钟(Linux 5.6+)CLONE_NEWTIME

最值得关注的是 PID namespace:容器内第一个进程 PID 为 1(相当于容器的"init"),容器内只能看到自己命名空间里的进程。若该 PID 1 进程退出,整个容器会被内核杀掉(相当于系统关机)。Network namespace 让每个容器拥有独立的 loopback、网卡与路由表——这就是容器"有自己的 IP"的来源。

clone / unshare / setns

创建 namespace 有三个入口:

# 1) clone():创建新进程时同时创建新的 namespace
# 2) unshare(1):不创建进程,让当前进程脱离原 namespace
unshare --pid --net --uts --fork bash   # 进入一个全新 PID/网络视图的 shell

# 3) setns(2):把当前进程加入一个已存在的 namespace(即 docker exec 的原理)
nsenter -t 12345 -n -m bash              # 进入 PID 12345 的 net/mnt namespace

docker exec 的底层就是 setns:找到容器中某个进程,把它所在的 namespace 附加给新进程。nsenter 是调试容器网络的利器。


cgroup:资源的"额度"控制

namespace 负责"看得见什么",cgroup 负责"能用多少"。没有 cgroup 的限制,一个容器内的进程完全可以把宿主机 CPU 跑满、把内存耗尽拖垮整台机器。

cgroup v2 统一层次

Linux 内核从 4.5 起推进 cgroup v2,并已在主流发行版成为默认。v2 用统一的 cgroup.subtree_control 文件声明子树启用了哪些控制器(controller),一个进程只能属于一个 cgroup,杜绝了 v1 中进程可以属于多个互相矛盾的控制组的问题。

# 挂载 cgroup2
mount -t cgroup2 none /sys/fs/cgroup

# 创建一个控制组
mkdir /sys/fs/cgroup/mycontainer
# 启用 CPU 与内存控制器
echo "+cpu +memory" > /sys/fs/cgroup/cgroup.subtree_control
echo 100000 > /sys/fs/cgroup/mycontainer/cpu.max        # 限制 100000/100000 即 1 核
echo 512M    > /sys/fs/cgroup/mycontainer/memory.max    # 内存上限 512MB
# 把进程放入控制组
echo $$ > /sys/fs/cgroup/mycontainer/cgroup.procs

CPU 控制器

cpu.max 的格式是 quota period:quota 是周期内的配额,period 是周期长度。例如 100000 100000 表示每个 100ms 周期最多用 100ms,即 1 个核。写成 50000 100000 就是半核。这对应 Docker 的 --cpus=0.5。

内存控制器与 OOM

memory.max 限制 cgroup 内所有进程的总内存。当内存使用触及上限且无页可回收时,内核会在该 cgroup 内挑选进程执行 OOM Kill——而不是影响宿主机其他进程。这比全局 OOM 精确得多。memory.high 是软上限,超限先回收页缓存,不立刻杀进程。

cgroup 与 /proc 的矛盾

容器里执行 top 或读 /proc/meminfo 看到的是宿主机数据,因为 /proc 属于宿主机 PID namespace 之外的内核视图。这就是为什么很多容器镜像会提供虚拟化的 /proc(lxcfs 等),把宿主机的内存/CPU 数据翻译成"容器视角"。这与 https://plumephp.com/os-linux-memory/ 中提到的 cgroup v2 内存统计(memory.current / memory.stat)一脉相承。


容器运行时原理:从镜像到运行中的进程

一个容器 = namespace(视图)+ cgroup(配额)+ rootfs(文件系统)+ 若干安全约束(seccomp/capabilities)。容器运行时(runtime)负责把这些零件组装起来。

OCI 与 runc

OCI(Open Container Initiative) 定义了容器镜像与运行时的标准。runc 是参考实现,也是 Docker 与 containerd 的默认底层 runtime。它做的事很朴素:

  1. 创建 namespace(clone 加各种 CLONE_NEW* flag)。
  2. 创建 cgroup 并写入限制。
  3. pivot_root / chroot 切换 rootfs。
  4. 降权、丢 capabilities、加载 seccomp 过滤。
  5. exec 用户指定的进程。
# 用 runc 直接运行一个容器的示意
runc create myctr        # 按 OCI bundle(config.json + rootfs)创建
runc start myctr         # 启动容器内进程
runc exec myctr /bin/sh  # 进入容器
runc delete myctr

overlayfs:分层镜像

容器镜像分层存于宿主机,靠 OverlayFS 合成为"一整个"文件系统。下层(lowerdir)是只读的镜像层,上层(upperdir)是容器自己的可写层。读文件时从上往下找,写文件时在 upperdir 复制出一份(copy-up)。

mount -t overlay overlay \
  -o lowerdir=/lower1:/lower2,upperdir=/upper,workdir=/work \
  /merged

这带来一个有趣的属性:多个容器共享相同的镜像层,只在各自 upperdir 里存增量数据,因此 100 个相同镜像的容器不会消耗 100 份磁盘。这也是 docker build 分层缓存高效的原因。

容器生命周期与 init 进程

容器里 PID 1 是镜像入口点(entrypoint)。它承担"reaper"职责:回收孤儿进程。容器内没有 systemd 管理服务(除非运行完整 systemd 容器),所以信号处理也要靠 PID 1 自己转发——这也是为什么很多容器应用要注意对 SIGTERM 的响应,与 https://plumephp.com/os-linux-signals/ 中信号可靠投递的讨论相关。


虚拟机 vs 容器:隔离边界与性能对比

这是架构选型绕不开的对比。下面这张表从多个维度给出结论:

维度虚拟机(KVM/QEMU)容器(runc)
隔离层级硬件级(独立内核)内核级(共享内核)
Guest 内核完整独立内核无,共享宿主机内核
启动时间秒级(需引导内核)毫秒级(只是起进程)
资源开销高(vCPU/vRAM/设备模拟)极低(一个进程)
隔离强度高(内核崩溃不传染)中(内核漏洞可波及全部容器)
性能损耗中(virtio 已接近裸机)极低(仅 namespace/cgroup 开销)
镜像体积GB 级(含内核)MB 级(仅应用+依赖)
灵活性可运行 Windows/任意 OS只能运行同类内核

为什么 KVM 仍是公有云的底座

容器的安全隔离边界依赖内核自身的安全(seccomp、capabilities、LSM)。一旦内核有漏洞(如脏管道 Dirty Pipe),攻击者理论上可以击穿容器边界。而 KVM 中 Guest 与宿主之间有完整的特权级与页表隔离,Guest 拿不到宿主机 CR3。因此多租户生产环境仍普遍用虚拟机做"租户边界",容器则部署在虚拟机内部。

性能:virtio 已让差距缩小

早年的"虚拟化损耗"主要来自设备模拟。现代 KVM + virtio + vhost 已经让网络与磁盘吞吐逼近裸机,尤其在使用 https://plumephp.com/os-high-performance-io/ 中讨论的 io_uring / polling 之后。容器胜在延迟和密度,虚拟机胜在安全与兼容。


安全与逃逸风险

无论虚拟机还是容器,“逃逸"都是最严重的安全事件——攻击者从 Guest/容器拿到宿主机权限。

  • 容器逃逸常见路径:内核漏洞(如 CVE-2022-0847 脏管道)、有缺陷的挂载(宿主 / 被写挂载进容器)、过强的 capabilities(CAP_SYS_ADMIN)、--privileged 容器、root 进程缺少 seccomp。
  • 虚拟机逃逸:QEMU 设备模拟代码的漏洞(Virtio 设备解析漏洞历史上多次出现)、KVM 自身漏洞。这也是为什么现代云平台给 QEMU 套 seccomp、用 -machine type=q35、开启 KVM 的 kvm-amd/kvm-intel 嵌套限制。
  • 纵深防御:最小权限(non-root、去 capabilities)、只读 rootfs、seccomp 默认黑名单、LSM(AppArmor/SELinux)、定期扫描镜像。关于 LSM 与 seccomp 的机制,可阅读 https://plumephp.com/os-security-hardening-trust/。
# Docker 层面降低逃逸风险的基线配置示例
docker run \
  --cap-drop=ALL \
  --security-opt seccomp=default.json \
  --security-opt apparmor=docker-default \
  --read-only \
  --user 10001:10001 \
  nginx

生产实践:从 Docker 到 Kubernetes 的隔离全景

理解 KVM 与容器原理之后,再看工业界的组装方式就会豁然开朗。

Docker、containerd 与 runc 的分层

很多人混淆 Docker 与容器运行时。实际的分层是:

组件职责类比
Docker CLI用户命令入口(build/run/push)前端
Docker Engine / containerd镜像管理、容器生命周期编排调度大脑
runc真正创建/运行容器进程(OCI runtime)发动机
containerd-shim隔离 runc 与容器引擎,防止引擎退出杀死容器守护

containerd 通过 shim 保持容器进程独立:即使 Docker 守护进程崩溃,容器依然运行——shim 是"容器进程的孤儿院”。

CNI:容器网络的统一接口

Kubernetes 不直接管理容器网络细节,而是通过 CNI(Container Network Interface) 插件(Calico、Cilium、Flannel)为每个 Pod 分配网络。CNI 插件本质上就是在 Pod 的 network namespace 里创建 veth 对、配置路由与 iptables——这些动作的底层都是本文前面讲的 net namespace 与网络栈操作。Cilium 更是直接用 eBPF 替代 iptables,实现高性能网络与安全策略,其技术底座可参考 https://plumephp.com/os-ebpf-observability/。

叠加:虚拟机里的容器

云厂商为了在"虚拟机隔离"之上再提供"容器体验",常见组合是:物理机 → KVM 虚拟机(租户边界)→ 虚拟机内跑 Kubernetes 容器(应用隔离)。这套组合的每一层都依赖前文的机制:

  • 外层:KVM + EPT + virtio(虚拟机隔离,安全边界)
  • 内层:namespace + cgroup(容器隔离,效率工具)
  • 网络:CNI + eBPF(Pod 网络与策略)

理解这三层,就能看懂绝大多数云基础设施的架构图。


KVM 生产调优要点

虚拟机性能不是打开就有,常见调优手段:

# 1) 大页内存:减少 Guest 的 EPT 遍历与宿主机页表压力
#    宿主机开启 hugepages,QEMU 用 -mem-prealloc -mem-path
qemu-system-x86_64 -m 8G -mem-prealloc -mem-path /dev/hugepages \
  -cpu host -enable-kvm ...

# 2) CPU 直通/亲和:vCPU 绑核,避免 vCPU 迁移
taskset -c 0,1 qemu-system-x86_64 ...

# 3) 设备直通(PCI passthrough):把物理网卡/GPU 直接给 Guest
#    用 VFIO + IOMMU,绕过 virtio 模拟,性能接近裸机
modprobe vfio-pci
echo 0000:01:00.0 > /sys/bus/pci/drivers/vfio-pci/bind
调优收益排序(从高到低):
virtio-net/vhost > EPT 大页 > vCPU 绑核 > PCI 直通(场景特定)> 多队列 virtio(多核并行)

注意:设备直通虽然性能极致,但牺牲了迁移能力与弹性,通常只在 GPU/DPDK 等特殊场景使用。


结语

虚拟化与容器,本质上是同一目标(隔离与复用)的两种工程实现。KVM 用硬件辅助在 CPU 里造出"平行宇宙",容器用 namespace 与 cgroup 在同一个内核里切开"视界"。它们各有权衡:虚拟机是安全边界,容器是效率工具。

理解这套体系的关键,不在于背下命令,而在于建立心智模型——知道一次 docker run 背后发生了 clone、写 cgroup、pivot_root、降权这一系列系统调用;知道一次 qemu 启动背后发生了 KVM_CREATE_VM、VMLAUNCH、EPT 翻译与 virtio 队列初始化。这套心智模型,会帮助你在云原生与虚拟化面试中游刃有余,也会帮助你在线上故障时快速定位隔离层问题。


延伸阅读

  1. OCI Runtime Specification 与 runc 源码:github.com/opencontainers/runc
  2. Linux Kernel Documentation: Documentation/virt/kvm/ 与 Documentation/admin-guide/cgroup-v2.rst
  3. Intel SDM Vol. 3C —— VMX 指令与 VMCS 字段
  4. virtio 规范:docs.oasis-open.org/virtio
  5. 深入理解 eBPF 在容器逃逸检测中的应用(结合 https://plumephp.com/os-ebpf-observability/)

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「os」更多文章

  1. 系统启动全流程:从 BIOS/UEFI、GRUB 到内核与 systemd
  2. 操作系统安全加固与可信计算:LSM、内核加固、TPM 与容器逃逸防护
  3. 实时操作系统:RTOS 内核设计、优先级反转与 Linux PREEMPT_RT