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

@ -50,9 +50,9 @@ const (
// [24,28) ctxSizeStageContext segment 字节数)
// [28,32) evtOffEvtRing segment 偏移)
// [32,36) evtSizeEvtRing segment 字节数)
// [36,48) reserved未来 lane 偏移/大小
// [48,56) reserved
// [56,64) reserved(对齐到 8 字节)
// [36,40) arenaOffExchange Arena 起点8 字节对齐
// [40,44) arenaCapExchange Arena 字节数)
// [44,64) reserved
const (
sbOffMagic = 0
sbOffVersion = 4
@ -62,17 +62,15 @@ const (
sbOffCtxSize = 24
sbOffEvtOff = 28
sbOffEvtSize = 32
sbOffArenaOff = 36 // 动态 arena 起始偏移
sbOffArenaCap = 40 // 动态 arena 容量
sbOffArenaUsed = 44 // 动态 arena 已用字节(原子)
sbOffArenaOff = 36 // Exchange Arena 起
sbOffArenaCap = 40 // Exchange Arena 容量
sbOffReserved1 = 44
sbOffReserved5 = 48
sbOffReserved6 = 52
sbOffReserved7 = 56
sbOffReserved8 = 60
)
const unifiedArenaSize = 256 * 1024 // 默认动态 arena 256KB
// SharedRef 是跨进程共享内存描述符,替代内联 JSON 数据。
//
// 所有数据交换(工具调用参数/结果、Cleaner、输入/输出通道消息)
@ -93,6 +91,9 @@ func (r SharedRef) IsZero() bool {
}
// Slice 从共享内存中按 SharedRef 切片。data 必须是完整的 mmap 区域。
//
// 注意:这是**无校验**的原始切片,仅供已知安全的路径使用。
// 跨进程引用一律走 arenaRegion.Read校验 generation / 槽号 / 占用状态)。
func (r SharedRef) Slice(data []byte) []byte {
end := uint64(r.Offset) + uint64(r.Length)
if r.IsZero() || end > uint64(len(data)) {
@ -102,6 +103,9 @@ func (r SharedRef) Slice(data []byte) []byte {
}
// unifiedRegion 统一共享内存区域的内核侧视图。
//
// arenaOff/arenaCap 指向 Exchange Arena 段;槽池句柄由 Host 持有
// (见 arena.gounifiedRegion 只负责布局与 generation。
type unifiedRegion struct {
data []byte
@ -110,9 +114,8 @@ type unifiedRegion struct {
evtOff uint32
evtSize uint32
arenaOff uint32 // 动态 arena 起始偏移(相对 data
arenaCap uint32 // 动态 arena 容量
arenaUsed *uint32 // 指向 SuperBlock 中的 arenaUsed 字段(原子 bump 游标)
arenaOff uint32 // Exchange Arena 起点8 字节对齐
arenaCap uint32 // Exchange Arena 容量
}
// initUnifiedRegion 在 mmap 区域上初始化 SuperBlock + 两个 segment。
@ -125,7 +128,8 @@ func initUnifiedRegion(data []byte, ctxTotal, evtTotal int) (*unifiedRegion, err
ctxOff := uint32(superBlockSize)
evtOff := ctxOff + uint32(ctxTotal)
arenaOff := evtOff + uint32(evtTotal)
// arena 基址必须 8 字节对齐:位图用 4 字节原子操作,槽头含 uint32。
arenaOff := (evtOff + uint32(evtTotal) + 7) &^ 7
arenaCap := cap - arenaOff
putU32(data[sbOffMagic:], unifiedMagic)
@ -138,7 +142,6 @@ func initUnifiedRegion(data []byte, ctxTotal, evtTotal int) (*unifiedRegion, err
putU32(data[sbOffEvtSize:], uint32(evtTotal))
putU32(data[sbOffArenaOff:], arenaOff)
putU32(data[sbOffArenaCap:], arenaCap)
putU32(data[sbOffArenaUsed:], 1)
return &unifiedRegion{
data: data,
@ -148,9 +151,6 @@ func initUnifiedRegion(data []byte, ctxTotal, evtTotal int) (*unifiedRegion, err
evtSize: uint32(evtTotal),
arenaOff: arenaOff,
arenaCap: arenaCap,
arenaUsed: (*uint32)(unsafe.Pointer(
uintptr(unsafe.Pointer(&data[0])) + uintptr(sbOffArenaUsed),
)),
}, nil
}
@ -197,9 +197,6 @@ func attachUnifiedRegion(data []byte) (*unifiedRegion, error) {
evtSize: evtSize,
arenaOff: arenaOff,
arenaCap: arenaCap,
arenaUsed: (*uint32)(unsafe.Pointer(
uintptr(unsafe.Pointer(&data[0])) + uintptr(sbOffArenaUsed),
)),
}, nil
}
@ -253,56 +250,7 @@ func getU64(b []byte) uint64 {
uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56
}
// arenaAlloc 在动态 arena 中分配 n 字节,返回相对 arenaOff 的偏移
// append-onlyoffset 0 保留给“空”语义。
func (r *unifiedRegion) arenaAlloc(n int) (uint32, error) {
if n <= 0 {
return 0, fmt.Errorf("arena: 非法分配长度 %d", n)
}
for {
used := atomic.LoadUint32(r.arenaUsed)
if used == 0 {
used = 1
}
end := uint64(used) + uint64(n)
if end > uint64(r.arenaCap) {
return 0, fmt.Errorf("arena: 空间不足(需 %d剩 %d", n, uint64(r.arenaCap)-uint64(used))
}
if atomic.CompareAndSwapUint32(r.arenaUsed, used, uint32(end)) {
return used, nil
}
}
}
// arenaWrite 把 b 写入动态 arena 并返回 SharedRef。
func (r *unifiedRegion) arenaWrite(b []byte) (SharedRef, error) {
if len(b) == 0 {
return SharedRef{}, nil
}
off, err := r.arenaAlloc(len(b))
if err != nil {
return SharedRef{}, err
}
base := r.arenaOff + off
copy(r.data[base:base+uint32(len(b))], b)
gen := uint32(r.generation())
return SharedRef{Offset: base, Length: uint32(len(b)), Generation: gen}, nil
}
// arenaRead 按 SharedRef 读取数据。generation 不匹配或引用越出 arena 时拒绝。
func (r *unifiedRegion) arenaRead(ref SharedRef) []byte {
if ref.IsZero() || ref.Generation != uint32(r.generation()) {
return nil
}
end := uint64(ref.Offset) + uint64(ref.Length)
arenaEnd := uint64(r.arenaOff) + uint64(r.arenaCap)
if uint64(ref.Offset) < uint64(r.arenaOff)+1 || end > arenaEnd {
return nil
}
return ref.Slice(r.data)
}
// arenaReset 压实后重置游标(仅在无活跃 slot 时调用)。
func (r *unifiedRegion) arenaReset() {
atomic.StoreUint32(r.arenaUsed, 1)
}
// arenaReset 已移除
//
// 旧实现把共享游标重置为 1会覆盖其他进程/goroutine 仍在使用的分配。
// 现设计用槽位图:释放是精确的 per-slot CAS不存在"整体重置"语义。