PPysolate SpineREAD THE CODE · UNDERSTAND THE SYSTEM
课程与章节
CODE READING / 20

初始化与内存恢复的分岔

prepared.go

本章目录
配套饭点图解 · 2 节(按需展开)

本章回答一个问题:NewPrepared 和 NewPreparedCOW 捕获的到底是什么,下一次 Run 又怎样把它交给一个新 Guest?答案从两个构造器开始:full-copy 把初始化后的 Guest 线性内存复制到 Runner.image;Linux COW 把同一段内存写入由 Runner.cow 持有的 sealed memfd。两条路径都先创建一个参考 Guest,再关闭它;后续每一轮仍创建自己的 Guest。

“基线”只指 Guest 线性内存中、_initialize 与 CPython init 完成后的状态。它不包括 Host 的工具注册表、Go globals、tables、WASI 资源、Future、执行栈或其他 Go 对象,也不是任意执行时刻的完整恢复记录。Runner 负责拥有编译后的 module、工具和基线;参考 Guest 只在构造阶段存活;每轮 api.Module 拥有自己的 Guest 状态。

1. 先区分三条路径和所有者

prepared.go 共享一个 newGuest 生命周期,但初始化后的下一步不同:

Runner 状态 _initialize 之后 基线所有者 本轮交付
image == nil、cow == nil 只有新实例自己的 Wasm 初始内存 无 调 CPython init
image != nil、cow == nil 新实例已完成 Wasm 初始化 Runner.image 的 Go 字节 按 Wasm 页增长并写入 image
cow.ready() == true 新实例已完成 Wasm 初始化,且 allocator 预留了地址范围 Runner.cow 的 sealed memfd 在本实例原 mapping base 上 attach 私有映射

image 是 full-copy 的 Go 拥有字节;COW 模式不把完整镜像留在 image。mapping base 是某个实例的线性内存起始虚拟地址:allocator 先给这个实例预留它,COW attach 再在同一实例的起点替换前缀。它不是所有实例共享的 Host 地址。

2. 包边界和准备机制的限制

prepared.go · L1–L15 · 打开完整源码

package spine

import (
	"context"
	"errors"
	"fmt"

	"github.com/tetratelabs/wazero"
	"github.com/tetratelabs/wazero/api"
	"github.com/tetratelabs/wazero/experimental"
)

// NewPrepared captures only our Guest's clean init, before any user code or tool.
// Each Run still has private allocated memory: this is full-copy, NOT page COW.
// This is not a generic Wasm checkpoint (Host resources/globals/tables aren't captured).

这一段只建立包、错误处理、wazero 类型和实验性内存 allocator 的依赖。文件顶部的注释已经限定了机制边界:NewPrepared 是 full-copy,不是页级 COW;捕获对象是本项目 Guest 的干净初始化内存,不是通用 Wasm checkpoint。api.Module 表示每轮实例化后的 Guest,experimental.WithMemoryAllocator 则允许 COW 实现控制这段实例内存的地址。

这里的“干净”依赖调用顺序,而不是某个单独的标记:构造器会建立参考 Guest,让 _initialize 和 CPython init 完成,然后才读取或捕获内存。用户代码和工具调用没有进入这条准备路径。

3. full-copy 初始化:Runner 先拥有一份字节基线

prepared.go · L16–L35 · 打开完整源码

func NewPrepared(ctx context.Context, wasm []byte, manifest Manifest) (*Runner, error) {
	r, err := New(ctx, wasm, manifest)
	if err != nil {
		return nil, err
	}
	m, err := r.newGuest(ctx, &boundedText{}, &boundedText{})
	if err != nil {
		r.Close(context.Background())
		return nil, err
	}
	defer m.Close(context.Background())
	memory := m.Memory()
	data, ok := memory.Read(0, memory.Size())
	if !ok || len(data) == 0 {
		r.Close(context.Background())
		return nil, errors.New("cannot capture initialized Guest memory")
	}
	r.image = append([]byte(nil), data...)
	return r, nil
}

NewPrepared 先调用普通 New。成功后,Runner 已拥有 runtime、编译后的 r.code 和 Host 配置,但 r.image 仍为空。它调用 r.newGuest 创建参考 Guest;因为此时没有 COW image,也没有 full-copy image,newGuest 会走普通初始化路径。

参考 Guest 成功后,构造器把它的 Memory() 读成 data。空内存或读取失败表示不能形成基线,构造器会关闭已经创建的 Runner 并返回错误。data 只是对参考内存的读取结果;参考模块随后要关闭,所以 append([]byte(nil), data...) 把内容复制到由 Runner 长期拥有的 image。构造器返回时,参考 Guest 已由 defer 关闭,Runner 状态变为:编译代码仍在、image 非空、没有暴露给调用者的活参考实例。

