mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 09:28:14 +00:00
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:
@ -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 继承 fd,Windows 命名
|
||||
// 内核对象),故拆成带 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 {
|
||||
|
||||
@ -4,30 +4,78 @@ package proc
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// evtfdCreate 用 pipe 模拟 Linux eventfd 的通知语义(macOS 无 eventfd)。
|
||||
// macOS 侧事件通知:用 pipe 模拟 eventfd(macOS 无 eventfd_create)。
|
||||
//
|
||||
// 限制:不具 eventfd 的计数合并(多次写会触发多次读),
|
||||
// 但事件环本身允许溢出丢弃,consumer 在 drainEvents 里按 readSeq 批量读取,
|
||||
// 故多次唤醒只多几次无效循环(readSeq == writeSeq 时立即返回),不造成正确性问题。
|
||||
// 与 eventfd 的语义差异:pipe 不具计数合并,多次写会触发多次读。
|
||||
// 这不影响正确性——消费者在 drainEvents 里按 readSeq 追 writeSeq 批量读,
|
||||
// 多次唤醒只多几次空循环(readSeq == writeSeq 时立即返回)。
|
||||
//
|
||||
// 走 Go netpoller(os.File.Read 阻塞时只 park goroutine,实验 1 已验证)。
|
||||
// 走 Go netpoller:os.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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {}
|
||||
|
||||
@ -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) {}
|
||||
|
||||
103
internal/plugin/proc/evtfd_windows.go
Normal file
103
internal/plugin/proc/evtfd_windows.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -31,10 +31,11 @@ type Host struct {
|
||||
shmSize int
|
||||
|
||||
// 事件环段(独立于 StageContext)
|
||||
evtfd *os.File // eventfd fd(fd 5 的句柄,子进程读取消费)
|
||||
evtRing *EvtRing // 内核侧事件环句柄
|
||||
evtRingFd *os.File // 事件环段 memfd(fd 4,子进程 mmap 读事件)
|
||||
evtData []byte // 事件环段 mmap 数据
|
||||
evtfd *os.File // Unix:eventfd/pipe 读端(fd 5)。Windows 为 nil,用 evtNotifyFd 。
|
||||
evtNotifyFd int // 通知句柄的平台无关标识(Unix 是真 fd,Windows 是伪 fd)
|
||||
evtRing *EvtRing // 内核侧事件环句柄
|
||||
evtRingFd *os.File // Unix:事件环段 memfd(fd 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_*.go):Linux 用 memfd,macOS 用
|
||||
// 立即 unlink 的临时文件(无 memfd_create),其余平台明确报错。
|
||||
// 两者语义一致:无文件名残留,fd 可经 ExtraFiles 传给子进程,
|
||||
// 子进程 mmap 同一 inode——「全部插件共享一块段」的前提得以成立。
|
||||
// 实验 2 已验证父子 mmap 到不同虚拟地址时相对偏移仍正确解引用。
|
||||
// 段的**传递机制**按平台分开(shmalloc_*.go),但**布局**完全一致:
|
||||
// - Linux:memfd,经 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.File(Unix;eventfd/pipe)。
|
||||
// Windows 返回 nil——命名 Event 不是文件句柄,用 EvtNotifyFd 代替。
|
||||
func (h *Host) Evtfd() *os.File { return h.evtfd }
|
||||
|
||||
// EvtNotifyFd 返回通知句柄的平台无关标识,供 EventRing 写通知。
|
||||
//
|
||||
// Unix 是真 fd;Windows 是映射到命名 Event 句柄的伪 fd。
|
||||
// EvtfdNotify 接受这个值并按平台分派。
|
||||
func (h *Host) EvtNotifyFd() int { return h.evtNotifyFd }
|
||||
|
||||
// SetEvtSubscriber 注入事件环订阅接口(由 Registry 在创建 Host 后设置)。
|
||||
func (h *Host) SetEvtSubscriber(sub EvtRingSubscriber) { h.evtSubscriber = sub }
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 是插件 → 内核的建链应答:上报自身信息。
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
//go:build !linux && !darwin
|
||||
//go:build !linux && !darwin && !windows
|
||||
|
||||
package proc
|
||||
|
||||
|
||||
137
internal/plugin/proc/shmalloc_windows.go
Normal file
137
internal/plugin/proc/shmalloc_windows.go
Normal 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.go),stage 只下发 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 为 nil:Windows 不经 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 ""
|
||||
}
|
||||
21
internal/plugin/proc/shmpass_unix.go
Normal file
21
internal/plugin/proc/shmpass_unix.go
Normal 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}
|
||||
}
|
||||
26
internal/plugin/proc/shmpass_windows.go
Normal file
26
internal/plugin/proc/shmpass_windows.go
Normal 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 }
|
||||
Reference in New Issue
Block a user