feat(shm): 统一共享内存区域(§13.1) — 单 memfd 容纳 SuperBlock+StageContext+EvtRing

- unified.go: SuperBlock 布局 + SharedRef 类型
- NewHost(): 两段 memfd 合并为单一 memfd,fd 3 = 统一区域,fd 4 = eventfd
- 子进程侧: testdata 插件从 SuperBlock 解析 ctxOff/evtOff
- streaming 测试: EvtConsumer 协程在 host.Close 前 Stop+Wait 防 SIGSEGV
This commit is contained in:
JianFeeeee
2026-09-10 11:42:22 +08:00
parent 7a328679da
commit ad016e4410
7 changed files with 291 additions and 60 deletions

View File

@ -9,48 +9,37 @@ import (
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
// Host 持有**被全部子进程插件共享的一块 StageContext 段**,是共享内存数据面的
// Host 持有**被全部子进程插件共享的统一内存区域**,是共享数据面的
// 所有权中心§3.3/§3.4)。
//
// ❗ 为什么必须共享一块段(这是一个容易走错的关键点)
// 若每个插件各持一块段,则「内核 ctx → 段 → 插件改 → 回读 ctx」在多插件下退化成
// ❗ 为什么必须共享一块区域
// 若每个插件各持一块段,则「内核 ctx → 段 → 插件改 → 回读 ctx」退化成
// 副本模型——两个插件各写各的段、各自回读,最后回读者覆盖前者,
// lost update 原样复现§8.4 实测 35.8~36.8%)。
// 实验 8 的做法是 5 个 worker 进程 mmap **同一个 memfd**,本实现与之一致。
//
// 生命周期Host 由 registry 创建一次,随内核存活;每个插件 spawn 时经
// ExtraFiles 拿到同一 memfdfd 3mmap 后即看到同一份物理页。
// 统一区域§13.1):单 memfd 包含 SuperBlock + StageContext + EvtRing。
// 子进程经 fd 3 mmap 同一 memfd → 同一份物理页,区域头 SuperBlock
// 告知各 segment 的偏移与大小。
//
// 另外持有事件环段§3.6):独立于 StageContext 的事件通知通道,
// 子进程从 eventfd 感知新事件并从 mmap 读 slot。
// fd 分配fd 3 = StageContextfd 4 = 事件环fd 5 = eventfd。
// fd 分配fd 3 = 统一区域fd 4 = eventfd。
type Host struct {
memfd *os.File
data []byte
seg *Segment
shmSize int
data []byte // 统一区域完整 mmap
unified *unifiedRegion // SuperBlock 解析结果
seg *Segment // StageContext segment位于 unified ctxData
shmSize int // 统一区域总大小
// 事件环段(独立于 StageContext
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 数据
// 事件通知(独立于共享段
evtfd *os.File // Unixeventfd/pipe 读端fd 4。Windows 为 nil。
evtNotifyFd int // 通知句柄的平台无关标识
evtRing *EvtRing // 内核侧事件环句柄(位于 unified evtData
// evtSubscriber 由 internal/plugin 注入coreHandler 用它接子进程的 events.subscribe 请求。
// proc 包不依赖 internal/plugin循环依赖故用接口类型存储。
evtSubscriber EvtRingSubscriber
locks *lockRegistry
stageMu sync.Mutex
coordMu sync.Mutex
coord *stageCoordinator
// sup 是内核侧唯一的子进程台账,与共享段同生命周期。
//
// 放在 Host 而不是 registry 的理由:能拿到 Host 的地方就能拿到台账,
// 而 Host 本就是「全部子进程插件共享的那一份内核侧状态」。
sup *Supervisor
locks *lockRegistry
stageMu sync.Mutex
coordMu sync.Mutex
coord *stageCoordinator
sup *Supervisor
}
// NewHost 创建共享段(平台层 allocShm + 布局初始化)。
@ -63,22 +52,34 @@ type Host struct {
// 三者共同点:全部插件看到同一份物理页,段内一律用相对偏移而非指针
// (实验 2 已验证各进程 mmap 到不同虚拟地址时偏移解引用仍正确)。
func NewHost() (*Host, error) {
memfd, data, err := allocShm(shmDefaultSize)
// 统一区域大小SuperBlock + StageContext segment + EvtRing segment
unifiedSize := superBlockSize + shmDefaultSize + evtTotalSize
memfd, data, err := allocShm(unifiedSize)
if err != nil {
return nil, err
}
seg, err := NewSegment(data)
// 初始化 SuperBlock + 两个 segment
ur, err := initUnifiedRegion(data, shmDefaultSize, evtTotalSize)
if err != nil {
freeShm(memfd, data)
return nil, err
}
// 创建事件环段(独立于 StageContext
evtRingFd, evtData, efd, err := allocEvtRing()
// 创建 StageContext segment位于 SuperBlock 之后
seg, err := NewSegment(ur.ctxData())
if err != nil {
freeShm(memfd, data)
return nil, fmt.Errorf("事件环: %w", err)
return nil, err
}
// 初始化 EvtRing 子区域头部magic/version/cap
evtData := ur.evtData()
putU32(evtData[evtOffMagic:], evtRingMagic)
putU32(evtData[evtOffVersion:], evtRingVersion)
putU32(evtData[evtOffCap:], evtRingCap)
// 创建 EvtRing segment位于 StageContext 之后)
evtRing, err := NewEvtRing(evtData)
if err != nil {
freeShm(memfd, data)
@ -86,17 +87,23 @@ func NewHost() (*Host, error) {
}
evtRing.Init()
// eventfd 独立于共享段,仍为单独 fd
efd, err := evtfdCreate()
if err != nil {
freeShm(memfd, data)
return nil, fmt.Errorf("创建 eventfd: %w", err)
}
return &Host{
sup: NewSupervisor(),
memfd: memfd,
data: data,
unified: ur,
seg: seg,
shmSize: shmDefaultSize,
shmSize: unifiedSize,
evtfd: evtfdReadFile(efd),
evtNotifyFd: efd,
evtRing: evtRing,
evtRingFd: evtRingFd,
evtData: evtData,
locks: &lockRegistry{},
}, nil
}
@ -123,14 +130,7 @@ func (h *Host) Close() error {
if err := freeShm(h.memfd, h.data); err != nil && firstErr == nil {
firstErr = err
}
h.data, h.memfd = nil, nil
}
if h.evtData != nil {
if h.evtRingFd != nil {
h.evtRingFd.Close()
h.evtRingFd = nil
}
h.evtData = nil
h.data, h.memfd, h.unified = nil, nil, nil
}
if h.evtfd != nil {
h.evtfd.Close()
@ -321,7 +321,7 @@ func (h *Host) EvtNotifyFd() int { return h.evtNotifyFd }
func (h *Host) SetEvtSubscriber(sub EvtRingSubscriber) { h.evtSubscriber = sub }
// EvtData 返回事件环段 mmap 数据(子进程消费者用)。
func (h *Host) EvtData() []byte { return h.evtData }
func (h *Host) EvtData() []byte { return h.unified.evtData() }
// EvtfdReadFile 返回 eventfd 的 *os.File供子进程读取消费
func (h *Host) EvtfdReadFile() *os.File { return h.evtfd }

View File

@ -11,11 +11,10 @@ func (h *Host) procEnvForShm() []string { return nil }
//
// 顺序即 fd 编号cmd.ExtraFiles[0] → 子进程 fd 3
//
// fd 3 = StageContext
// fd 4 = 事件环段
// fd 5 = 事件通知eventfd / pipe 读端)
// fd 3 = 统一共享内存区域SuperBlock + StageContext + EvtRing
// fd 4 = 事件通知eventfd / pipe 读端)
//
// 插件侧模板 z_proc_shm_unix.go 的常量与此严格对应。
func (h *Host) procExtraFilesForShm() []*os.File {
return []*os.File{h.memfd, h.evtRingFd, h.evtfd}
return []*os.File{h.memfd, h.evtfd}
}

View File

@ -26,18 +26,26 @@ func TestStreaming_SlowConsumerDoesNotBlockPublish(t *testing.T) {
if err != nil {
t.Fatalf("NewHost: %v", err)
}
defer host.Close()
ring := host.EvtRing()
var consumed atomic.Int64
consumer := NewEvtConsumer(host.EvtData(), host.EvtfdReadFile(), 0,
func(evt *pubsdk.Event) error {
time.Sleep(20 * time.Microsecond) // 刻意的慢订阅者
time.Sleep(20 * time.Microsecond)
consumed.Add(1)
return nil
})
go consumer.Run()
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
consumer.Run()
}()
// defer LIFOStop → 关 evtfd 打断 Read → 等协程退出 → Close host
defer host.Close()
defer wg.Wait()
defer host.EvtfdReadFile().Close()
defer consumer.Stop()
const tokens = 5000
@ -71,7 +79,9 @@ func TestStreaming_PublishLatencyFlatAcrossSubscribers(t *testing.T) {
if err != nil {
t.Fatalf("NewHost: %v", err)
}
// defer LIFO先停消费者再 Close host
defer host.Close()
defer host.EvtfdReadFile().Close()
ring := host.EvtRing()
payload := []byte(`{"type":"content_delta","payload":{"text":"t"}}`)
@ -93,13 +103,14 @@ func TestStreaming_PublishLatencyFlatAcrossSubscribers(t *testing.T) {
cc.Run()
}(c)
}
// defer LIFOStop → 等协程退出
defer wg.Wait()
defer func() {
for _, c := range active {
c.Stop()
}
}()
// 让消费者先就位
time.Sleep(10 * time.Millisecond)
start := time.Now()

View File

@ -181,7 +181,10 @@ func main() {
send(response{ID: req.ID, Error: fmt.Sprintf("mmap: %v", err)})
continue
}
shm = m
// 统一区域:前 64B 是 SuperBlockStageContext 段在其后
ctxOff := binary.LittleEndian.Uint32(m[20:])
ctxSize := binary.LittleEndian.Uint32(m[24:])
shm = m[ctxOff : ctxOff+ctxSize]
}
send(response{ID: req.ID, Result: map[string]interface{}{
"protocol": 1, "sdk_version": "test",

View File

@ -133,7 +133,10 @@ func main() {
send(response{ID: req.ID, Error: fmt.Sprintf("mmap: %v", err)})
continue
}
shm = m
// 统一区域:前 64B 是 SuperBlockStageContext 段在其后
ctxOff := binary.LittleEndian.Uint32(m[20:])
ctxSize := binary.LittleEndian.Uint32(m[24:])
shm = m[ctxOff : ctxOff+ctxSize]
}
send(response{ID: req.ID, Result: map[string]interface{}{
"protocol": 1, "sdk_version": "test", "plugin_name": "readonly", "pid": os.Getpid(),

View File

@ -223,14 +223,17 @@ func main() {
json.Unmarshal(req.Params, &hp)
shmSize = hp.ShmSize
if shmSize > 0 {
// fd 3 = 内核传入的共享段 memfd
// fd 3 = 内核传入的统一共享内存区域
m, err := syscall.Mmap(3, 0, shmSize,
syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED)
if err != nil {
send(response{ID: req.ID, Error: fmt.Sprintf("mmap 共享段失败: %v", err)})
continue
}
shm = m
// 统一区域:前 64B 是 SuperBlockStageContext 段在其后
ctxOff := binary.LittleEndian.Uint32(m[20:])
ctxSize := binary.LittleEndian.Uint32(m[24:])
shm = m[ctxOff : ctxOff+ctxSize]
}
send(response{ID: req.ID, Result: map[string]interface{}{
"protocol": 1, "sdk_version": "test", "plugin_name": "stage", "pid": os.Getpid(),

View File

@ -0,0 +1,212 @@
package proc
// 统一共享内存区域布局§13.1)。
//
// 之前的两块独立 memfdStageContext 256KB + EvtRing ~320KBfd 3/4
// 合并为单一 memfdfd 3eventfd 独占 fd 4。
//
// 区域内按固定偏移定位各 segment所有偏移相对段起始跨进程 mmap 到
// 不同虚拟地址仍能正确解引用§3.3 实验 2 已验证)。
//
// ┌──────────────────────────────────────────────────────┐
// │ SuperBlock 64B │
// │ magic / version / generation / capacity / reserved │
// │ ctxOff / ctxSize / evtOff / evtSize │
// ├──────────────────────────────────────────────────────┤
// │ StageContext segment (内部布局不变) │
// ├──────────────────────────────────────────────────────┤
// │ EvtRing segment (内部布局不变) │
// ├──────────────────────────────────────────────────────┤
// │ ToolCall lane [§13.3] │
// ├──────────────────────────────────────────────────────┤
// │ InputCh lane [§13.5] │
// ├──────────────────────────────────────────────────────┤
// │ OutputCh lane [§13.6] │
// ├──────────────────────────────────────────────────────┤
// │ Dynamic Arena │
// └──────────────────────────────────────────────────────┘
import (
"fmt"
"sync/atomic"
"unsafe"
)
const (
// unifiedMagic 标识统一共享内存区域。
unifiedMagic uint32 = 0x554D5352 // "UMSR" — Unified Memory Shared Region
unifiedVersion uint32 = 1
superBlockSize = 64 // SuperBlock 占前 64 字节
)
// SuperBlock 布局64 字节,所有偏移相对区域起始。
//
// [0,4) magic
// [4,8) version
// [8,16) generationatomic uint64resize 时 bump
// [16,20) capacity区域总字节数
// [20,24) ctxOffStageContext segment 偏移)
// [24,28) ctxSizeStageContext segment 字节数)
// [28,32) evtOffEvtRing segment 偏移)
// [32,36) evtSizeEvtRing segment 字节数)
// [36,48) reserved未来 lane 偏移/大小)
// [48,56) reserved
// [56,64) reserved对齐到 8 字节)
const (
sbOffMagic = 0
sbOffVersion = 4
sbOffGeneration = 8
sbOffCapacity = 16
sbOffCtxOff = 20
sbOffCtxSize = 24
sbOffEvtOff = 28
sbOffEvtSize = 32
)
// SharedRef 是跨进程共享内存描述符,替代内联 JSON 数据。
//
// 所有数据交换(工具调用参数/结果、Cleaner、输入/输出通道消息)
// 都通过 SharedRef 传递RPC 只传 16 字节描述符,实际数据在共享内存中。
//
// Generation 防 ABA扩容 remap 后旧描述符自动失效。
type SharedRef struct {
Offset uint32 // 相对区域起始的偏移
Length uint32 // 数据字节数
Generation uint32 // 扩容后 bump
Flags uint32 // 保留,位 0 = 二进制,位 1 = JSON
}
const sharedRefSize = 16
func (r SharedRef) IsZero() bool {
return r.Offset == 0 && r.Length == 0
}
// Slice 从共享内存中按 SharedRef 切片。data 必须是完整的 mmap 区域。
func (r SharedRef) Slice(data []byte) []byte {
if r.IsZero() || int(r.Offset)+int(r.Length) > len(data) {
return nil
}
return data[r.Offset : r.Offset+r.Length]
}
// unifiedRegion 统一共享内存区域的内核侧视图。
type unifiedRegion struct {
data []byte
ctxOff uint32
ctxSize uint32
evtOff uint32
evtSize uint32
}
// initUnifiedRegion 在 mmap 区域上初始化 SuperBlock + 两个 segment。
func initUnifiedRegion(data []byte, ctxTotal, evtTotal int) (*unifiedRegion, error) {
cap := uint32(len(data))
total := superBlockSize + ctxTotal + evtTotal
if int(cap) < total {
return nil, fmt.Errorf("unified: 区域过小(%d 字节,至少需要 %d", cap, total)
}
ctxOff := uint32(superBlockSize)
evtOff := ctxOff + uint32(ctxTotal)
putU32(data[sbOffMagic:], unifiedMagic)
putU32(data[sbOffVersion:], unifiedVersion)
putU64(data[sbOffGeneration:], 0)
putU32(data[sbOffCapacity:], cap)
putU32(data[sbOffCtxOff:], ctxOff)
putU32(data[sbOffCtxSize:], uint32(ctxTotal))
putU32(data[sbOffEvtOff:], evtOff)
putU32(data[sbOffEvtSize:], uint32(evtTotal))
return &unifiedRegion{
data: data,
ctxOff: ctxOff,
ctxSize: uint32(ctxTotal),
evtOff: evtOff,
evtSize: uint32(evtTotal),
}, nil
}
// attachUnifiedRegion 从已有 mmap 区域解析 SuperBlock插件侧调用
func attachUnifiedRegion(data []byte) (*unifiedRegion, error) {
if len(data) < superBlockSize {
return nil, fmt.Errorf("unified: 区域过小(%d 字节)", len(data))
}
magic := getU32(data[sbOffMagic:])
if magic != unifiedMagic {
return nil, fmt.Errorf("unified: 魔数不匹配0x%x期望 0x%x", magic, unifiedMagic)
}
ver := getU32(data[sbOffVersion:])
if ver != unifiedVersion {
return nil, fmt.Errorf("unified: 版本不匹配(%d期望 %d", ver, unifiedVersion)
}
ctxOff := getU32(data[sbOffCtxOff:])
ctxSize := getU32(data[sbOffCtxSize:])
evtOff := getU32(data[sbOffEvtOff:])
evtSize := getU32(data[sbOffEvtSize:])
if int(evtOff)+int(evtSize) > len(data) {
return nil, fmt.Errorf("unified: EvtRing 越界off=%d size=%d total=%d", evtOff, evtSize, len(data))
}
return &unifiedRegion{
data: data,
ctxOff: ctxOff,
ctxSize: ctxSize,
evtOff: evtOff,
evtSize: evtSize,
}, nil
}
func (r *unifiedRegion) generation() uint64 {
return getU64(r.data[sbOffGeneration:])
}
func (r *unifiedRegion) bumpGeneration() uint64 {
for {
old := getU64(r.data[sbOffGeneration:])
new := old + 1
p := (*uint64)(unsafe.Pointer(&r.data[sbOffGeneration]))
if atomic.CompareAndSwapUint64(p, old, new) {
return new
}
}
}
func (r *unifiedRegion) ctxData() []byte {
return r.data[r.ctxOff : r.ctxOff+r.ctxSize]
}
func (r *unifiedRegion) evtData() []byte {
return r.data[r.evtOff : r.evtOff+r.evtSize]
}
func putU32(b []byte, v uint32) {
b[0] = byte(v)
b[1] = byte(v >> 8)
b[2] = byte(v >> 16)
b[3] = byte(v >> 24)
}
func getU32(b []byte) uint32 {
return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
}
func putU64(b []byte, v uint64) {
b[0] = byte(v)
b[1] = byte(v >> 8)
b[2] = byte(v >> 16)
b[3] = byte(v >> 24)
b[4] = byte(v >> 32)
b[5] = byte(v >> 40)
b[6] = byte(v >> 48)
b[7] = byte(v >> 56)
}
func getU64(b []byte) uint64 {
return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 |
uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56
}