这个复制动作只复制 Guest 线性内存。它不会把参考 Guest 的 Host 对象或运行时句柄搬进下一轮;这些对象仍由各轮自己的执行路径建立和关闭。

4. COW 初始化:先建立容器,再提交基线

prepared.go · L37–L67 · 打开完整源码

// NewPreparedCOW captures the initialized Guest in a sealed Linux memfd and
// attaches each fresh instance with MAP_PRIVATE at the original address. It
// shares only linear-memory pages; globals, tables, runtime objects and Host
// resources remain per-instance and are not part of this image.
func NewPreparedCOW(ctx context.Context, wasm []byte, manifest Manifest) (*Runner, error) {
	r, err := New(ctx, wasm, manifest)
	if err != nil {
		return nil, err
	}
	cow, err := newCOWRuntime()
	if err != nil {
		_ = r.Close(context.Background())
		return nil, err
	}
	r.cow = cow
	m, err := r.newGuest(ctx, &boundedText{}, &boundedText{})
	if err != nil {
		_ = r.Close(context.Background())
		return nil, err
	}
	if err := cow.capture(m.Memory()); err != nil {
		_ = m.Close(context.Background())
		_ = r.Close(context.Background())
		return nil, err
	}
	if err := m.Close(context.Background()); err != nil {
		_ = r.Close(context.Background())
		return nil, err
	}
	return r, nil
}

NewPreparedCOW 同样先调用 New,但随后创建平台相关的 cowRuntime,并把它挂到 r.cow。此时 memfd 可能已经存在,cow.ready() 却仍为假:它只有在 capture 成功并提交 image 大小后才表示可用。

构造器在 r.cow 已设置的状态下创建参考 Guest。allocator 会为它预留实例内存,但由于 COW 还没有基线,newGuest 仍要执行普通初始化。初始化成功后,cow.capture(m.Memory()) 把这份 Guest 内存交给平台实现;捕获失败时先关闭参考 Guest,再关闭 Runner。捕获成功也不代表构造器已经完成:参考模块还要成功关闭,之后才返回 Runner。

因此两条构造器路径的最终状态不同:

NewPrepared:    Runner.image != nil, Runner.cow == nil
NewPreparedCOW: Runner.image == nil, Runner.cow != nil, Runner.cow.ready() == true

错误按阶段收束:普通 New 失败时没有 Runner 可清理;COW runtime 创建失败时关闭已创建的 Runner;参考 Guest 创建、capture 或关闭失败时同时处理参考模块和 Runner。这里没有把 COW 失败静默降级为 full-copy,调用者能看到真正的平台错误。

5. newGuest:先决定实例内存,再决定恢复方式

prepared.go · L69–L80 · 打开完整源码

