第 19 章 虚拟化与沙箱:抽象层再叠一层

第 15 章讲了”容器”(进程级隔离)。这一章,我们把”隔离”这件事推到极致,讲一个更大的主题:虚拟化和沙箱。

从进程 → 容器 → 虚拟机 → WASM,隔离强度在变,安全边界在变,但本质没变——都是在现有的抽象之上,再叠一层抽象,用”多一层”来换取”多一分隔离”。

而这一章的核心问题,和第 1 章开头那句话呼应:“加一层”能解决问题,但”加一层”也会带来新的问题。


引子:一个”不可信的代码”,你敢让它直接跑吗?

想象两个场景:

场景一:你的 SaaS 平台,允许用户上传并运行"自定义代码"(插件、脚本)。
  用户的代码可能是:挖矿、删文件、偷数据、提权攻击。

场景二:你做一个"函数计算"平台(Serverless),
  租户 A 的函数和租户 B 的函数,跑在同一台机器上。
  租户 A 的函数想偷看租户 B 的数据。

问题:怎么让”不可信的代码”跑起来,又不伤害”你的系统”和其他租户?

答案就是”沙箱”(sandbox)——把代码关在一个”受控的箱子里”,给它资源、限制它的权限、隔离它的影响。

而”沙箱”有很多种强度,从弱到强:

进程(弱隔离):共享内核,隔离靠虚拟内存 + 特权级
容器(中隔离):namespace + cgroup,还是共享内核
虚拟机(强隔离):独立内核,硬件级隔离
WASM(特殊):语言级沙箱,靠类型安全和内存安全

这一章,我们沿着这个”隔离光谱”,逐个拆解。

给你一句总纲:

沙箱的本质,是”用一层新的抽象,来限制’不可信代码’的能力和影响范围”。

每一层抽象(进程、容器、虚拟机、WASM)都是一道”墙”,墙越高(隔离越强),安全越有保障,但成本也越高(越重、越慢)。

所以沙箱设计的问题,永远是:你的”威胁模型”需要多高的墙?


本章路线图

第一幕:隔离光谱
  Q1  从进程到虚拟机,隔离强度是怎么递增的?各自的"墙"是什么?

第二幕:虚拟机
  Q2  Type-1 和 Type-2 Hypervisor 有什么区别?
  Q3  硬件辅助虚拟化(VT-x)解决了什么问题?
  Q4  影子页表到 EPT,是怎么优化"虚拟机里的内存"的?

第三幕:轻量沙箱
  Q5  gVisor 的"用户态内核"是什么?它和容器、VM 什么关系?
  Q6  seccomp-bpf 是怎么"限制系统调用"的?
  Q7  Firecracker 为什么能做到 125ms 启动?

第四幕:WASM 与选型
  Q8  WASM 会不会取代容器?它的沙箱模型是什么?
  Q9  你的服务,该选哪种隔离级别?(决策框架)

第一幕:隔离光谱

Q1 从进程到虚拟机,隔离强度是怎么递增的?各自的”墙”是什么?

这是理解”沙箱”的全局地图。

隔离光谱(从弱到强):

进程(process):
  墙:虚拟内存 + 特权级(第 1、13 章)
  隔离:进程之间隔离(内存、文件描述符)
  共享:内核、硬件
  强度:弱(共享内核,内核 bug 影响所有)

线程(thread):
  墙:无(共享地址空间)
  隔离:几乎没有(只隔离栈)
  共享:进程的所有资源
  强度:最弱

容器(container):
  墙:namespace + cgroup(第 15 章)
  隔离:视野(进程、网络、文件)+ 资源
  共享:内核、硬件
  强度:中(还是共享内核)

gVisor(用户态内核):
  墙:用户态的"内核"(拦截系统调用)
  隔离:容器之上,多了"用户态内核"的检查
  共享:宿主内核(但经过 gVisor 过滤)
  强度:中偏强

虚拟机(VM):
  墙:Hypervisor(硬件级虚拟化)
  隔离:完整机器(独立内核、独立内存)
  共享:物理硬件(CPU、内存)
  强度:强(内核都隔离了)

WASM(WebAssembly):
  墙:语言级沙箱(类型安全 + 内存安全 + 能力受限)
  隔离:内存隔离 + 能力隔离
  共享:宿主进程(但 WASM 的代码被"关"在沙箱里)
  强度:特殊(靠"语言安全"而不是"硬件隔离")

一张对比表:

沙箱墙隔离强度共享启动性能
线程无最弱地址空间最快最快
进程虚拟内存+特权级弱内核快快
容器namespace+cgroup中内核快快
gVisor用户态内核中强宿主内核(过滤)快中
VMHypervisor强硬件慢中
WASM语言沙箱特殊宿主进程快快

核心认知:隔离强度 = “共享的东西有多少”。

  • 共享内核 → 隔离弱(容器、进程)
  • 共享硬件但独立内核 → 隔离强(VM)
  • 共享宿主进程但语言隔离 → 隔离特殊(WASM)

