本章沿一次 Linux COW 实例的状态前进:先创建空的 memfd,按模块声明的最大 Wasm 页数预留实例地址范围;参考 Guest 完成初始化后,把线性内存捕获并封存;新 Guest 先完成 _initialize,再在自己的 mapping base 上 attach sealed image;增长只扩大当前可见范围;最后分别释放实例映射和 Runner 的 fd。
需要保持三个单位的区别:Wasm page 固定为 64 KiB,memory.grow 按它计数;OS page 由 unix.Getpagesize() 返回,mmap、地址对齐和私有化按它工作;Guest memory 是 Wasm 可见字节,Host state(Go globals、工具、WASI、执行栈等)不在 memfd image 中。mapping base 是一个实例的 Host 虚拟地址起点,不是 Guest 地址协议,也不要求不同实例相同。
1. Linux 文件只提供 Linux 实现
cow_linux.go · L1–L20 · 打开完整源码
//go:build linux
package spine
import (
"errors"
"fmt"
"runtime"
"sync"
"unsafe"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental"
"golang.org/x/sys/unix"
)
const wasmPageSize = 65536
var cowPageSize = unix.Getpagesize()
构建标签使这份文件只在 Linux 参与编译;非 Linux 使用 cow-platform.md 的构造器。导入中的 unix 提供 memfd、mmap、seal、Pwrite、Munmap 等系统调用,unsafe 只在 attach 时观察已有 slice 的起点,runtime.KeepAlive 保证 unsafe 调用结束前该 slice 仍被视为存活。
wasmPageSize = 65536 是 Guest 内存的协议单位;cowPageSize 则从内核查询 OS page 大小。两者可能相同,但代码不能依赖这种巧合:Wasm 增长用前者,文件长度和 mapping 地址检查用后者。
2. 两个结构体分别持有什么
cow_linux.go · L21–L41 · 打开完整源码
type linuxCOW struct {
fd int
imageSize uint64
closed bool
}
type linuxCOWMemory struct {
mapping []byte
max int
mu sync.Mutex
}
func newCOWRuntime() (cowRuntime, error) {
fd, err := unix.MemfdCreate("pysolate-spine-cow", unix.MFD_CLOEXEC|unix.MFD_ALLOW_SEALING)
if err != nil {
return nil, fmt.Errorf("create COW image memfd: %w", err)
}
return &linuxCOW{fd: fd}, nil
}
func (c *linuxCOW) allocator(def api.MemoryDefinition) (cowMemory, error) {
linuxCOW 是 Runner 级对象,持有 memfd、已提交的 imageSize 和 closed 状态。新建 runtime 只调用 MemfdCreate,得到一个允许加 seals 的空文件;此时 fd 存在,imageSize 仍为零,ready() 必须返回假。
linuxCOWMemory 是实例级对象。mapping 是对一段 Host 虚拟地址范围的 Go slice 描述,实际页由内核映射承载;max 是预留范围的字节上限;mu 保护增长与释放之间的状态。这个 slice 不是 Go 堆里的完整镜像,也不能在实例释放后继续当作有效 Guest memory 使用。
3. 初始化:按最大 Wasm 页数预留 mapping
cow_linux.go · L41–L63 · 打开完整源码
func (c *linuxCOW) allocator(def api.MemoryDefinition) (cowMemory, error) {
if def == nil {
return nil, errors.New("COW needs an exported memory named memory")
}
maximum, bounded := def.Max()
if !bounded {
return nil, errors.New("COW needs a declared memory maximum")
}
return newLinuxCOWMemory(uint64(maximum) * wasmPageSize)
}
func newLinuxCOWMemory(maximum uint64) (*linuxCOWMemory, error) {
if maximum == 0 || maximum > uint64(maxInt()) {
return nil, fmt.Errorf("invalid COW memory maximum: %d", maximum)
}
mapping, err := unix.Mmap(-1, 0, int(maximum), unix.PROT_READ|unix.PROT_WRITE, unix.MAP_PRIVATE|unix.MAP_ANONYMOUS)
if err != nil {
return nil, fmt.Errorf("reserve COW linear memory (%d bytes): %w", maximum, err)
}
return &linuxCOWMemory{mapping: mapping, max: int(maximum)}, nil
}
// The one-memory Guest gets the range reserved before InstantiateModule.
allocator 先要求模块存在导出的 memory 定义,并要求该定义声明 maximum。COW 需要一个已知上限,才能在 InstantiateModule 之前为每个实例预留完整地址范围。代码把最大 Wasm 页数乘以 64 KiB 得到最大字节数,再检查它是否非零且能放进 Go int。
newLinuxCOWMemory 用 MAP_PRIVATE|MAP_ANONYMOUS 创建可读写匿名范围。这里还没有映射 memfd,也没有捕获 image;它只是给 wazero 一个实例专属的 mapping base 和最大容量。后续 Reallocate 会用这个范围的前缀表示 Guest 当前可见长度,尾部仍属于预留范围。
例如最大值为 8192 个 Wasm 页时,预留的是 512 MiB 的虚拟地址范围,不等于立即占有 512 MiB 独占物理内存。这个实现只建立地址和生命周期条件,不从预留大小推出性能结果。
错误集中在初始化阶段:memory 定义缺失、没有 declared maximum、最大值为零或超出 int、mmap 失败都会返回错误;它们不会创建一个没有明确上限的可用 COW memory。
4. 实例增长:固定起点,改变可见前缀
cow_linux.go · L63–L78 · 打开完整源码
// The one-memory Guest gets the range reserved before InstantiateModule.
func (m *linuxCOWMemory) Allocate(_, _ uint64) experimental.LinearMemory {
return m
}
func (m *linuxCOWMemory) Reallocate(size uint64) []byte {
m.mu.Lock()
defer m.mu.Unlock()
if m.mapping == nil {
return nil
}
if size > uint64(m.max) || size > uint64(maxInt()) {
return nil
}
return m.mapping[:int(size):m.max]
}
Allocate 直接返回已经预留的 linuxCOWMemory,因为范围在实例化前就准备好了。Reallocate 加锁后检查 mapping 是否已释放,再拒绝超过 max 或 int 上限的字节数,最后返回 m.mapping[:size:max]。三索引 slice 让当前长度变成 size,同时把容量限制在预留上限;它不移动起点,也不把尾部复制到别处。
这里的 size 是字节数。wazero 的 Wasm Memory.Grow 会按 Wasm page 计算并通过 allocator 调整它;新增的匿名尾部应当读为零。Linux OS page 只参与底层映射和私有化,不改变 Memory.Grow 的 Wasm page 语义。
5. 实例释放:Free 和 release 的职责不同
cow_linux.go · L80–L95 · 打开完整源码
func (m *linuxCOWMemory) Free() {
// Wazero's allocator hook has no error return.
_ = m.release()
}
func (m *linuxCOWMemory) release() error {
m.mu.Lock()
defer m.mu.Unlock()
if m.mapping != nil {
if err := unix.Munmap(m.mapping); err != nil {
return err
}
m.mapping = nil
}
return nil
}
wazero 的 Free 没有错误返回,因此它调用带错误的 release 后丢弃错误。release 自己加锁;如果 mapping 还存在,就调用 Munmap,成功后把 mapping 置为 nil。再次 release 或 Free 不会重复解除同一范围;释放后 Reallocate 返回 nil。
如果 Munmap 失败,release 保留原 mapping 并返回错误,避免把未完成的释放报告成成功。这个方法只处理实例级匿名范围和 attach 后的映射,不关闭 linuxCOW.fd。fd 属于 Runner,必须等所有实例结束后由 close 处理。
6. 基线捕获:Guest memory 写入 memfd
cow_linux.go · L97–L108 · 打开完整源码
func (c *linuxCOW) capture(memory api.Memory) error {
if memory == nil || memory.Size() == 0 {
return errors.New("cannot capture empty COW memory")
}
data, ok := memory.Read(0, memory.Size())
if !ok {
return errors.New("cannot read initialized Guest memory for COW image")
}
return c.captureBytes(data)
}
// captureBytes writes directly from the allocator-backed mapping into the
capture 要求输入的 api.Memory 非空且有字节,然后从偏移 0 读取当前 Guest 可见内容,交给 captureBytes。这段读取发生在参考 Guest 完成 _initialize 和 CPython init 之后,调用顺序由 prepared.go 保证。
memory.Read 返回借用 view;captureBytes 在写入 fd 时使用它,但不会把它作为 Runner 的长期 Go image 保存。COW 的基线所有者是封存后的内核文件对象,Runner 只通过 linuxCOW 持有 fd 和 image 大小。
7. 提交基线:对齐、写入、封存
cow_linux.go · L108–L141 · 打开完整源码
// captureBytes writes directly from the allocator-backed mapping into the
// sealed memfd. It deliberately does not retain a Go-owned image copy.
func (c *linuxCOW) captureBytes(data []byte) error {
if len(data) == 0 || len(data)%cowPageSize != 0 || len(data)%wasmPageSize != 0 {
return fmt.Errorf("COW image size must be Wasm-page aligned: %d", len(data))
}
if c.closed {
return errors.New("COW image is closed")
}
if c.imageSize != 0 {
return errors.New("COW image already captured")
}
if err := unix.Ftruncate(c.fd, int64(len(data))); err != nil {
return fmt.Errorf("size COW image: %w", err)
}
for offset := 0; offset < len(data); {
n, err := unix.Pwrite(c.fd, data[offset:], int64(offset))
if err != nil {
return fmt.Errorf("write COW image: %w", err)
}
if n == 0 {
return errors.New("short write while creating COW image")
}
offset += n
}
seals := unix.F_SEAL_SHRINK | unix.F_SEAL_GROW | unix.F_SEAL_WRITE | unix.F_SEAL_SEAL
if _, err := unix.FcntlInt(uintptr(c.fd), unix.F_ADD_SEALS, seals); err != nil {
return fmt.Errorf("seal COW image: %w", err)
}
c.imageSize = uint64(len(data))
return nil
}
func (c *linuxCOW) ready() bool {
captureBytes 先要求长度非零,并同时满足 OS page 和 Wasm page 对齐。前者是文件映射和地址操作的条件,后者保证 Guest 内存形状按 Wasm page 表示。它还拒绝已关闭或已经捕获过的 runtime。
随后代码把 memfd 截断到 image 长度,用带明确 offset 的 Pwrite 循环写完所有字节;部分写入继续,写错误或零字节写立即结束。写完后一次性加入 F_SEAL_SHRINK、F_SEAL_GROW、F_SEAL_WRITE 和 F_SEAL_SEAL。只有加 seals 成功,才把 imageSize 写入对象,因此 ready() 不会把半成品当成基线。
这一段的状态提交点是 imageSize = len(data)。在它之前,fd 可能已截断或写入但仍不算可 attach 的 image;在它之后,capture 被视为完成且不能重复。失败按“长度/状态检查、文件大小、写入、加 seal”分组返回,调用者负责关闭构造阶段的 Guest 和 Runner。
8. attach:初始化之后在原 mapping base 替换前缀
cow_linux.go · L141–L160 · 打开完整源码
func (c *linuxCOW) ready() bool {
return !c.closed && c.imageSize != 0
}
func (c *linuxCOW) attach(memory api.Memory) error {
if memory == nil {
return errors.New("cannot attach COW image to nil memory")
}
if uint64(memory.Size()) < c.imageSize {
pages := uint32((c.imageSize - uint64(memory.Size())) / wasmPageSize)
if _, ok := memory.Grow(pages); !ok {
return errors.New("cannot grow memory to COW image")
}
}
data, ok := memory.Read(0, memory.Size())
if !ok {
return errors.New("cannot inspect Guest memory for COW attach")
}
return c.attachBytes(data)
}
ready 只有在 runtime 未关闭且 imageSize 非零时为真。attach 先检查目标 Guest,然后在其当前大小小于 image 时按 Wasm page 增长;它不主动缩小已经存在的实例。接着读取当前 Guest memory,目的不是复制这些字节,而是取得 wazero 当前持有的 mapping base,并把这个地址交给 attachBytes。
因此 attach 必须发生在 _initialize 之后:初始化先在实例的匿名范围中完成,attach 再替换 image 覆盖的前缀。匿名尾部保持本实例的映射,之后仍可按 Wasm page 增长。
9. MAP_PRIVATE|MAP_FIXED 提供写隔离
cow_linux.go · L162–L185 · 打开完整源码
// attachBytes replaces exactly the current mapping range. MAP_PRIVATE keeps
// the sealed file pages shared until a Guest write faults a page private.
func (c *linuxCOW) attachBytes(data []byte) error {
if c.closed {
return errors.New("COW image is closed")
}
if c.imageSize == 0 || uint64(len(data)) != c.imageSize {
return fmt.Errorf("COW memory shape changed: got %d, want %d", len(data), c.imageSize)
}
if len(data) == 0 || uintptr(unsafe.Pointer(unsafe.SliceData(data)))%uintptr(cowPageSize) != 0 {
return errors.New("COW memory address is not page-aligned")
}
address := unsafe.Pointer(unsafe.SliceData(data))
mapped, err := unix.MmapPtr(c.fd, 0, address, uintptr(len(data)), unix.PROT_READ|unix.PROT_WRITE, unix.MAP_PRIVATE|unix.MAP_FIXED)
if err != nil {
return fmt.Errorf("map sealed COW image at existing address: %w", err)
}
if mapped != address {
_ = unix.MunmapPtr(mapped, uintptr(len(data)))
return errors.New("COW mapping address changed")
}
runtime.KeepAlive(data)
return nil
}
attachBytes 拒绝 closed、未 capture、长度不等于 image、空 slice 和未按 OS page 对齐的地址。它用 unsafe.SliceData 观察当前 slice 的首地址,然后调用 MmapPtr:文件偏移从 0 开始,长度正好是 image,权限可读写,标志是 MAP_PRIVATE|MAP_FIXED。
MAP_FIXED 把 sealed memfd 前缀放回这个实例原来的 mapping base;MAP_PRIVATE 才提供写入时的私有化语义。内核在写者首次修改某个 OS page 时建立该页的私有副本,其他实例仍可读封存页;这不是按 64 KiB Wasm page 整块复制。MAP_FIXED 返回的地址必须与请求地址完全相同,代码拒绝任何“接近”的结果,并尝试解除错误映射。
runtime.KeepAlive(data) 只保护这次 unsafe 调用期间 slice 的 Go 引用存活。它不拥有内核 mapping,也不能替代 Munmap。attach 成功后,前缀由 memfd 私有映射承载,未覆盖的匿名尾部仍在同一实例范围内。
状态可以写成:
预留: [匿名前缀 | 匿名尾部]
_initialize: [实例初始写入 | 匿名尾部]
attach: [sealed memfd 私有映射 | 匿名尾部]
A 写 OS page: [A 的私有页 | 匿名尾部]
B 未写: [封存基线页 | 匿名尾部]
A、B 的 Host mapping base 可以不同;每个实例只需满足自身 attach 前后地址不变。
10. Runner 释放:先实例,后 fd
cow_linux.go · L187–L201 · 打开完整源码
func (c *linuxCOW) close() error {
if c.closed {
return nil
}
c.closed = true
if err := unix.Close(c.fd); err != nil {
return fmt.Errorf("close COW image: %w", err)
}
c.fd = -1
return nil
}
func maxInt() int {
return int(^uint(0) >> 1)
}
close 是幂等的。它先把 closed 设为真,再关闭 fd;失败时返回带上下文的错误,成功后把 fd 设为 -1。关闭 fd 释放 Runner 对内核文件对象的引用,但不能代替实例映射的 Free/release;已有映射的生命周期仍由实例持有。
maxInt 根据当前架构计算 Go int 上限,供最大内存和 slice 长度检查使用。它不是 Wasm 的上限;Wasm maximum 来自 module definition,OS page size 只用于底层映射条件。
11. 一条完整状态链
以 image 为 2 个 Wasm 页、实例最大值为 4 个 Wasm 页为例:
- Runner 创建空 memfd,
ready=false。 - 参考实例预留 4 页,完成
_initialize/init,捕获前 2 页并加 seals,ready=true。 - 新实例预留自己的 4 页,完成
_initialize,attach 前 2 页;后 2 页仍是匿名尾部。 Memory.Grow(1)只扩大 Guest 可见长度,mapping base 不变,新尾部为零。- Guest 写入前缀的一个 OS page,只有该实例获得私有页;memfd 和另一个实例不变。
- 实例关闭时解除自己的范围;所有 Run 结束后 Runner 关闭 fd。
这条链解释的是本仓库的 prepared COW 实现,不是通用 snapshot/restore 格式。内核层断言见 cow-kernel-tests.md,真实 Guest 行为见 cow-guest-tests.md。