Files
HomeAgent/internal/plugin/proc/shmalloc_windows.go
JianFeeeee d027c964e2 proc: Windows 共享内存 + 事件通知适配(Part 6.2 内核侧)
补齐内核侧的 Windows 创建端,与 6.1 的插件侧打开端配对。三平台
(linux/darwin/windows)现在都能构建 internal/plugin/proc。

## Windows 走命名内核对象(无 fd 继承语义)

os/exec 的 ExtraFiles 在 Windows 实现里不被支持,故:
- shmalloc_windows.go:CreateFileMappingW(INVALID_HANDLE_VALUE + 命名 →
  系统页文件支撑的匿名段,不落盘)+ MapViewOfFile
- evtfd_windows.go:CreateEventW 命名 Event 对象 + SetEvent 通知
- shmpass_windows.go:把段名/对象名经环境变量注入子进程
  (HOMEAGENT_SHM_STAGE / HOMEAGENT_SHM_EVTRING / HOMEAGENT_EVT_EVENT)

名字带 PID + 递增序号:多个 homed 实例并存时不能撞名。

Event 与 eventfd 的语义差异:Event 是二元信号,多次 SetEvent 只对应一次
唤醒,不累积。不影响正确性——消费者被唤醒后按 readSeq 追 writeSeq 批量
drain,丢的是"唤醒次数"不是"事件";事件环本身就允许溢出丢弃并让消费者
知道丢了(dropped 计数),通知面从来不是可靠投递语义。

## 传递机制抽象为 shmpass_*.go

Plugin.Start 不再直接构造 ExtraFiles 列表,改为问 Host 要:
  Env:        p.host.procEnvForShm()        // Windows 返回段名,Unix 返回 nil
  ExtraFiles: p.host.procExtraFilesForShm() // Unix 返回 fd 列表,Windows 返回 nil

平台差异被收敛到这一对函数,Plugin/coreHandler/stage 全部平台无关。

## macOS pipe 生命周期修正

原实现只返回读端 fd,写端 *os.File 无人持有 → 可能被 GC 回收 →
读端收到 EOF 而非阻塞 → 消费循环变忙转。改为 pipePair 表同时持有两端,
evtfdClose 一并关闭。

## E2E 测试跟进模板拆分

模板从单文件拆成三个(主体 + unix/windows 挂载),测试需要一并落盘,
否则编译报 attachStageShm undefined。procRuntimeTemplates 表必须与
SDK 仓 proc_runtime.go 的 procRuntimeFiles 一致。

验证:三平台 go build ./internal/plugin/... 通过(gojieba 的 cgo 依赖
导致 internal/memory 在非 linux 失败,与本次无关);
go test -race ./internal/plugin/... 全绿,含 2 项真实模板 E2E。

Ref: docs/zh/架构迁移评估.md §9.2、docs/zh/plugin-migration-plan.md Part 6
2026-09-02 19:07:14 +08:00