而”共享越多”,性能越好,隔离越弱;“共享越少”,隔离越强,性能越差。

所以选沙箱 = 选”隔离强度 vs 性能成本”的平衡点。


第二幕:虚拟机

Q2 Type-1 和 Type-2 Hypervisor 有什么区别?

Hypervisor(虚拟机监视器):管理虚拟机的软件层,负责”模拟硬件”和”分配资源”。

Type-1(裸机型,bare-metal):

Hypervisor 直接跑在硬件上,不依赖宿主操作系统。

结构:
  硬件
   └── Hypervisor(ESXi、Xen、KVM、Hyper-V)
        ├── 虚拟机 1(自己的 OS)
        ├── 虚拟机 2(自己的 OS)
        └── ...

特点:
  - 性能好(直接管理硬件,无中间宿主 OS)
  - 隔离强(Hypervisor 是"最小可信基")
  - 用于:数据中心、云(AWS、阿里云)

Type-2(宿主型,hosted):

Hypervisor 跑在宿主操作系统之上。

结构:
  硬件
   └── 宿主 OS(Windows、Linux、macOS)
        └── Hypervisor 软件(VirtualBox、VMware Workstation)
             ├── 虚拟机 1
             └── 虚拟机 2

特点:
  - 性能差一点(多一层宿主 OS)
  - 隔离弱一点(宿主 OS 是更大的攻击面)
  - 用于:个人电脑、开发测试

对比:

Type-1(裸机)Type-2(宿主)
位置直接跑在硬件上跑在宿主 OS 上
性能好差一点
隔离强(最小可信基)弱一点
用途云、数据中心个人、开发
例子ESXi、Xen、KVMVirtualBox、VMware

KVM 的特殊性:

KVM(Kernel-based Virtual Machine)是 Type-1 和 Type-2 的混合:
  - 它是"Linux 内核的一个模块",把 Linux 内核变成 Hypervisor
  - 每个虚拟机是 Linux 的一个"进程"
  - 利用了 Linux 的调度、内存管理、驱动

所以 KVM 既"在宿主 OS 里"(像 Type-2),
又"直接管理硬件"(像 Type-1)。
现代云的虚拟机,基本都是 KVM。

Type-1/2 的区别,本质是”Hypervisor 离硬件有多近”:

  • Type-1:Hypervisor 直接管硬件(少一层,快、安全)
  • Type-2:Hypervisor 在宿主 OS 之上(多一层,慢一点、攻击面大)

而”离硬件越近”,“可信基(TCB)越小”,越安全。

Q3 硬件辅助虚拟化(VT-x)解决了什么问题?

问题:早期虚拟化,是怎么”模拟”CPU 的?为什么慢、为什么难?

早期的软件虚拟化(二进制翻译 / 半虚拟化):

问题一:特权指令的问题。

  Guest OS(虚拟机里的操作系统)以为自己运行在 ring 0(内核态),
  但实际它运行在 ring 3(被 Hypervisor 降权了)。

  当 Guest OS 执行"特权指令"(比如改页表、开关中断),
  硬件会报错(ring 3 不能执行特权指令),
  Hypervisor 捕获这个错误,"模拟"这条指令的效果。

  结果:Guest OS 的每次特权操作,都要"陷入 Hypervisor 模拟",
  慢(每次特权指令都要 trap)。

问题二:二进制翻译(像 QEMU 的 TCG):
  把 Guest 的代码翻译成"安全的"宿主代码,逐条翻译,慢。

VT-x(Intel)的硬件辅助虚拟化:

核心思想:CPU 硬件直接支持"虚拟化",不需要软件模拟。

VT-x 提供了:
  1. VMX root / VMX non-root 模式:
     - Hypervisor 跑在 root 模式(最高权限)
     - Guest OS 跑在 non-root 模式(受限)
     - Guest 的特权指令,硬件自动处理,不用软件模拟

  2. VM entry / VM exit:
     - Guest 执行特权指令 → 硬件自动"VM exit"到 Hypervisor
     - Hypervisor 处理完 → "VM entry"回 Guest
     - 硬件加速了"陷入/返回"

  3. 硬件管理虚拟机状态(VMCS):
     - 每个虚拟机的 CPU 状态,存在内存结构里
     - 硬件自动保存/恢复(快)

VT-x 的收益:

1. 快:特权指令由硬件处理,不用软件模拟
2. 简单:Guest OS 不用修改(不用半虚拟化),可以直接跑
3. 安全:硬件隔离,Guest 逃逸更难

代价:VM exit(Guest 陷入 Hypervisor)还是有开销,
  但比"软件模拟特权指令"快得多。

VT-x 的本质:把”虚拟化”从”软件模拟”变成”硬件支持”。

这和很多技术的演进一样:先是软件模拟(慢),然后硬件支持(快)。

  • 虚拟化:软件模拟 → VT-x/AMD-V
  • 浮点运算:软件模拟 → 硬件 FPU
  • 加密:软件算法 → AES 指令

