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

@ -68,6 +68,20 @@ func (p *e2ePlugin) Start(s *sdk.PluginSDK) error {
func (p *e2ePlugin) Stop() error { return nil }
`
// procRuntimeTemplates 列出 plugindev 会生成到插件目录的运行时文件。
//
// 必须与 SDK 仓 tools/plugindev/proc_runtime.go 的 procRuntimeFiles 一致:
// 共享段与事件通知的传递机制按平台不同Unix 继承 fdWindows 命名
// 内核对象),故拆成带 build tag 的文件;只写主模板会编译失败。
var procRuntimeTemplates = []struct {
tmpl string
out string
}{
{"proc_main.go.tmpl", "z_proc_gen.go"},
{"proc_shm_unix.go.tmpl", "z_proc_shm_unix.go"},
{"proc_shm_windows.go.tmpl", "z_proc_shm_windows.go"},
}
// buildPluginWithRealTemplate 用 plugindev 的真实模板编译一个插件二进制。
func buildPluginWithRealTemplate(t *testing.T, businessCode string) string {
t.Helper()
@ -75,17 +89,19 @@ func buildPluginWithRealTemplate(t *testing.T, businessCode string) string {
t.Skip("环境无 go 工具链,跳过端到端测试")
}
tmpl := filepath.Join("..", "..", "..",
"third_party", "homeagent-sdk", "tools", "plugindev",
"templates", "proc_main.go.tmpl")
runtime, err := os.ReadFile(tmpl)
if err != nil {
t.Skipf("plugindev 模板不可读SDK 仓可能未就位): %v", err)
}
tmplDir := filepath.Join("..", "..", "..",
"third_party", "homeagent-sdk", "tools", "plugindev", "templates")
dir := t.TempDir()
mustWriteFile(t, filepath.Join(dir, "plugin.go"), businessCode)
mustWriteFile(t, filepath.Join(dir, "z_proc_gen.go"), string(runtime))
for _, rt := range procRuntimeTemplates {
data, err := os.ReadFile(filepath.Join(tmplDir, rt.tmpl))
if err != nil {
t.Skipf("plugindev 模板 %s 不可读SDK 仓可能未就位): %v", rt.tmpl, err)
}
mustWriteFile(t, filepath.Join(dir, rt.out), string(data))
}
sdkPath, err := filepath.Abs(filepath.Join("..", "..", "..", "third_party", "homeagent-sdk"))
if err != nil {

View File

@ -4,30 +4,78 @@ package proc
import (
"os"
"sync"
"syscall"
)
// evtfdCreate 用 pipe 模拟 Linux eventfd 的通知语义macOS 无 eventfd
// macOS 侧事件通知:用 pipe 模拟 eventfdmacOS 无 eventfd_create)。
//
// 限制:不具 eventfd 的计数合并多次写会触发多次读
// 但事件环本身允许溢出丢弃consumer 在 drainEvents 里按 readSeq 批量读
// 多次唤醒只多几次无效循环readSeq == writeSeq 时立即返回),不造成正确性问题
// eventfd 的语义差异pipe 不具计数合并多次写会触发多次读
// 这不影响正确性——消费者在 drainEvents 里按 readSeq 追 writeSeq 批量读,
// 多次唤醒只多几次循环readSeq == writeSeq 时立即返回)。
//
// 走 Go netpolleros.File.Read 阻塞时只 park goroutine实验 1 已验证)。
// 走 Go netpolleros.File.Read 阻塞时只 park goroutine不占 OS 线程
// (实验 1 已验证 200 个等待者仅增 1 个 OS 线程)。
//
// ❗ 必须同时持有读端与写端:写端若被 GC 回收,读端会收到 EOF 而非阻塞,
// 消费循环变成忙转。故用 pipePair 表存住两端。
type pipePair struct {
r *os.File
w *os.File
}
var (
evtPipes = map[int]*pipePair{}
evtPipesMu sync.Mutex
)
// evtfdCreate 建 pipe返回读端 fd。
func evtfdCreate() (int, error) {
r, w, err := os.Pipe()
if err != nil {
return -1, err
}
return int(r.Fd()), nil
fd := int(r.Fd())
evtPipesMu.Lock()
evtPipes[fd] = &pipePair{r: r, w: w}
evtPipesMu.Unlock()
return fd, nil
}
// evtfdNotify 写 1 字节通知子进程有新事件post-and-forget
// EvtfdNotify 写 1 字节通知子进程有新事件post-and-forget
//
// 直接写裸 fd 而非 pipePair.w本函数在 Bus.Publish 路径上被高频调用,
// 查表加锁不值得。写端 fd 由 pipePair 持有引用故不会被 GC 回收。
func EvtfdNotify(efd int) {
evtPipesMu.Lock()
p, ok := evtPipes[efd]
evtPipesMu.Unlock()
if !ok {
return
}
var buf [1]byte
syscall.Write(efd, buf[:])
// 忽略错误:管道满说明消费者落后,事件环本身允许溢出丢弃
syscall.Write(int(p.w.Fd()), buf[:])
}
// evtfdReadFile 把事件通知读端包装成 *os.File 供 netpoller 消费。
// evtfdReadFile 返回通知读端供 netpoller 消费
func evtfdReadFile(efd int) *os.File {
return os.NewFile(uintptr(efd), "evtring-notify")
evtPipesMu.Lock()
defer evtPipesMu.Unlock()
if p, ok := evtPipes[efd]; ok {
return p.r
}
return nil
}
// evtfdClose 关闭 pipe 两端。
func evtfdClose(efd int) {
evtPipesMu.Lock()
p, ok := evtPipes[efd]
delete(evtPipes, efd)
evtPipesMu.Unlock()
if ok {
p.r.Close()
p.w.Close()
}
}

View File

@ -33,3 +33,6 @@ func EvtfdNotify(efd int) {
func evtfdReadFile(efd int) *os.File {
return os.NewFile(uintptr(efd), "evtring-notify")
}
// evtfdClose 关闭通知句柄。Unix 侧由 *os.File.Close 负责,此处为跨平台签名占位。
func evtfdClose(efd int) {}

View File

@ -1,4 +1,4 @@
//go:build !linux && !darwin
//go:build !linux && !darwin && !windows
package proc
@ -16,3 +16,6 @@ type errPlatformNotSupported string
func (e errPlatformNotSupported) Error() string {
return "当前平台尚未支持事件环通知(" + string(e) + "§9.2"
}
// evtfdClose 关闭通知句柄。Unix 侧由 *os.File.Close 负责,此处为跨平台签名占位。
func evtfdClose(efd int) {}

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 路径省线程。每插件一个消费 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)
}
}

View File

@ -68,8 +68,8 @@ func evtTypeMask(types ...pubsdk.EventType) uint32 {
const (
evtRingMagic uint32 = 0x48455654 // "HEVT"
evtRingVersion uint32 = 1
evtRingCap uint32 = 8192 // 2^13满足流式场景突发实验 4
evtRingSlotLen uint32 = 32 // seq(8)+type(4)+off(4)+len(4)+pad(12)
evtRingCap uint32 = 8192 // 2^13满足流式场景突发实验 4
evtRingSlotLen uint32 = 32 // seq(8)+type(4)+off(4)+len(4)+pad(12)
evtOffMagic uint32 = 0
evtOffVersion uint32 = 4

View File

@ -31,10 +31,11 @@ type Host struct {
shmSize int
// 事件环段(独立于 StageContext
evtfd *os.File // eventfd fdfd 5 的句柄,子进程读取消费)
evtRing *EvtRing // 内核侧事件环句柄
evtRingFd *os.File // 事件环段 memfdfd 4子进程 mmap 读事件)
evtData []byte // 事件环段 mmap 数据
evtfd *os.File // Unixeventfd/pipe 读端fd 5。Windows 为 nil用 evtNotifyFd 。
evtNotifyFd int // 通知句柄的平台无关标识Unix 是真 fdWindows 是伪 fd
evtRing *EvtRing // 内核侧事件环句柄
evtRingFd *os.File // Unix事件环段 memfdfd 4。Windows 为 nil命名段
evtData []byte // 事件环段 mmap 数据
// evtSubscriber 由 internal/plugin 注入coreHandler 用它接子进程的 events.subscribe 请求。
// proc 包不依赖 internal/plugin循环依赖故用接口类型存储。
@ -48,11 +49,13 @@ type Host struct {
// NewHost 创建共享段(平台层 allocShm + 布局初始化)。
//
// 段的分配按平台分开shmalloc_*.goLinux 用 memfdmacOS 用
// 立即 unlink 的临时文件(无 memfd_create其余平台明确报错。
// 两者语义一致无文件名残留fd 可经 ExtraFiles 传给子进程,
// 子进程 mmap 同一 inode——「全部插件共享一块段」的前提得以成立。
// 实验 2 已验证父子 mmap 到不同虚拟地址时相对偏移仍正确解引用。
// 段的**传递机制**按平台分开shmalloc_*.go,但**布局**完全一致:
// - Linuxmemfd经 ExtraFiles 传继承 fd
// - macOS立即 unlink 的临时文件(无 memfd_create同样走 fd 继承
// - Windows命名 FileMapping无 fd 继承语义),插件按名字打开
//
// 三者共同点:全部插件看到同一份物理页,段内一律用相对偏移而非指针
// (实验 2 已验证各进程 mmap 到不同虚拟地址时偏移解引用仍正确)。
func NewHost() (*Host, error) {
memfd, data, err := allocShm(shmDefaultSize)
if err != nil {
@ -78,15 +81,16 @@ func NewHost() (*Host, error) {
evtRing.Init()
return &Host{
memfd: memfd,
data: data,
seg: seg,
shmSize: shmDefaultSize,
evtfd: evtfdReadFile(efd),
evtRing: evtRing,
evtRingFd: evtRingFd,
evtData: evtData,
locks: &lockRegistry{},
memfd: memfd,
data: data,
seg: seg,
shmSize: shmDefaultSize,
evtfd: evtfdReadFile(efd),
evtNotifyFd: efd,
evtRing: evtRing,
evtRingFd: evtRingFd,
evtData: evtData,
locks: &lockRegistry{},
}, nil
}
@ -117,6 +121,7 @@ func (h *Host) Close() error {
h.evtfd.Close()
h.evtfd = nil
}
evtfdClose(h.evtNotifyFd)
return firstErr
}
@ -240,9 +245,16 @@ func (h *Host) ShmSize() int { return h.shmSize }
// EvtRing 返回内核侧事件环句柄。
func (h *Host) EvtRing() *EvtRing { return h.evtRing }
// Evtfd 返回 eventfd 的 *os.File供 EventRing 写通知)。
// Evtfd 返回通知读端的 *os.FileUnixeventfd/pipe)。
// Windows 返回 nil——命名 Event 不是文件句柄,用 EvtNotifyFd 代替。
func (h *Host) Evtfd() *os.File { return h.evtfd }
// EvtNotifyFd 返回通知句柄的平台无关标识,供 EventRing 写通知。
//
// Unix 是真 fdWindows 是映射到命名 Event 句柄的伪 fd。
// EvtfdNotify 接受这个值并按平台分派。
func (h *Host) EvtNotifyFd() int { return h.evtNotifyFd }
// SetEvtSubscriber 注入事件环订阅接口(由 Registry 在创建 Host 后设置)。
func (h *Host) SetEvtSubscriber(sub EvtRingSubscriber) { h.evtSubscriber = sub }

View File

@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"log"
"os"
"sync"
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
@ -69,10 +68,10 @@ func (p *Plugin) Start(core CoreSDK) error {
}
p.handler = &coreHandler{
sdk: core,
name: p.name,
host: p.host,
locks: p.host.locks,
sdk: core,
name: p.name,
host: p.host,
locks: p.host.locks,
evtRing: p.host.evtSubscriber,
}
// 反向调用闭包:注册回调时捕获,运行期经 RPC 打到插件进程。
@ -82,12 +81,15 @@ func (p *Plugin) Start(core CoreSDK) error {
proc, err := Spawn(p.name, p.bin, Options{
Dir: p.dir,
Env: p.env,
// 子进程 fd 布局:3=StageContext 段,4=事件环5=eventfd
ExtraFiles: []*os.File{p.host.memfd, p.host.evtRingFd, p.host.evtfd},
ShmSize: p.host.shmSize,
Handler: p.handler.Handle,
OnExit: p.handleExit,
// 共享段的传递机制按平台不同shmpass_*.go
// Unix 经 ExtraFiles 传继承 fd 3=StageContext, 4=事件环, 5=通知);
// Windows 无 fd 继承语义,改用命名内核对象,名字经环境变量传入。
Env: append(p.env, p.host.procEnvForShm()...),
ExtraFiles: p.host.procExtraFilesForShm(),
ShmSize: p.host.shmSize,
EvtRingSize: evtTotalSize,
Handler: p.handler.Handle,
OnExit: p.handleExit,
})
if err != nil {
return err

View File

@ -62,6 +62,8 @@ type Process struct {
// shmSize 是握手时告知插件的共享段大小0 表示本插件不用共享段)。
shmSize int
// evtRingSize 是事件环段大小0 表示不支持事件环)。
evtRingSize int
}
// RequestHandler 处理插件 → 内核的调用。
@ -79,6 +81,8 @@ type Options struct {
ExtraFiles []*os.File
// ShmSize 是共享段大小,握手时告知插件(与 ExtraFiles[0] 的 memfd 对应)。
ShmSize int
// EvtRingSize 是事件环段大小0 表示不支持事件环)。
EvtRingSize int
// Handler 处理插件反向调用。
Handler RequestHandler
// OnExit 进程退出回调。
@ -127,18 +131,19 @@ func Spawn(name, bin string, opts Options) (*Process, error) {
}
p := &Process{
name: name,
bin: bin,
dir: opts.Dir,
cmd: cmd,
stdin: bufio.NewWriter(stdinPipe),
stdout: stdoutPipe,
pending: make(map[uint64]chan *Response),
handler: opts.Handler,
exited: make(chan struct{}),
ready: make(chan struct{}),
onExit: opts.OnExit,
shmSize: opts.ShmSize,
name: name,
bin: bin,
dir: opts.Dir,
cmd: cmd,
stdin: bufio.NewWriter(stdinPipe),
stdout: stdoutPipe,
pending: make(map[uint64]chan *Response),
handler: opts.Handler,
exited: make(chan struct{}),
ready: make(chan struct{}),
onExit: opts.OnExit,
shmSize: opts.ShmSize,
evtRingSize: opts.EvtRingSize,
}
if err := cmd.Start(); err != nil {
@ -190,6 +195,7 @@ func (p *Process) handshake(timeout time.Duration) error {
PluginName: p.name,
ShmVersion: shmVersion,
ShmSize: p.shmSize,
EvtRingSize: p.evtRingSize,
})
if err != nil {
return fmt.Errorf("proc: %s 握手失败: %w", p.name, err)

View File

@ -153,10 +153,10 @@ type HandshakeParams struct {
Protocol int `json:"protocol"` // 内核支持的协议版本
CoreVersion string `json:"core_version"` // 内核版本(诊断用)
PluginName string `json:"plugin_name"` // 内核分配的插件名
// ShmVersion 让插件确认共享段布局一致;不匹配时插件应拒绝启动而非错读。
ShmVersion uint32 `json:"shm_version"`
// ShmSize 是内核分配的共享段大小,插件据此 mmap段本身经 fd 3 传入)
ShmSize int `json:"shm_size"`
ShmVersion uint32 `json:"shm_version"`
ShmSize int `json:"shm_size"`
// EvtRingSize 是事件环段大小0 表示不支持事件环)。插件据此 mmap fd 4
EvtRingSize int `json:"evt_ring_size,omitempty"`
}
// HandshakeResult 是插件 → 内核的建链应答:上报自身信息。

View File

@ -1,4 +1,4 @@
//go:build !linux && !darwin
//go:build !linux && !darwin && !windows
package proc

View File

@ -0,0 +1,137 @@
//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 ""
}

View File

@ -0,0 +1,21 @@
//go:build linux || darwin || freebsd
package proc
import "os"
// procEnvForShm 在 Unix 返回 nil段经继承 fd 传递,无需环境变量。
func (h *Host) procEnvForShm() []string { return nil }
// procExtraFilesForShm 返回经 ExtraFiles 传给子进程的 fd 列表。
//
// 顺序即 fd 编号cmd.ExtraFiles[0] → 子进程 fd 3
//
// fd 3 = StageContext 段
// fd 4 = 事件环段
// fd 5 = 事件通知eventfd / pipe 读端)
//
// 插件侧模板 z_proc_shm_unix.go 的常量与此严格对应。
func (h *Host) procExtraFilesForShm() []*os.File {
return []*os.File{h.memfd, h.evtRingFd, h.evtfd}
}

View File

@ -0,0 +1,26 @@
//go:build windows
package proc
import (
"fmt"
"os"
)
// procEnvForShm 返回子进程挂载共享段所需的环境变量Windows
//
// Windows 没有 fd 继承语义os/exec 的 ExtraFiles 在 Windows 不被支持),
// 故段与事件对象的**名字**经环境变量传给子进程,插件侧模板的
// z_proc_shm_windows.go 按同名 OpenFileMappingW / OpenEventW 打开。
//
// 名字带 PID 与递增序号:多个 homed 实例并存时不能撞名。
func (h *Host) procEnvForShm() []string {
return []string{
fmt.Sprintf("HOMEAGENT_SHM_STAGE=%s", shmNameOf(h.data)),
fmt.Sprintf("HOMEAGENT_SHM_EVTRING=%s", shmNameOf(h.evtData)),
fmt.Sprintf("HOMEAGENT_EVT_EVENT=%s", evtEventNameOf(h.evtNotifyFd)),
}
}
// procExtraFilesForShm 在 Windows 返回 nil段不经 fd 传递。
func (h *Host) procExtraFilesForShm() []*os.File { return nil }