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
This commit is contained in:
JianFeeeee
2026-09-02 19:07:14 +08:00
parent 53a148cb54
commit d027c964e2
14 changed files with 445 additions and 68 deletions

View File

@ -0,0 +1,103 @@
//go:build windows
package proc
import (
"fmt"
"os"
"sync"
"sync/atomic"
"golang.org/x/sys/windows"
)
// Windows 侧事件通知:命名 Event 对象。
//
// 与 eventfd 的语义差异:Event 是二元信号(Set/Reset),不是计数器。
// 多次 SetEvent 只对应一次唤醒,不会累积。
//
// 这不影响正确性:消费者被唤醒后按 readSeq 追 writeSeq 批量 drain,
// 一次唤醒能处理累积的全部事件(漏掉的是"唤醒次数",不是"事件")。
// 事件环本身允许溢出丢弃并让消费者知道丢了(dropped 计数),
// 通知面从来不是可靠投递语义。
//
// 代价:WaitForSingleObject 阻塞 OS 线程而非仅 goroutine,不如 eventfd
// 的 netpoller 路径省线程。每插件一个消费 goroutine,17 插件即 17 线程
// (实验 5 实测 17 子进程共 84 线程,仍在可接受范围)。
var evtEventSeq atomic.Uint64
// evtEventHandles 记录 fd 伪值 → Event 句柄的映射。
//
// 为何需要:跨平台签名用 int 表示通知句柄(Unix 是真 fd)。
// Windows 的 windows.Handle 是 uintptr,直接转 int 在 32 位上会截断,
// 故用递增伪 fd 做 key,句柄存表里。
var (
evtEvents = map[int]windows.Handle{}
evtEventsMu sync.Mutex
evtEventFd atomic.Int64
)
// evtfdCreate 创建命名 Event 对象,返回伪 fd。
//
// 手动重置(manualReset=false → 自动重置):被一个等待者唤醒后自动 Reset,
// 语义最接近 eventfd 的"取出后清零"。
func evtfdCreate() (int, error) {
name := fmt.Sprintf("%s_%d_%d", evtEventNamePfx, os.Getpid(), evtEventSeq.Add(1))
namePtr, err := windows.UTF16PtrFromString(name)
if err != nil {
return -1, fmt.Errorf("proc: 事件对象名字非法 %q: %w", name, err)
}
h, err := windows.CreateEvent(nil, 0 /*autoReset*/, 0 /*initiallyNonSignaled*/, namePtr)
if err != nil {
return -1, fmt.Errorf("proc: 创建事件对象 %q: %w", name, err)
}
fd := int(evtEventFd.Add(1))
evtEventsMu.Lock()
evtEvents[fd] = h
evtEventNames[fd] = name
evtEventsMu.Unlock()
return fd, nil
}
// evtEventNames 记录伪 fd → 对象名(供注入子进程环境变量)。
var evtEventNames = map[int]string{}
// EvtfdNotify 唤醒等待者(post-and-forget)。
//
// SetEvent 不阻塞,满足 §3.6 约束 B(Bus.Publish 路径上绝不等待)。
func EvtfdNotify(efd int) {
evtEventsMu.Lock()
h, ok := evtEvents[efd]
evtEventsMu.Unlock()
if !ok {
return
}
// 忽略错误:句柄有效时 SetEvent 不会失败
windows.SetEvent(h)
}
// evtfdReadFile 在 Windows 上返回 nil。
//
// 内核侧不消费事件环(只写入),消费在插件进程里由模板的
// windowsEvtWaiter 完成。这个函数只为跨平台签名存在。
func evtfdReadFile(efd int) *os.File { return nil }
// evtEventNameOf 返回某个伪 fd 对应的 Event 对象名(供注入子进程环境变量)。
func evtEventNameOf(efd int) string {
evtEventsMu.Lock()
defer evtEventsMu.Unlock()
return evtEventNames[efd]
}
// evtfdClose 关闭 Event 句柄。
func evtfdClose(efd int) {
evtEventsMu.Lock()
h, ok := evtEvents[efd]
delete(evtEvents, efd)
delete(evtEventNames, efd)
evtEventsMu.Unlock()
if ok {
windows.CloseHandle(h)
}
}