当”软件模拟”成为瓶颈时,“硬件加速”就是答案。

Q4 影子页表到 EPT,是怎么优化”虚拟机里的内存”的?

这是虚拟化里”内存”的经典优化,涉及”两层地址翻译”。

问题:虚拟机里的内存,要经过”两层翻译”。

Guest OS 管理"Guest 虚拟地址 → Guest 物理地址"(第一层页表)
Hypervisor 管理"Guest 物理地址 → 宿主物理地址"(第二层页表)

所以 Guest 里的一次内存访问,要翻译两次:
  Guest 虚拟地址 → Guest 物理地址 → 宿主物理地址

方案一:影子页表(shadow page table,软件方案)。

思路:Hypervisor 维护一张"影子页表",
  直接把"Guest 虚拟地址 → 宿主物理地址"映射好,
  绕过 Guest 的页表。

问题:
  1. 影子页表要和 Guest 的页表"同步"(Guest 改了页表,影子页表也要改)
  2. 同步的成本高(Guest 频繁改页表,影子页表频繁更新)
  3. 这就是"影子页表"的性能瓶颈

方案二:EPT(Extended Page Table,硬件方案,Intel)。

思路:硬件支持"两层页表翻译",不用软件维护影子页表。

EPT 的工作方式:
  1. Guest 用自己的页表(Guest VA → Guest PA)
  2. 硬件再查 EPT(Guest PA → Host PA)
  3. 两层翻译,硬件自动完成

好处:
  1. 不用维护影子页表(省掉了"同步"的开销)
  2. Guest 改自己的页表,不用通知 Hypervisor
  3. 性能好(硬件翻译,且减少 trap)

对比:

影子页表EPT
实现软件(Hypervisor 维护)硬件(CPU 支持)
同步成本高(Guest 改页表要同步)无(硬件翻译两层)
性能中好
TLB一层翻译,TLB 友好两层翻译,TLB miss 更贵

影子页表 → EPT 的演进,又是”软件模拟 → 硬件加速”:

影子页表是”软件维护一张合成的页表”(复杂、要同步), EPT 是”硬件直接支持两层翻译”(简单、快)。

这个演进的启示和第 3 章”多级页表”、第 1 章”TLB”是呼应的: 内存翻译的性能,取决于”翻译的开销”和”缓存的命中率”。 而硬件加速(EPT)和缓存(TLB)是优化翻译的两大手段。


第三幕:轻量沙箱

Q5 gVisor 的”用户态内核”是什么?它和容器、VM 什么关系?

问题:容器(共享内核)隔离弱,VM(独立内核)太重,有没有中间方案?

gVisor(Google 开源):一个”用户态内核”,拦截容器的系统调用。

gVisor 的思路:
  容器的进程,不直接调宿主内核的系统调用,
  而是调 gVisor 的"用户态内核"(Sentry),
  Sentry 再(受限地)调宿主内核。

结构:
  容器进程
   └── 系统调用
        ↓
  Sentry(用户态内核,Go 写的)
   └── 经过过滤的系统调用
        ↓
  宿主内核

效果:
  1. 容器进程看不到宿主内核(只能看到 Sentry 模拟的"内核")
  2. Sentry 是一个"沙箱",过滤了危险系统调用
  3. 比容器更安全(多一层用户态隔离)
  4. 比 VM 更轻(不用完整模拟硬件,共享宿主内核)

gVisor 的定位(光谱的中间):

容器:进程直接调宿主内核(隔离弱)
gVisor:进程调用户态内核,再调宿主内核(隔离中强)
VM:进程跑在独立内核里(隔离强)

gVisor = "容器 + 用户态内核" = 比容器安全,比 VM 轻。

gVisor 的代价:

1. 性能:每个系统调用都要经过 Sentry(用户态),
   比直接调内核慢(几倍到十几倍)
   (尤其 syscall-heavy 的工作负载)

2. 兼容性:Sentry 是"重新实现的内核",
   不可能 100% 兼容 Linux 内核的所有系统调用
   (某些特殊 syscall 不支持)

gVisor 的应用:

- Google Cloud Run、App Engine:多租户 Serverless,用 gVisor 隔离
- 需要"比容器更安全,但比 VM 更轻"的场景

gVisor 的启示:

“隔离强度”和”性能成本”之间,不是”二选一”,而是一个”光谱”。

  • 容器:轻,但隔离弱(共享内核)
  • gVisor:中,隔离中强(用户态内核)
  • VM:重,隔离强(独立内核)

而 gVisor 的价值,是提供了”光谱中间”的一个点: 用”用户态内核”这个”多一层”,换”比容器更强的隔离”,代价是”系统调用变慢”。

这又是”加一层抽象”(第 1 章)的体现:加一层用户态内核,换取更强的隔离。

Q6 seccomp-bpf 是怎么”限制系统调用”的?

问题:容器的进程能调”所有”系统调用,但有些系统调用很危险(比如 mount、reboot、加载内核模块)。怎么限制?

