refactor(shm): 内核独占共享槽池,插件经 RPC 申请/归还(§13.2/§13.3)

推翻前两版跨进程分配器设计,根因是"共享内存里放了只被单一进程
更新的可变游标":

- v1 在 SuperBlock 放 arenaUsed 游标,内核 CAS bump。但插件模板里
  arenaUsed 是**进程本地变量**,两进程各自 bump 必写同一段内存;
  arenaReset 还会重置共享游标覆盖对方数据。
- v2 把位图 CAS 下沉到插件模板,正确但把分配器实现泄漏进插件运行时,
  且插件必须与内核保持位图布局同步。

新设计(用户明确的架构约束):内核全权管理共享内存,插件通过
syscall 风格 RPC 申请/归还,内核返回偏移与大小。分配器只存在于内核
进程内,一把 sync.Mutex 即可。共享内存是内部实现,不对插件开发者
暴露——SDK 公开 API 仍是普通字符串/Map。

主要改动:
- arena.go: 定长槽 + 位图,Alloc/Put/Read/Free/ReclaimOwner,内核独占;
  槽头记录 owner,Free 校验归属;payload 超槽容量退回内联 RPC
- protocol.go: 新增 arena.alloc / arena.free(CapCore)
- capability.go: 登记两个新 method(checkAllMethodsClassified 要求)
- corehandler.go: 实现 arena.alloc/free;Cleaner 改为内核预分配
  请求槽 + 响应槽(插件完全不分配)
- plugin.go: Start 领 ownerID;handleExit 调 ReclaimOwner 回收残留槽
- unified.go: 移除坏的 bump 分配器;arena 基址 8 字节对齐
- 删除 toollane.go: ring 状态机是死代码(从未接线),且把 RPC 已有的
  请求 ID 关联/错误传递/ctx 取消重新实现了一遍。控制面保留 RPC,
  只把 payload 搬进共享槽。
- 测试: TestArena_*(归属/回收/并发唯一/耗尽/超限/非法引用/布局)+
  TestPlugin_ArenaAllocFreeAcrossProcess(真进程申请→写入→随业务
  RPC 回传→归还→内核读回一致且池归零)
This commit is contained in:
JianFeeeee
2026-09-10 17:56:50 +08:00
parent cf3c99233b
commit ca584bda3f
13 changed files with 1157 additions and 654 deletions

View File

@ -5,6 +5,7 @@ import (
"log"
"os"
"sync"
"sync/atomic"
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
@ -17,9 +18,9 @@ import (
// 副本模型——两个插件各写各的段、各自回读,最后回读者覆盖前者,
// lost update 原样复现§8.4 实测 35.8~36.8%)。
//
// 统一区域§13.1):单 memfd 包含 SuperBlock + StageContext + EvtRing
// 子进程经 fd 3 mmap 同一 memfd → 同一份物理页,区域头 SuperBlock
// 告知各 segment 的偏移与大小。
// 统一区域§13.1):单 memfd 包含 SuperBlock + StageContext + EvtRing
// + Exchange Arena。子进程经 fd 3 mmap 同一 memfd → 同一份物理页,
// 区域头 SuperBlock 告知各 segment 的偏移与大小。
//
// fd 分配fd 3 = 统一区域fd 4 = eventfd。
type Host struct {
@ -29,6 +30,13 @@ type Host struct {
seg *Segment // StageContext segment位于 unified ctxData
shmSize int // 统一区域总大小
// arena 是跨进程共享槽池§13.2 重设计)。分配/释放走共享位图 CAS
// 任意进程/goroutine 并发调用都安全,不需要任何锁。
arena *arenaRegion
// nextOwner 给每个插件进程分配一个不透明 owner ID供 ReclaimOwner 使用。
nextOwner atomic.Uint32
// 事件通知(独立于共享段)
evtfd *os.File // Unixeventfd/pipe 读端fd 4。Windows 为 nil。
evtNotifyFd int // 通知句柄的平台无关标识
@ -36,7 +44,6 @@ type Host struct {
evtSubscriber EvtRingSubscriber
locks *lockRegistry
arenaMu sync.Mutex // 动态 arena 跨进程调用的生命周期锁
stageMu sync.Mutex
coordMu sync.Mutex
coord *stageCoordinator
@ -53,8 +60,11 @@ type Host struct {
// 三者共同点:全部插件看到同一份物理页,段内一律用相对偏移而非指针
// (实验 2 已验证各进程 mmap 到不同虚拟地址时偏移解引用仍正确)。
func NewHost() (*Host, error) {
// 统一区域大小SuperBlock + StageContext segment + EvtRing segment
unifiedSize := superBlockSize + shmDefaultSize + evtTotalSize + unifiedArenaSize
// 统一区域大小SuperBlock + StageContext + EvtRing + Exchange Arena
//
// +8 是 arena 基址 8 字节对齐的 padding 余量arenaOff 向上取整可能
// 吃掉最多 4 字节,预留 8 字节保证 arenaCap 不会小于 arenaPublishSize。
unifiedSize := superBlockSize + shmDefaultSize + evtTotalSize + int(arenaPublishSize) + 8
memfd, data, err := allocShm(unifiedSize)
if err != nil {
return nil, err
@ -67,6 +77,17 @@ func NewHost() (*Host, error) {
return nil, err
}
// 初始化 Exchange Arena 槽池
arena, err := initArena(data, ur.arenaOff, ur.arenaCap, arenaDefaultSlotCount, arenaDefaultSlotSize)
if err != nil {
freeShm(memfd, data)
return nil, fmt.Errorf("槽池初始化: %w", err)
}
if used, total := arena.Stats(); used != 0 || total != arenaDefaultSlotCount {
freeShm(memfd, data)
return nil, fmt.Errorf("槽池初始化异常used=%d total=%d", used, total)
}
// 创建 StageContext segment位于 SuperBlock 之后)
seg, err := NewSegment(ur.ctxData())
if err != nil {
@ -102,6 +123,7 @@ func NewHost() (*Host, error) {
unified: ur,
seg: seg,
shmSize: unifiedSize,
arena: arena,
evtfd: evtfdReadFile(efd),
evtNotifyFd: efd,
evtRing: evtRing,
@ -302,6 +324,20 @@ func (c *stageCoordinator) finish(sc *pubsdk.StageContext, written bool) error {
return nil
}
// Arena 返回跨进程共享槽池。
func (h *Host) Arena() *arenaRegion { return h.arena }
// Generation 返回统一区域当前 generationSharedRef 校验用)。
func (h *Host) Generation() uint64 { return h.unified.generation() }
// NextOwnerID 分配一个插件专用的槽 owner ID。
//
// 从 1 开始OwnerHost=0 保留给内核),单调递增,不会重复。
func (h *Host) NextOwnerID() uint32 { return h.nextOwner.Add(1) }
// ReclaimOwner 回收某个 owner 名下所有槽(插件退出时调用)。
func (h *Host) ReclaimOwner(owner uint32) int { return h.arena.ReclaimOwner(owner) }
// ShmSize 返回共享段大小(供诊断/日志)。
func (h *Host) ShmSize() int { return h.shmSize }