Files
HomeAgent/internal/plugin/proc/evtfd_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

104 lines
3.2 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"
"golang.org/x/sys/windows"
)
// Windows 侧事件通知:命名 Event 对象。
//
// 与 eventfd 的语义差异Event 是二元信号Set/Reset不是计数器。
// 多次 SetEvent 只对应一次唤醒,不会累积。
//
// 这不影响正确性:消费者被唤醒后按 readSeq 追 writeSeq 批量 drain
// 一次唤醒能处理累积的全部事件(漏掉的是"唤醒次数",不是"事件")。
// 事件环本身允许溢出丢弃并让消费者知道丢了dropped 计数),
// 通知面从来不是可靠投递语义。
//
// 代价WaitForSingleObject 阻塞 OS 线程而非仅 goroutine不如 eventfd
// 的 netpoller 路径省线程。每插件一个消费 goroutine17 插件即 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 约束 BBus.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)
}
}