seccomp(Secure Computing Mode):

seccomp 是 Linux 的"系统调用过滤"机制:
  在进程发起系统调用时,检查"这个 syscall 允许吗",
  不允许就拒绝(返回错误,或者 kill 进程)。

seccomp-bpf:用 eBPF(第 13 章)程序来"过滤"系统调用。

seccomp-bpf 的工作方式:

1. 写一个 eBPF 程序,定义"允许哪些 syscall、拒绝哪些"

2. 把 eBPF 程序加载到进程(prctl/seccomp 系统调用)

3. 进程每次发起 syscall:
   - 内核先跑 eBPF 程序
   - eBPF 返回"允许"→ 正常执行
   - eBPF 返回"拒绝"→ 返回错误(或 kill 进程)

效果:
  进程只能调用"白名单"里的系统调用,
  危险的(mount、reboot、ptrace)被拒绝。

seccomp-bpf 的例子(Docker 默认配置):

Docker 默认的 seccomp 配置,禁用了危险的系统调用:
  - mount、umount(文件系统挂载)
  - reboot(重启)
  - ptrace(调试/注入)
  - kexec_load(加载内核)
  - ...

只允许"安全的"系统调用(read、write、open 等)。

seccomp 的价值:

1. 减少攻击面:即使容器被攻破,攻击者也"调不了危险 syscall"
   → 提权攻击(需要 mount、ptrace 等)被阻断

2. 简单:一个 eBPF 程序,声明"白名单"

3. 和 eBPF(第 13 章)结合:eBPF 的"验证器"保证程序安全

seccomp-bpf 的启示:

“最小权限”原则(principle of least privilege)——进程只能调用”它需要的”系统调用。

这个原则在安全领域无处不在:

  • 进程:只给需要的 syscall(seccomp)
  • 容器:只给需要的 capabilities(权限)
  • 微服务:只给需要的数据库权限、API 权限
  • 用户:只给需要的功能权限

核心:权限越小,被攻破后的”破坏力”越小。 这就是”纵深防御”——即使一层被突破,还有下一层限制。

Q7 Firecracker 为什么能做到 125ms 启动?

Firecracker(AWS 开源):专为 Serverless 设计的”微虚拟机”(microVM)。

问题:传统 VM 启动慢(分钟级),怎么做到”毫秒级”?

Firecracker 的设计:

Firecracker 是一个"极简的 Hypervisor":
  1. 基于 KVM(Linux 内核的虚拟化),
     但去掉了所有"不需要的"东西
  2. 只支持"一个虚拟机、一个 vCPU、少量内存"
  3. 用 Rust 写,安全且快

为什么快?
  1. 极简:不模拟完整硬件(没有 BIOS、没有显卡、没有 USB)
     只提供"最小可用的设备"(网络、磁盘)
  2. 轻量:一个 VM 就是一个进程,启动 = 启动进程 + 加载内核
  3. 预启动内核:内核可以预先加载,减少启动时间

125ms 启动的秘诀:

传统 VM 启动慢,因为要:
  1. 启动 BIOS/UEFI(几秒)
  2. 加载内核(几百毫秒)
  3. 初始化设备(几秒)
  → 总共几十秒

Firecracker 省掉了:
  1. 没有 BIOS(用精简的引导)
  2. 预加载内核(不用从磁盘读)
  3. 只有必要设备(网络、磁盘)
  → 总共 125ms

Firecracker 的定位:

容器:启动快(毫秒),但隔离弱(共享内核)
Firecracker:启动也快(125ms),且隔离强(独立内核)
传统 VM:隔离强,但启动慢(分钟)

Firecracker = "VM 的隔离强度 + 容器级的启动速度"

Firecracker 的应用:

AWS Lambda、Fargate:用 Firecracker 跑"函数/容器",
  每个函数/容器一个 microVM,隔离强,启动快。

这就是为什么 Serverless 能"毫秒级冷启动"——
  用 Firecracker 快速启动一个 microVM。

Firecracker 的启示:

“慢”往往不是”本质”,而是”没用的东西太多”。

传统 VM 慢,是因为它模拟了”完整的硬件”(BIOS、显卡、USB), 而 Serverless 只需要”最小可用的机器”。

Firecracker 砍掉所有”不需要的”,就快了。

这和”少做无用功”(第 10 章)、“摊销”(第 12 章)是呼应的: 性能优化的核心,常常是”砍掉不需要的”,而不是”做得更快”。


第四幕:WASM 与选型

Q8 WASM 会不会取代容器?它的沙箱模型是什么?

WASM(WebAssembly):一种”可移植的二进制格式”,设计目标是”在浏览器里跑高性能代码”。

WASM 的沙箱模型(和容器、VM 完全不同):

容器/VM:用"硬件/OS"隔离(虚拟内存、特权级、Hypervisor)
WASM:用"语言/类型系统"隔离(内存安全 + 能力受限)

