本章回答:Linux COW 的关键状态能否在不运行 Python 的情况下被直接观察?测试直接操作 linuxCOW 和 linuxCOWMemory,按“创建资源 → 捕获并封存 → 两个实例 attach → 私有写入 → 固定范围增长 → release”推进;第二个测试覆盖预约失败、重复释放和地址复用。
这些断言区分三种对象:sealed memfd 是 Runner 级基线;first、second 是各自拥有 mapping 的实例;slice 只是当前 Guest memory 的借用视图。smaps 只提供当前 Linux 进程对 VMA 的观测,不推出固定的物理内存节省或所有工作负载的性能结论。
1. Linux 测试工具和第一条入口
cow_linux_test.go · L1–L17 · 打开完整源码
//go:build linux
package spine
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
"testing"
"unsafe"
"golang.org/x/sys/unix"
)
func TestLinuxCOWMappingSealsAddressAndIsolation(t *testing.T) {
构建标签限制测试在 Linux。测试导入 bufio、文件/字符串/数字解析、testing、unsafe 和 unix:前者读取 /proc/self/smaps,unsafe 只取 slice 起点,unix 读取 seal、执行 Pread。第一条测试名已经列出它要同时检查的证据:mapping、seals、地址稳定和写隔离。
2. 建立空 runtime,捕获两页基线并检查 seals
cow_linux_test.go · L18–L44 · 打开完整源码
resource, err := newCOWRuntime()
if err != nil {
t.Fatal(err)
}
cow := resource.(*linuxCOW)
defer cow.close()
seed := testCOWMemory(t, 4*wasmPageSize)
baseline := seed.Reallocate(2 * wasmPageSize)
if baseline == nil {
t.Fatal("seed allocation failed")
}
for i := range baseline {
baseline[i] = byte((i / cowPageSize) + 17)
}
if err := cow.captureBytes(baseline); err != nil {
t.Fatal(err)
}
seals, err := unix.FcntlInt(uintptr(cow.fd), unix.F_GET_SEALS, 0)
if err != nil {
t.Fatal(err)
}
wantSeals := unix.F_SEAL_SHRINK | unix.F_SEAL_GROW | unix.F_SEAL_WRITE | unix.F_SEAL_SEAL
if seals&wantSeals != wantSeals {
t.Fatalf("seals=%#x want all %#x", seals, wantSeals)
}
seed.Free()
测试创建 newCOWRuntime 并确认具体类型为 *linuxCOW,用 defer 关闭 Runner 级 fd。随后预留最大 4 个 Wasm 页,只把前 2 页通过 Reallocate 暴露为当前 baseline;填充模式时按 OS page 递增,从而第一个系统页全为 17,第二个全为 18。
captureBytes 把这两页写入 memfd 并加 seals。测试通过 F_GET_SEALS 从内核回读 F_SEAL_SHRINK、F_SEAL_GROW、F_SEAL_WRITE、F_SEAL_SEAL,检查的是 fd 的实际状态。最后释放只用于生成基线的 seed mapping;后面的实例从 sealed fd attach。
状态转移:
runtime:空 fd,imageSize=0
seed: 匿名 4 页,当前可见 2 页,内容=17/18
capture:fd 写入并加四个 seal,imageSize=2 Wasm 页
seed: 释放;sealed fd 仍属于 runtime
3. 两个实例 attach 到各自的 mapping base
cow_linux_test.go · L45–L69 · 打开完整源码
first := testCOWMemory(t, 4*wasmPageSize)
second := testCOWMemory(t, 4*wasmPageSize)
defer first.Free()
defer second.Free()
firstBytes := first.Reallocate(2 * wasmPageSize)
secondBytes := second.Reallocate(2 * wasmPageSize)
firstAddr := sliceAddress(firstBytes)
if err := cow.attachBytes(firstBytes); err != nil {
t.Fatal(err)
}
if got := sliceAddress(firstBytes); got != firstAddr {
t.Fatalf("mapping address changed: before=%#x after=%#x", firstAddr, got)
}
if err := cow.attachBytes(secondBytes); err != nil {
t.Fatal(err)
}
if firstBytes[0] != 17 || secondBytes[0] != 17 || secondBytes[cowPageSize] != 18 {
t.Fatalf("attached mapping did not expose sealed baseline")
}
sharedClean, sharedDirty := smapsForAddress(firstAddr)
if smapsKB(sharedClean)+smapsKB(sharedDirty) == 0 {
t.Fatalf("smaps showed no shared clean/dirty pages before private writes")
}
t.Logf("smaps shared baseline before writes: Shared_Clean=%s Shared_Dirty=%s", sharedClean, sharedDirty)
测试为 first 和 second 各预留最大 4 页,并注册 cleanup。两者先把当前长度设为 2 个 Wasm 页;测试保存 first 的 slice 地址,然后 attach。attach 前后地址必须完全相同,说明 MAP_FIXED 替换的是原范围,而不是分配了一个新起点。
两个实例 attach 后都应读到封存模式:first 的第一个字节为 17,second 的第一个 OS page 为 17、第二个为 18。测试再从 first 的地址读取 smaps,要求 Shared_Clean 或 Shared_Dirty 至少一个非零。这只是“尚未私有写时内核观察到共享页”的证据,不是性能测量。
first 与 second 不要求拥有相同的 Host 地址;每个实例只要求自己的 attach 前后地址稳定。Guest memory 的相同内容来自同一个 fd,mapping base 仍是实例级状态。
4. 私有写入、封存基线和匿名尾部增长
cow_linux_test.go · L70–L94 · 打开完整源码
firstBytes[0] = 99
firstBytes[cowPageSize] = 100
if secondBytes[0] != 17 || secondBytes[cowPageSize] != 18 {
t.Fatalf("private write leaked across mappings: second=%d,%d", secondBytes[0], secondBytes[cowPageSize])
}
sealed := make([]byte, 2*wasmPageSize)
n, err := unix.Pread(cow.fd, sealed, 0)
if err != nil || n != len(sealed) {
t.Fatalf("read sealed baseline: n=%d err=%v", n, err)
}
if sealed[0] != 17 || sealed[cowPageSize] != 18 {
t.Fatalf("private writes changed sealed baseline")
}
grown := first.Reallocate(4 * wasmPageSize)
if sliceAddress(grown) != firstAddr || grown[2*wasmPageSize] != 0 {
t.Fatalf("fixed-range growth moved or dirtied the anonymous tail")
}
if err := first.release(); err != nil {
t.Fatalf("unmap COW prefix and anonymous tail: %v", err)
}
if first.Reallocate(wasmPageSize) != nil {
t.Fatal("released mapping remains usable")
}
}
first 修改两个不同 OS page 的首字节为 99 和 100,second 仍应读到 17 和 18,证明写入没有穿透到另一个 MAP_PRIVATE 映射。测试随后用 Pread 从 fd 读回两页,确认 sealed baseline 也没有改变。
接着把 first 的当前长度扩到 4 个 Wasm 页。slice 起点必须不变,原 image 之后的第一个字节必须为零;尾部来自预留的匿名范围,而不是 memfd。最后显式 release,再要求 Reallocate 返回 nil,证明释放后的实例不再可用。
这里的写隔离粒度是 OS page:写两个字节会使它们所在的系统页私有化,不表示整个 Wasm 线性内存按 64 KiB 一次复制。测试只覆盖这个具体访问。
5. helper 的资源兜底和分配失败测试
cow_linux_test.go · L96–L105 · 打开完整源码
func testCOWMemory(t *testing.T, maximum uint64) *linuxCOWMemory {
t.Helper()
m, err := newLinuxCOWMemory(maximum)
if err != nil {
t.Fatal(err)
}
t.Cleanup(m.Free)
return m
}
testCOWMemory 把构造错误转成测试失败,并注册 t.Cleanup(m.Free)。cleanup 保护中途 Fatal 的路径;它不替代前一节对 release 行为的显式断言。第二条测试随后专门检查分配失败和 Free。
cow_linux_test.go · L106–L125 · 打开完整源码
func TestLinuxCOWAllocationFailureAndFree(t *testing.T) {
if _, err := newLinuxCOWMemory(0); err == nil {
t.Fatal("empty reservation must fail")
}
// An impossible address-space request exercises the real mmap error path.
if m, err := newLinuxCOWMemory(uint64(maxInt()) & ^uint64(wasmPageSize-1)); err == nil {
m.Free()
t.Fatal("impossible reservation unexpectedly succeeded")
}
m := testCOWMemory(t, 2*wasmPageSize)
data := m.Reallocate(2 * wasmPageSize)
address := sliceAddress(data)
if err := m.release(); err != nil {
t.Fatalf("munmap failed: %v", err)
}
m.Free()
if m.Reallocate(wasmPageSize) != nil {
t.Fatal("freed memory remains available")
}
clean, dirty := smapsForAddress(address)
零 maximum 必须失败。接近 maxInt 且按 Wasm page 对齐的请求用真实 mmap 错误路径测试不可行地址空间预约,而不是注入一个假的错误。正常建立 2 页 mapping 后,测试保存地址,先 release 再 Free,要求二次清理安全,且之后 Reallocate 返回 nil。
若 smaps 仍能看到同一数字地址,测试读取 /proc/self/maps 记录当前覆盖它的 VMA;它不把地址数字再次出现当作原对象仍然存在。成功 munmap 后地址被其他分配复用是允许状态。
6. 地址读取和 /proc 解析
cow_linux_test.go · L126–L137 · 打开完整源码
if clean != "" || dirty != "" {
maps, _ := os.ReadFile("/proc/self/maps")
for _, line := range strings.Split(string(maps), "\n") {
var start, end uintptr
if _, err := fmt.Sscanf(line, "%x-%x", &start, &end); err == nil && address >= start && address < end {
t.Logf("freed address %#x currently in %s", address, line)
}
}
t.Log("address was reused after successful munmap; this is permitted")
}
}
这段分支只在释放地址仍有 smaps 信息时运行。测试逐行解析 /proc/self/maps 的十六进制区间,记录是否有当前映射覆盖旧地址,并明确把复用写成日志信息。真正的资源断言是 release 结果与后续 Reallocate,不是地址永远从进程地址空间消失。
cow_linux_test.go · L138–L161 · 打开完整源码
func sliceAddress(data []byte) uintptr {
return uintptr(unsafe.Pointer(unsafe.SliceData(data)))
}
func smapsForAddress(address uintptr) (sharedClean, sharedDirty string) {
file, err := os.Open("/proc/self/smaps")
if err != nil {
return "", ""
}
defer file.Close()
var inRange bool
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if fields := strings.Fields(line); len(fields) > 0 && strings.Contains(fields[0], "-") {
var start, end uintptr
if _, err := fmt.Sscanf(fields[0], "%x-%x", &start, &end); err == nil {
inRange = address >= start && address < end
}
continue
}
if !inRange {
continue
}
sliceAddress 用 unsafe.SliceData 取当前 slice 首地址,只用于比较,不长期保存这个地址。smapsForAddress 打开 /proc/self/smaps;若观测入口不可用,返回空字段,调用方只失去 smaps 证据,seal、读值和隔离断言仍独立存在。
扫描器识别 VMA 头的 start-end 区间,设置 inRange,跳过头行及目标范围外的属性。它把一个地址落在哪个 VMA 与 Guest memory 的字节内容分开处理。
7. 共享页字段和状态表
cow_linux_test.go · L162–L182 · 打开完整源码
if strings.HasPrefix(line, "Shared_Clean:") {
sharedClean = strings.TrimSpace(strings.TrimPrefix(line, "Shared_Clean:"))
}
if strings.HasPrefix(line, "Shared_Dirty:") {
sharedDirty = strings.TrimSpace(strings.TrimPrefix(line, "Shared_Dirty:"))
}
if sharedClean != "" && sharedDirty != "" {
return sharedClean, sharedDirty
}
}
return sharedClean, sharedDirty
}
func smapsKB(value string) int {
fields := strings.Fields(value)
if len(fields) == 0 {
return 0
}
kb, _ := strconv.Atoi(fields[0])
return kb
}
扫描器在目标 VMA 中读取 Shared_Clean 和 Shared_Dirty 文本;两个字段齐全就返回,否则返回已经读到的部分。smapsKB 只解析数值的 KB 部分,因为测试只比较是否为零,不把它当成通用内存统计。
| 阶段 | memfd | first | second |
|---|---|---|---|
| capture 后 | 2 页、四个 seal | seed 释放 | 尚未建立 |
| attach 后、写入前 | 17/18 不变 | 17/18 | 17/18 |
| first 写两个 OS page | 17/18 不变 | 99/100 | 17/18 |
| first 增长到 4 Wasm 页 | 仍 2 页 | 起点不变,尾部为零 | 不受影响 |
| first release | fd 仍可供其他实例使用 | 不可再 Reallocate | cleanup 负责释放 second |
| runtime close | fd 关闭 | 实例应已结束 | 实例应已结束 |
这组测试把初始化、基线、私有映射、增长和释放分别落到可观察状态;真实 Guest 对 Memory.Grow、Python 写入和 Runner 关闭的检查见 cow-guest-tests.md。