// All constructors use this lifecycle. Only CPython init vs memory restore differs.
func (r *Runner) newGuest(ctx context.Context, stdout, stderr *boundedText) (api.Module, error) {
	instantiateCtx := ctx
	var mapped cowMemory
	if r.cow != nil {
		var err error
		mapped, err = r.cow.allocator(r.code.ExportedMemories()["memory"])
		if err != nil {
			return nil, err
		}
		instantiateCtx = experimental.WithMemoryAllocator(instantiateCtx, mapped)
	}

所有构造器最终都进入 newGuest。函数先把调用者的 ctx 用于实例化和初始化,再为当前实例准备一个局部的 mapped cowMemory。如果 Runner 有 COW runtime,它从编译模块的 memory 定义取得最大 Wasm 页数,调用 allocator,并把返回的 allocator 放入实例化上下文。

这一步的对象关系很具体:wazero 需要的是一个能提供线性内存的 allocator;Linux 实现背后先预留一个最大范围,返回的 cowMemory 同时携带那段范围的地址和增长/释放方法。memory 定义、Wasm 页数和 Host 虚拟地址是三个不同层次;不能把导出的 Wasm 内存定义当成已经捕获的字节,也不能把 mapping base 当成 Guest 可见长度。

6. 实例化失败与模块所有权交接

prepared.go · L81–L93 · 打开完整源码

	m, err := r.runtime.InstantiateModule(instantiateCtx, r.code, wazero.NewModuleConfig().WithName("").WithStartFunctions().WithStdout(stdout).WithStderr(stderr))
	if err != nil {
		if mapped != nil {
			mapped.Free() // Instantiation may fail before wazero takes ownership.
		}
		return nil, err
	}
	failed := true
	defer func() {
		if failed {
			m.Close(context.Background())
		}
	}()

实例化关闭 wazero 的自动 start functions,绑定本轮 stdout/stderr,然后显式执行 _initialize。如果实例化在 wazero 接管 mapped 之前失败,代码主动调用 mapped.Free();成功实例化后,failed 保护 defer 负责关闭模块,直到函数把所有权交给返回值。

这两个清理点解决的是不同状态:allocator 已创建但 module 尚未接管时释放映射;module 已创建但初始化/恢复失败时关闭 module。成功返回后,failed=false,调用者负责关闭返回的 api.Module,而实例级映射随模块生命周期回收。

7. _initialize 之后才恢复基线

prepared.go · L94–L109 · 打开完整源码

	if _, err = m.ExportedFunction("_initialize").Call(ctx); err != nil {
		return nil, fmt.Errorf("initialize Guest: %w%s", err, stderr.String())
	}
	if r.cow != nil && r.cow.ready() {
		if err := r.cow.attach(m.Memory()); err != nil {
			return nil, err
		}
	} else if r.image == nil {
		status, err := m.ExportedFunction("init").Call(ctx)
		if err != nil {
			return nil, fmt.Errorf("init CPython: %w%s", err, stderr.String())
		}
		if status[0] != 0 {
			return nil, fmt.Errorf("CPython initialization failed: %s", stderr.String())
		}
	} else {

代码先调用导出的 _initialize。Wasm data segments、运行库初始化和导出初始化逻辑可能写线性内存,所以基线恢复必须排在它之后。否则初始化写入可能覆盖刚 attach 或刚写入的基线,改变“准备状态”的定义。

初始化成功后的分支是:

  • COW 已 ready:调用 r.cow.attach(m.Memory()),让当前实例原有 mapping base 的前缀由 sealed memfd 的 MAP_PRIVATE 映射承载。
  • 没有 COW 且没有 image:调用 CPython init,检查调用错误和非零状态。这是 fresh 路径。
  • 没有 COW 但有 image:进入下一节的 full-copy 恢复。

初始化错误、COW attach 错误和 CPython 错误都走同一个失败保护:当前模块关闭,错误返回给调用者;它们不会改变 Runner 已拥有的基线。错误文本会附带本轮 stderr,便于定位 Guest 初始化失败,但这不等于把 stderr 作为基线的一部分。

8. full-copy 恢复:按 Wasm 页调整可见长度

prepared.go · L110–L127 · 打开完整源码

		// Instantiate/_initialize write data segments; restore the image AFTER them.
		memory := m.Memory()
		size := memory.Size()
		if int(size) > len(r.image) {
			return nil, errors.New("prepared memory shape changed")
		}
		if int(size) < len(r.image) {
			if _, ok := memory.Grow(uint32(len(r.image)-int(size)) / 65536); !ok {
				return nil, errors.New("cannot grow memory to prepared image")
			}
		}
		if !memory.Write(0, r.image) {
			return nil, errors.New("cannot copy prepared memory")
		}
	}
	failed = false
	return m, nil
}

full-copy 路径把 r.image 写入当前实例的 Memory()。先检查当前实例大小不能大于基线长度;如果它更小,代码按 65536 字节计算需要增长的 Wasm 页数,然后调用 Memory.Grow。这里的 65536 是 Wasm 页(64 KiB),不是 Linux 系统页;它只服务于 Wasm 的内存大小协议。Linux COW 的地址对齐和 mmap 还要使用系统页大小,见 cow-linux.md。

增长成功后,Memory.Write(0, r.image) 把整份基线写到本实例。失败就返回错误,Runner 的 image 不被修改。只有 _initialize、形状检查、增长和写入全部成功,函数才把 failed 置为假并返回 module。

这解释了 Run 之后的状态:本轮 Guest 可以改变自己的线性内存;Runner.image 仍是原始基线;本轮 module 关闭后不能继续使用其 Memory.Read 返回的借用 view。下一轮重新实例化,再按同一分支恢复。

9. 从初始化到释放的状态链

构造器进入
  → Runner 拥有编译代码,基线尚未提交
  → 参考 Guest 实例化并完成 _initialize / init
  → full-copy: 复制到 Runner.image
    COW: 写入 memfd、加 seals、提交 imageSize
  → 关闭参考 Guest,Runner 可服务 Run

每次 newGuest
  → (COW)预留本实例最大范围
  → InstantiateModule + _initialize
  → fresh 调 init;full-copy 写 image;COW attach 基线前缀
  → 调用者运行用户代码
  → 关闭 module,释放本实例内存

Runner.Close
  → 等待所有 Run 结束后关闭编译 runtime 和 COW Runner 资源

下一章 prepared-tests.md 用真实 Guest 检查这条状态链;COW 的接口、Linux 映射和平台边界分别见 cow-interface.md、cow-linux.md 和 cow-platform.md。