WASM 的沙箱:
  1. 内存安全:WASM 的代码,只能访问"分配给它的线性内存",
     不能越界(越界会被检查)
     → 没有"缓冲区溢出"(内存安全是"编译期/运行时"保证的)

  2. 能力受限:WASM 代码不能直接"系统调用",
     只能调用"宿主显式提供的"导入函数(imports)
     → 默认"什么都不能做",宿主给什么权限,才能做什么

  3. 类型安全:WASM 的类型系统严格,
     不能"伪造指针"(没有指针运算)

WASM 沙箱的特点:

1. 极轻:WASM 模块很小(几 KB 到几 MB),启动快(微秒级)
2. 快:接近原生速度(JIT 编译)
3. 跨平台:一次编译,到处运行(浏览器、服务器、嵌入式)
4. 隔离强:内存安全 + 能力受限,比容器"理论上"更安全
   (因为没有"共享内核"的攻击面)

WASM 会不会取代容器?

会取代一部分,但不是全部:

WASM 的优势场景:
  - 插件系统(安全地运行不可信插件)
  - 边缘计算(轻量、快速启动)
  - Serverless 函数(启动比容器还快)
  - 浏览器(本来就是 WASM 的主场)

WASM 的劣势(还取代不了容器的场景):
  - 需要"完整 OS 环境"的(文件系统、网络栈、进程)
  - 需要"任意语言"的(WASM 目前主要支持 Rust、C/C++、Go 等)
  - 生态成熟度(容器生态更成熟)

所以:WASM 和容器是"互补",不是"替代"。
  WASM 适合"轻量、快速、安全"的沙箱,
  容器适合"完整环境"的场景。

WASM 的沙箱启示(很重要):

“隔离”不一定靠”硬件/OS”,也可以靠”语言/类型系统”。

  • 容器/VM:运行时隔离(虚拟内存、特权级)
  • WASM:编译期 + 运行时隔离(类型安全、内存安全、能力受限)

WASM 的”内存安全”是”根本性”的——它从语言层面杜绝了”越界访问”, 所以不需要”虚拟内存”这种”运行时检查”来保证隔离。

这和第 7 章”内存模型”、第 13 章”特权级”是呼应的: “安全”可以靠”运行时检查”(贵),也可以靠”语言设计”(从根源消除)。

Q9 你的服务,该选哪种隔离级别?(决策框架)

这一章的落地总结,选沙箱的决策框架。

决策框架:四个问题。

问题一:威胁模型是什么?(跑的是什么代码?)

  1. 可信代码(自己的服务)→ 容器就够了
  2. 半可信代码(合作方插件)→ 容器 + seccomp,或 gVisor
  3. 不可信代码(用户上传的代码)→ VM / WASM / gVisor

问题二:隔离要多强?(被攻破的后果)

  1. 后果轻(只是自己崩)→ 容器
  2. 后果中(可能影响其他租户)→ gVisor / microVM
  3. 后果重(多租户、敏感数据)→ VM

问题三:性能/成本预算?

  1. 极致性能、高密度 → 容器
  2. 平衡 → gVisor
  3. 能接受慢、要强隔离 → VM

问题四:启动速度和弹性?

  1. 毫秒级启动、频繁扩缩 → 容器 / WASM / Firecracker
  2. 分钟级启动、长期运行 → 传统 VM

一个选择表:

场景推荐隔离理由
自己的微服务容器可信代码,容器够用
多租户 ServerlessFirecracker / gVisor不可信 + 要隔离 + 要快
用户上传插件WASM / VM不可信 + 安全第一
数据库/关键服务VM(或裸机)隔离强 + 性能稳
CI/CD 构建容器 + seccomp半可信,容器 + 加固
边缘计算WASM / Firecracker轻量 + 快启动

一个关键认知:隔离是”分层”的,不是”单一”的。

实际系统往往"多层隔离":
  容器(第一层)+ seccomp(第二层)+ gVisor/VM(第三层)

例:K8s 里跑容器,容器里有 seccomp,宿主机是 VM(云上)
  三层隔离,纵深防御。

所以"选隔离级别"不是"选一个",而是"叠加几层"。

这一章的最终落脚点:

沙箱是”用抽象换隔离”。每一层抽象(进程、容器、gVisor、VM、WASM)都是一道墙。

而”加一层”的代价,是”性能和成本”(第 1 章的核心思想)。

所以选沙箱,本质是回答:

  1. 威胁有多严重?(决定墙要多高)
  2. 性能成本能接受多少?(决定墙能多高)

而真正的高手,会用”多层隔离”(纵深防御),而不是”赌一层墙够高”。


思考题

思考题 1

面试官问:“容器和虚拟机有什么区别?什么时候用容器,什么时候用虚拟机?“请回答,并说明 gVisor、Firecracker 这类”中间方案”的出现说明了什么。

参考答案

核心答案:容器隔离进程(共享内核),虚拟机隔离硬件(独立内核)。容器轻但隔离弱,VM 重但隔离强。中间方案(gVisor、Firecracker)说明”隔离强度 vs 成本”是个光谱,不是二选一。

