亚毫秒级 VM 沙箱:写时复制的实践
基于写时复制的内存分叉,让虚拟机沙箱能在 1 毫秒内启动,并改变无服务器计算与安全隔离的做法。

启动一个 Docker 容器大约需要 500 毫秒,Firecracker microVM 大约需要 125 毫秒,V8 isolate 大约需要 5 毫秒。但新一代轻量级沙箱利用写时复制(copy-on-write)内存分叉技术,可以在 1 毫秒以内启动一个隔离的执行环境,通常只需 50 到 200 微秒。这个速度足以让每一次函数调用都创建一个全新的沙箱。
这不仅仅是渐进式的改进,而是沙箱能力的质变。当沙箱创建需要 500 毫秒时,你只能少量创建并反复复用;当成本降到 50 微秒时,每一个不可信输入、每一次插件调用、每一个用户请求都可以单独配一个沙箱。安全模型也随之从“隔离租户”转向“隔离每一次操作”。
什么是写时复制
写时复制(CoW)是一种操作系统技术:创建一块内存区域的“副本”时,并不真的复制任何数据。原件和副本指向同一批物理内存页,并都被标记为只读,两者看到的数据完全一致。只有当其中一方尝试写入某个页面时,内核才会拦截这次写操作,只复制那一个页面,然后让写入在副本上完成。
Unix 的 fork() 系统调用从 20 世纪 90 年代起就在使用这一机制。fork 一个进程时,子进程会获得父进程内存的完整副本,但得益于写时复制,实际上并没有复制任何数据。如果子进程像通常那样紧接着调用 exec(),它的内存会被整体替换,写时复制的页面也就直接释放了。这次 fork 的开销几乎为零。
// Classic fork() — copy-on-write in action
pid_t pid = fork();
// At this point:
// - Parent and child share ALL physical memory pages
// - Both have identical virtual address spaces
// - Pages are marked read-only in both
// - Zero data has been copied
if (pid == 0) {
// Child process
// Reading shared_data is free — same physical page
int value = shared_data[0];
// Writing triggers CoW — only THIS page gets copied
shared_data[0] = 42;
// Now child has its own copy of this one page
// Parent's page is unchanged
}
从 fork 到沙箱
写时复制沙箱的核心思路是:不再从零启动一台新的虚拟机或容器,而是先预启动一个“模板”环境(加载好运行时、库和初始状态),再通过写时复制对其进行分叉,瞬间得到副本。每个副本都从模板停下的那个状态开始运行,已经完成初始化、随时可以执行,同时又拥有各自独立的内存空间。
两者的性能差距非常悬殊。传统虚拟机启动要经历:加载内核、初始化硬件、挂载文件系统、启动 init 系统、加载应用代码、初始化运行时。即使做了大量优化(Firecracker 就大幅精简了这些步骤),仍然要花费数百毫秒的初始化时间。
写时复制分叉则完全跳过了这些步骤。模板已经完成了初始化,分叉只需要在微秒级别创建一个预初始化的副本。它的“启动成本”仅仅是内核为新地址空间做的记账工作,以及复制页表项,无论模板占用多少内存,这个开销基本都是固定的几千次操作。
它如何改变游戏规则
无服务器函数
冷启动是无服务器计算的一大痛点。AWS Lambda 的冷启动需要 100 到 500 毫秒,基于 JVM 的运行时还会更久。对于延迟敏感的工作负载来说,这是无法接受的,用户只好让实例保持预热(这就违背了无服务器的初衷),或者接受不可预测的延迟。
借助写时复制沙箱,冷启动可以降到亚毫秒级。每一次调用都可以是一次“冷启动”,因为冷启动本身几乎没有成本。不再需要预热池,空闲实例也不会浪费内存,调用之间也不会残留旧状态。每次函数执行都能获得一个干净、隔离的环境,而无需承担初始化的代价。
插件与扩展系统
安全地运行不可信插件是软件领域最难的问题之一。浏览器用 V8 isolate 为 JavaScript 解决了这个问题。但对于任意代码(如编译型扩展、脚本语言、二进制插件)来说,隔离方案一直很有限:容器太慢,无法支撑每个请求都单独隔离;WebAssembly 则生态和语言支持都有限。
写时复制沙箱提供了一条中间路线:在宿主环境的隔离副本中运行任意代码,且搭建和销毁都在亚毫秒级完成。插件看到的是完整的操作系统环境(文件系统、网络、库),但它的修改被限制在沙箱内,沙箱退出后所有改动都会消失。这非常适合 CI 系统、notebook 环境和构建工具中运行用户提交的代码。
安全隔离
在处理不可信输入时,比如解析上传的 PDF、渲染用户提供的 HTML、执行数据库查询,把这些操作放进隔离沙箱运行,可以限制任何漏洞的影响范围。如果 PDF 解析器存在缓冲区溢出,攻击者拿到的只是一个即将被销毁的一次性沙箱,而不是应用服务器。
这种“为每一次不可信操作都做进程隔离”的做法,在传统沙箱方案下一直不切实际,因为开销远超处理本身的耗时。如果解析一份 PDF 只需 10 毫秒,那花 500 毫秒创建容器显然不划算。但花 100 微秒创建一个写时复制沙箱,成本几乎可以忽略不计。
实现细节
要构建一个实用的写时复制沙箱系统,除了直接调用 fork() 之外,还需要解决好几个问题。
- 内存统计。写时复制让内存用量变得模糊不清。如果模板占用 1 GB,你分叉出 100 个副本,每个副本修改了 10 MB 数据,那么实际物理内存占用约为 2 GB(1 GB 共享 + 100 × 10 MB 独占),而不是 100 GB。内核会区分共享页和私有页,但要准确统计每个沙箱的用量,就需要解析
/proc/[pid]/smaps。 - 文件系统隔离。写时复制只解决了内存问题,文件系统的写入还需要单独隔离。Overlay 文件系统(overlayfs)为文件提供了写时复制语义:沙箱看到的是模板的文件系统,但写入会落到一个独立的层中。沙箱退出时,这一层直接丢弃即可。
- 网络隔离。每个沙箱都需要自己的网络命名空间,以避免相互干扰。Linux 命名空间可以提供这一能力,但创建网络命名空间本身有可测量的开销,因此一些系统会复用预先创建好的命名空间池。
- 资源限制。如果沙箱可以无限制地分配内存或占用 CPU,就会成为拒绝服务攻击的入口。cgroups 可以提供内存、CPU、I/O 等资源限制,但 cgroup 的创建和销毁同样有开销,这里同样可以通过池化来优化。
- 确定性清理。沙箱退出时,它占用的所有资源(内存、文件描述符、网络连接、IPC 对象)都必须被可靠地回收。PID 命名空间在这里很有帮助:只要杀掉命名空间的 init 进程,其所有后代进程也会随之被终止。
写时复制沙箱与 WebAssembly 沙箱
WebAssembly(Wasm)是另一种主流的轻量级沙箱技术。两者在设计上做出了根本不同的取舍,值得放在一起比较。
Wasm 沙箱在内存安全的虚拟机中运行代码,采用线性内存模型。沙箱无法访问线性内存之外的任何东西:没有文件系统,没有网络,也没有系统调用(除非通过 WASI 显式提供)。这种方式安全性极高,但限制也很多:现有代码必须重新编译为 Wasm,而且并非所有语言都能很好地编译到 Wasm。
写时复制沙箱则是在隔离的操作系统环境中运行原生代码。沙箱可以访问完整的操作系统接口(可能会受 seccomp 过滤器限制),能运行任意二进制文件,也能使用常规的系统库。它的限制较少,但安全性也相对较弱:隔离边界是操作系统的进程模型,其攻击面比 Wasm 极简的虚拟机要大。
以下情况适合选择 Wasm:你能控制被沙箱化的代码,你的工作负载可以干净地编译为 Wasm,并且需要尽可能强的隔离性。以下情况适合选择写时复制沙箱:你需要运行任意的现有二进制文件,你的工作负载需要操作系统级的能力(文件系统、网络、子进程),并且你更看重兼容性,而不是极小化的攻击面。
陷阱:多线程程序中的 Fork
fork() 有一个众所周知的陷阱:它只会复制调用它的那个线程。如果父进程有 20 个线程,子进程只会得到一个。其他 19 个线程所持有的互斥锁,在子进程的内存中仍然显示为已被持有,但持有它们的线程已经不存在了。子进程第一次尝试获取这些互斥锁时,就会死锁。
写时复制沙箱系统通常通过确保模板进程在分叉时只有一个线程来规避这个问题。具体做法一般是:先在模板中完成所有初始化(加载库、设置运行时、准备初始状态),然后停止除主线程之外的所有线程,再执行 fork,之后让每个子进程按需重新创建线程。初始化成本只需支付一次,而 fork 则避开了线程带来的隐患。
一些较新的方案使用 userfaultfd 或自定义的缺页处理程序来实现类似写时复制的语义,完全不依赖 fork()。这样可以避开多线程问题,但也增加了复杂性,并且需要更多内核层面的协调。
值得关注的方向
亚毫秒级沙箱仍处于早期阶段,但底层构件已经相当成熟(fork、命名空间、cgroups、overlayfs 都是久经考验的技术)。基于它们构建的系统,正在无服务器计算、CI/CD 和安全代码执行等场景中证明:按操作隔离在规模化场景下是切实可行的。随着这些工具日趋成熟,“沙箱很昂贵”这一假设将会像“垃圾回收太慢,无法用于实时应用”那样过时。开销正在消失,而“沙箱一切”所带来的安全收益,也越来越难以忽视。


