本章回答:prepared.go 只调用接口时,哪些状态和所有权必须保持不变?答案是两层对象:cowRuntime 属于 Runner,拥有一份已经捕获的不可变 image;cowMemory 属于某个 Guest 实例,提供 wazero 所需的 allocator 和线性内存操作。接口不承诺每个平台都能实现 COW,也不把 Host 状态伪装成 Guest memory。
1. 依赖只描述 wazero 交接点
cow.go · L1–L7 · 打开完整源码
package spine
import (
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental"
)
文件导入 api 和 experimental。api.MemoryDefinition 用于从编译模块取得 memory 的形状,api.Memory 表示实例化后的 Guest 线性内存;实验接口分别描述自定义 allocator 和线性内存的生命周期。cow.go 不选择 Linux 系统调用,也不保存任何 Guest 字节。
2. cowMemory:一个实例的 allocator、地址和可见长度
cow.go · L8–L14 · 打开完整源码
// cowRuntime owns the immutable image; each instance owns its mapping.
// Implementations are intentionally Linux-specific; other platforms return an
// unsupported-constructor error rather than silently falling back to copying.
type cowMemory interface {
experimental.MemoryAllocator
experimental.LinearMemory
}
cowMemory 同时嵌入 experimental.MemoryAllocator 与 experimental.LinearMemory。前者让 wazero 在 InstantiateModule 时使用这段内存,后者要求实现提供分配、重分配和释放线性内存的行为。Linux 实现因此可以让 allocator 预留一个固定范围,再通过 Reallocate 改变 Guest 当前可见长度,而不搬动 mapping base。
这个对象属于一个 Guest。它不拥有 Runner 级 image;它只拥有该实例的匿名范围或被 attach 到该范围的私有映射。Free 是 wazero 的无返回值回调,具体实现可另提供带错误的 release,但调用者不能把 Free 的静默返回理解为系统调用一定成功。
3. cowRuntime:Runner 级 image 的状态机
cow.go · L15–L22 · 打开完整源码
type cowRuntime interface {
allocator(api.MemoryDefinition) (cowMemory, error)
capture(api.Memory) error
attach(api.Memory) error
ready() bool
close() error
}
cowRuntime 公开五个阶段动作:
allocator(def):从模块 memory 定义创建一个实例级cowMemory。输入决定最大 Wasm 页数;它不读取基线。capture(memory):接收完成初始化的 Guest memory,提交 Runner 的基线。attach(memory):在一个新实例已有的 mapping base 上接入基线。它不改变 Host 的 globals、tables 或工具资源。ready():报告 image 是否已提交且 runtime 未关闭。参考 Guest 创建时通常为假,capture 后为真。close():关闭 Runner 级资源,例如 Linux memfd。它不替代每个实例的 mapping 释放。
接口把“捕获一次、随后多次 attach”表达出来,但没有允许再次 capture 的方法。Linux 实现因此能拒绝重复捕获,维持基线不可变;平台实现也必须把不支持的情况作为错误交给构造器。
4. 四个状态交接点
Runner: newCOWRuntime → ready=false → capture → ready=true → close
实例 A: allocator → 当前长度/匿名范围 → attach → Guest 写入 → Free
实例 B: allocator → 当前长度/匿名范围 → attach → Guest 写入 → Free
两条线的交接发生在 prepared.go:它先让参考 Guest 完成初始化,再调用 capture;之后每个 newGuest 先调用 allocator,完成 _initialize 后调用 attach。因此 capture 看到的是 Guest memory,attach 接收的也是 Guest memory;Host 的编译 module、工具回调、WASI 资源和本轮执行状态不在接口的输入里。
“共享 image”只表示 A、B 可以从同一个不可变来源读到相同前缀。它不表示两个实例共享一个可写 Go slice,也不表示两个实例必须有相同的 Host 虚拟地址。每个实例的 mapping base 由自己的 allocator 决定,attach 只要求本实例的起点稳定。
5. 增长与释放如何落到接口
接口没有另定义 Grow。Guest 的 Memory.Grow 最终通过 cowMemory 的 Reallocate 改变当前可见字节数;实现必须拒绝超过模块声明最大值的请求,并保持本实例的起点。这里的长度通常是按 Wasm 页计算后传入的字节数,不能拿 Linux 系统页大小替代 Wasm 页协议。
释放分成两类:关闭一个 module 会让 wazero 释放对应 cowMemory,从而解除该实例映射;关闭 Runner 才关闭 cowRuntime 持有的 memfd。先关 fd 不等于所有实例都已释放,因为已有映射可以继续存在。调用者仍需等待 Run 退出,再关闭 Runner。
6. 错误边界
接口层把错误集中在资源和形状检查上:没有导出 memory、没有 declared maximum、无法预留范围、基线为空、重复 capture、image 已关闭、实例长度与 image 不一致、地址不对齐或 attach 失败,都必须中止当前阶段。接口没有提供“尽力复制”的备用语义;非 Linux 实现会明确报告不支持,而不是让 full-copy 冒充 COW。
具体的 sealed memfd、系统页对齐、MAP_PRIVATE|MAP_FIXED 和 munmap 语义见 cow-linux.md;对应的内核观察在 cow-kernel-tests.md。