容器 vs 虚拟机:

容器:
  隔离:namespace(视野)+ cgroup(资源)
  共享:宿主机内核
  隔离强度:中(共享内核)
  启动:快(秒级)
  密度:高(一台机器几十个)
  用途:微服务、可信代码

虚拟机:
  隔离:Hypervisor(硬件级)
  共享:物理硬件
  隔离强度:强(独立内核)
  启动:慢(分钟级)
  密度:低(一台机器几个)
  用途:多租户、不可信代码、需要强隔离

什么时候用容器,什么时候用 VM:

用容器:
  - 可信代码(自己的服务)
  - 高密度、快速扩缩
  - 性能敏感
  - 微服务、DevOps

用 VM:
  - 不可信代码(多租户、用户上传代码)
  - 需要强隔离(安全敏感、合规)
  - 需要完整 OS 环境
  - 传统应用(不好容器化)

中间方案说明什么:

gVisor(用户态内核):
  比容器安全(多一层用户态内核过滤),比 VM 轻(共享宿主内核)
  说明:容器"共享内核"有风险,但 VM"独立内核"太重,
    所以需要"中间地带"。

Firecracker(microVM):
  VM 的隔离强度 + 容器级的启动速度(125ms)
  说明:传统 VM"慢"不是本质,是"模拟了太多不需要的硬件",
    砍掉不需要的,就能又快又隔离。

这两个方案说明:
  1. "隔离强度"和"成本"是光谱,不是二选一
  2. 需求在变化(Serverless 要"快 + 隔离"),
     催生了"光谱中间"的方案
  3. 没有"银弹",只有"匹配场景"

这道题考察什么:

  1. 知不知道容器和 VM 的核心区别(隔离层次)
  2. 知不知道各自的适用场景
  3. 知不知道中间方案(gVisor、Firecracker)的定位
  4. 能不能说出”光谱”这个认知(不是二选一)

思考题 2

你的 SaaS 平台要支持”用户上传自定义脚本”(用户写代码,在你的平台执行)。用户可能是恶意的(挖矿、偷数据、攻击)。你会怎么设计沙箱?给出完整的方案。

参考答案

这是一道”沙箱设计”的综合题,核心是”多层隔离 + 最小权限 + 资源限制”。

需求分析:

威胁:用户代码可能:
  1. 挖矿(占 CPU)
  2. 偷数据(读宿主的文件、环境变量、网络)
  3. 攻击(提权、横向移动、DDoS)
  4. 破坏(删文件、改配置)

所以沙箱要:隔离(不能碰宿主)、限资源(不能占满)、限能力(不能乱调)

方案设计(多层隔离):

第一层:强隔离的执行环境(VM 或 WASM)。

用户代码是不可信的,容器(共享内核)不够安全。

选择一:VM(如 Firecracker)
  - 每个用户脚本,跑在一个 microVM 里
  - 强隔离(独立内核)
  - 快启动(125ms)

选择二:WASM
  - 用户脚本编译成 WASM
  - 内存安全 + 能力受限(默认无权限)
  - 极快(微秒级启动)

选择:如果脚本是"通用语言"(Python、JS)→ VM
      如果脚本能编译成 WASM(Rust、C/C++)→ WASM

第二层:资源限制(cgroup / 配额)。

限制用户脚本的资源,防止挖矿、资源耗尽:
  - CPU:限制 CPU 配额(比如最多 1 核)
  - 内存:限制内存(比如最多 512MB)
  - I/O:限制磁盘读写
  - 网络:限制带宽、禁止访问内网
  - 超时:脚本最多运行 10 秒

防止:一个用户的挖矿脚本,拖垮整个平台。

第三层:能力限制(seccomp / capabilities)。

限制脚本的系统调用和权限:
  - seccomp:只允许"必要的"系统调用(read、write、计算)
    禁止 mount、ptrace、reboot 等危险调用
  - capabilities:不给 root 权限,不给特殊能力
  - 只读文件系统:脚本不能改宿主文件
  - 网络隔离:默认无网络,需要时单独授权

第四层:输入输出限制。

限制脚本能"看到"和"访问"什么:
  - 环境变量:不传敏感环境变量(密钥、数据库密码)
  - 文件系统:脚本只能访问"自己的目录"
  - 网络:默认隔离,白名单授权
  - 输出:限制输出大小(防止输出炸弹)

一个完整的执行流程:

1. 用户提交脚本
2. 平台创建沙箱环境(microVM 或 WASM 实例)
3. 注入"必要的输入"(脚本要处理的数据)
4. 执行脚本(带资源限制、能力限制、超时)
5. 收集输出(限制大小)
6. 销毁沙箱环境

监控:
  - 记录资源使用(CPU、内存、网络)
  - 检测异常(挖矿特征、攻击行为)
  - 超时/超资源的,强制终止

关键设计原则:

1. 默认拒绝(deny by default):
   脚本默认"什么都不能做",需要什么,显式授权