138 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//go:build windows
package proc
import (
"fmt"
"os"
"sync"
"sync/atomic"
"unsafe"
"golang.org/x/sys/windows"
)
// Windows 侧共享段:命名 FileMapping + 命名 Event。
//
// 与 Unix 的机制差异(不是能力差异):
// Windows 没有 fd 继承语义——os/exec 的 ExtraFiles 在 Windows 实现里不被支持。
// 等价机制是命名内核对象:父进程 CreateFileMappingW 建带名字的段,
// 子进程 OpenFileMappingW 按同名打开,拿到同一份物理页。
//
// **这是 §9.2 的正解**。C ABI 时代 Windows 是第三套独立 ABI 实现
// dynamic_dll_windows.gostage 只下发 3 字段且完全没有写回,
// sanitizer 这类改写型插件静默失效。三套 ABI 收敛为单一 RPC 后,
// Windows 与 Unix 共用同一份 stage 逻辑与同一份共享段布局,
// 平台差异只剩本文件的创建端 + 插件侧模板的打开端。
//
// 名字带 PID 与递增序号:多个 homed 实例并存时不能撞名,
// 同一实例内 StageContext 段与事件环段也必须分开。
var shmNameSeq atomic.Uint64
const (
shmNamePrefix = "Local\\HomeAgentShm"
evtRingNamePfx = "Local\\HomeAgentEvtRing"
evtEventNamePfx = "Local\\HomeAgentEvtSignal"
envStageShmName = "HOMEAGENT_SHM_STAGE"
envEvtRingName = "HOMEAGENT_SHM_EVTRING"
envEvtEventName = "HOMEAGENT_EVT_EVENT"
)
// namedShm 持有一块命名共享段。
//
// 不用 *os.File 承载Windows 的 FileMapping 句柄不是文件句柄,
// 包进 os.File 后 Close 语义不对(会尝试当文件关)。故用独立类型,
// 由 shmHandles 表按 mmap 地址反查——freeShm 只拿到 (*os.File, []byte)。
type namedShm struct {
name string
mapping windows.Handle
addr uintptr
size int
}
// shmHandles 记录已分配的段,供 freeShm 按数据指针反查句柄。
//
// 为何需要这张表allocShm 的跨平台签名返回 (*os.File, []byte)
// Windows 没有对应的 fd只能把句柄存在旁路。key 用切片首地址。
var (
shmHandles = map[uintptr]*namedShm{}
shmHandlesMu sync.Mutex
)
// allocShm 创建命名共享段并映射。
//
// 返回的 *os.File 为 nilWindows 不经 fd 传递段,插件按名字打开。
// 名字通过 procEnvForShm 注入子进程环境变量。
func allocShm(size int) (*os.File, []byte, error) {
name := fmt.Sprintf("%s_%d_%d", shmNamePrefix, os.Getpid(), shmNameSeq.Add(1))
shm, data, err := createNamedMapping(name, size)
if err != nil {
return nil, nil, err
}
shmHandlesMu.Lock()
shmHandles[uintptr(unsafe.Pointer(&data[0]))] = shm
shmHandlesMu.Unlock()
return nil, data, nil
}
// createNamedMapping 建命名段并映射为 []byte。
func createNamedMapping(name string, size int) (*namedShm, []byte, error) {
namePtr, err := windows.UTF16PtrFromString(name)
if err != nil {
return nil, nil, fmt.Errorf("proc: 共享段名字非法 %q: %w", name, err)
}
// INVALID_HANDLE_VALUE + 命名 → 由系统页文件支撑的匿名段(不落盘)
mapping, err := windows.CreateFileMapping(
windows.InvalidHandle, nil, windows.PAGE_READWRITE,
uint32(uint64(size)>>32), uint32(size), namePtr)
if err != nil {
return nil, nil, fmt.Errorf("proc: 创建命名共享段 %q: %w", name, err)
}
addr, err := windows.MapViewOfFile(mapping, windows.FILE_MAP_WRITE, 0, 0, uintptr(size))
if err != nil {
windows.CloseHandle(mapping)
return nil, nil, fmt.Errorf("proc: 映射共享段 %q: %w", name, err)
}
return &namedShm{name: name, mapping: mapping, addr: addr, size: size},
unsafe.Slice((*byte)(unsafe.Pointer(addr)), size), nil
}
// freeShm 解除映射并关闭段句柄。
func freeShm(f *os.File, data []byte) error {
if len(data) == 0 {
return nil
}
key := uintptr(unsafe.Pointer(&data[0]))
shmHandlesMu.Lock()
shm, ok := shmHandles[key]
delete(shmHandles, key)
shmHandlesMu.Unlock()
if !ok {
return nil
}
var firstErr error
if err := windows.UnmapViewOfFile(shm.addr); err != nil {
firstErr = err
}
if err := windows.CloseHandle(shm.mapping); err != nil && firstErr == nil {
firstErr = err
}
return firstErr
}
// shmNameOf 返回某块已分配段的名字(供注入子进程环境变量)。
func shmNameOf(data []byte) string {
if len(data) == 0 {
return ""
}
shmHandlesMu.Lock()
defer shmHandlesMu.Unlock()
if shm, ok := shmHandles[uintptr(unsafe.Pointer(&data[0]))]; ok {
return shm.name
}
return ""
}