2. 最小权限:
   只给"完成任务需要"的最小权限

3. 纵深防御:
   VM/WASM(强隔离)+ cgroup(限资源)+ seccomp(限能力)
   → 多层,一层被突破,还有下一层

4. 资源隔离 + 超时:
   每个脚本独立配额,超时强杀
   → 防止"一个脚本拖垮平台"

这道题考察什么:

  1. 知不知道”用户代码不可信,要用强隔离”(VM/WASM)
  2. 知不知道”多层隔离”(VM + cgroup + seccomp)
  3. 知不知道”默认拒绝、最小权限”原则
  4. 知不知道”资源限制 + 超时”(防止挖矿、资源耗尽)

思考题 3

WASM 的沙箱和容器的沙箱,本质区别是什么?为什么说 WASM 是”语言级沙箱”,而容器是”运行时沙箱”?它们各自的优劣是什么?

参考答案

核心答案:容器是”运行时隔离”(靠虚拟内存、特权级在运行时检查),WASM 是”语言级隔离”(靠类型安全、内存安全在编译期/运行时从根源消除越界)。

容器的沙箱(运行时隔离):

机制:
  虚拟内存(第 1 章)+ 特权级(第 13 章)+ namespace(第 15 章)

特点:
  1. 运行时检查:每次内存访问,硬件检查"地址是否越界"
  2. 隔离靠"硬件 + OS":MMU、特权级、namespace
  3. 共享内核:容器之间共享宿主内核(攻击面)

优点:
  - 支持任意语言(不限制语言)
  - 完整 OS 环境(文件系统、网络、进程)
  - 生态成熟

缺点:
  - 共享内核,隔离不如 VM
  - 运行时检查有开销(地址翻译、特权级切换)

WASM 的沙箱(语言级隔离):

机制:
  类型安全 + 内存安全 + 能力受限(capability-based)

特点:
  1. 内存安全:WASM 代码只能访问"分配给它的线性内存",
     越界访问在"编译期/运行时"被类型系统阻止
  2. 能力受限:WASM 不能"系统调用",只能调"宿主显式提供的"导入函数
  3. 无指针运算:不能"伪造指针"访问任意内存

优点:
  - 极轻(模块小、启动快)
  - 快(接近原生)
  - 跨平台
  - 隔离"理论上"更根本(内存安全从语言层面保证)

缺点:
  - 语言受限(主要 Rust、C/C++、Go 等)
  - 没有完整 OS 环境(要宿主提供)
  - 生态较新

本质区别:

容器的隔离 = "事后检查"(运行时,越界了才发现)
  → 依赖硬件 MMU 在"运行时"拦截越界访问

WASM 的隔离 = "事前杜绝"(编译期,越界根本写不出来)
  → 依赖类型系统在"编译期"保证不越界

类比:
  容器 = 围墙(越界了,墙拦住你)
  WASM = 没有越界的可能(语言上就没法越界)

各自的优劣总结:

容器WASM
隔离方式运行时(MMU/特权级)语言(类型安全)
隔离强度中(共享内核)强(内存安全根本)
语言限制无受限
OS 环境完整宿主提供
性能快接近原生
启动秒级微秒级
生态成熟较新

一个深刻的观察:

“隔离”有两个实现路径:

  1. 运行时隔离(容器/VM):用”硬件检查”拦截越界(贵,但通用)
  2. 语言级隔离(WASM/Rust):用”类型系统”从根源消除越界(快,但语言受限)

而”语言级隔离”是更”根本”的——它不是在”越界发生”时拦截,而是让”越界”从一开始就”无法表达”。

这就是为什么 Rust 的”内存安全”、WASM 的”沙箱”被认为是”安全”的未来方向: 把安全做进”语言/类型系统”,而不是依赖”运行时的检查”。

这道题考察什么:

  1. 知不知道”运行时隔离”和”语言级隔离”的本质区别
  2. 知不知道 WASM 的内存安全、能力受限
  3. 能不能说出”事前杜绝 vs 事后检查”这个深刻区别
  4. 知不知道各自的优劣(语言限制 vs 共享内核)

思考题 4(开放题)

“隔离 = 抽象层再叠一层。“从进程、容器、gVisor、虚拟机、WASM 这个光谱,你能总结出”隔离”的统一本质吗?以及”叠一层”的代价是什么?

参考答案

开放题,考察你能不能把”隔离”抽象成统一思想,并理解”抽象”的代价。

隔离的统一本质:

隔离 = 在”执行者”和”资源”之间,插入一层”受控的中间层”,限制”执行者能对资源做什么”。

进程:执行者(进程)+ 中间层(虚拟内存 + 特权级)+ 资源(内存、硬件)
容器:执行者(进程)+ 中间层(namespace + cgroup)+ 资源(系统)
gVisor:执行者(进程)+ 中间层(用户态内核)+ 资源(宿主内核)
VM:执行者(Guest OS)+ 中间层(Hypervisor)+ 资源(硬件)
WASM:执行者(代码)+ 中间层(类型系统 + 能力)+ 资源(宿主能力)

每一层的”中间层”,做的事都一样:

1. 限制"能看到什么"(视野隔离)
   进程:虚拟内存(看不到别人的内存)
   容器:namespace(看不到别的进程/网络)
   WASM:能力受限(看不到没授权的资源)

2. 限制"能做什么"(能力隔离)
   进程:特权级(不能碰硬件)
   容器:cgroup(不能超配额)
   seccomp:不能调危险 syscall

3. 限制"影响范围"(故障隔离)
   进程:崩了不影响别人
   VM:内核崩了只影响这个 VM

“叠一层”的代价(第 1 章的核心呼应):

1. 性能代价:
   每叠一层,操作要"经过"这一层(翻译、检查、拦截)
   进程:地址翻译(TLB 缓解)
   容器:namespace 查找
   gVisor:系统调用经过用户态(慢几倍)
   VM:VM exit(Guest 陷入 Hypervisor)

2. 复杂度代价:
   每叠一层,多一个"要维护、要调试"的组件
   而且"抽象的泄漏"(第 1 章)——每一层都会"泄漏"一些真相

3. 资源代价:
   每叠一层,占一些内存、CPU
   VM 最明显(完整 OS)

一个统一的认知(这是这道题的核心):

“隔离”和”性能”是一对根本矛盾:

  • 隔离靠”加一层”(中间层)
  • 而”加一层”就有代价(性能、复杂度、资源)

所以:

  • 隔离越强(层越多、越厚),越安全,但越慢、越重
  • 隔离越弱(层越少、越薄),越快、越轻,但越危险

而”正确的隔离级别”,取决于”威胁模型”(需要多安全)和”性能预算”(能承受多慢)。

这和第 1 章”加一层抽象”的呼应:

第 1 章:"计算机科学任何问题,都可以通过增加一个中间层来解决,
  但你会多出一堆新的问题。"

本章验证了这句话:
  隔离问题 → 加一层(进程/容器/gVisor/VM/WASM)→ 解决了隔离
  但多出了新问题 → 性能下降、复杂度上升、抽象泄漏

而高手的能力,就是:
  1. 知道"加哪一层"(选择合适的抽象)
  2. 知道"加多厚"(平衡隔离和性能)
  3. 知道"新问题是什么"(预见抽象的代价)

这道题的价值:

它把整本书的”加一层抽象”主线,在”隔离”这个主题上做了总结:

隔离 = 加一层(受控中间层) 代价 = 性能、复杂度、资源 权衡 = 威胁模型 vs 性能预算

而理解了”隔离的统一本质”,你就能:

  • 理解任何”沙箱”技术(进程、容器、VM、WASM)的共同逻辑
  • 预判”加一层隔离”的代价
  • 选择合适的隔离级别(而不是盲目追求”最强隔离”)

一页纸总结

┌──────────────────────────────────────────────────────────────┐
│  第 19 章 · 虚拟化与沙箱:抽象层再叠一层                          │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  【隔离光谱】                                                   │
│    线程 < 进程 < 容器 < gVisor < VM,隔离递增,成本递增          │
│    WASM = 语言级沙箱(特殊)                                    │
│    隔离强度 = 共享的东西有多少                                   │
│                                                              │
│  【虚拟机】                                                     │
│    Type-1(裸机)vs Type-2(宿主):Hypervisor 离硬件多近         │
│    VT-x:硬件辅助虚拟化(特权指令不用软件模拟)                   │
│    影子页表 → EPT:软件模拟 → 硬件加速(两层地址翻译)             │
│                                                              │
│  【轻量沙箱】                                                   │
│    gVisor:用户态内核(容器 + 多一层过滤)                        │
│    seccomp-bpf:用 eBPF 限制系统调用(最小权限)                  │
│    Firecracker:极简 microVM(砍掉不需要的,125ms 启动)          │
│                                                              │
│  【WASM】                                                      │
│    语言级沙箱:类型安全 + 内存安全 + 能力受限                     │
│    vs 容器的"运行时隔离":事前杜绝 vs 事后检查                    │
│    WASM 和容器是互补,不是替代                                   │
│                                                              │
│  【选型框架】                                                   │
│    威胁模型(代码可信吗?)→ 隔离强度                            │
│    性能预算(能承受多慢?)→ 隔离成本                            │
│    实际用"多层隔离"(纵深防御),不是"赌一层够高"                  │
│                                                              │
│  【一句话】                                                     │
│    隔离 = 加一层受控中间层,代价是性能和复杂度;                    │
│    墙越高越安全,但越慢越重——选墙高,看威胁,也看预算。           │
│                                                              │
│  【下一章预告】                                                  │
│    最后一章,回到一个元问题:这么复杂的系统,怎么看得懂它?         │
│    可观测性:内核怎么让不可见的东西可见——tracepoint、eBPF、火焰图。│
└──────────────────────────────────────────────────────────────┘