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

@ -0,0 +1,411 @@
package proc
// Exchange Arena**内核独占管理**的跨进程共享内存槽池§13.2 重设计)。
//
// ── 所有权模型(架构约束)────────────────────────────────────────────
//
// 共享内存由内核全权管理。插件需要使用共享内存时,通过 syscall 风格的
// RPC 向内核申请,内核返回偏移与大小;使用完毕后插件再通知内核回收。
//
// 因此分配器只存在于**内核进程内**,用一把普通 sync.Mutex 保护即可:
// 不需要任何跨进程原子操作,也不存在"共享游标被两个进程各自更新"这一
// 类竞态——这正是前两版bump 游标 / CAS 位图跨进程分配)失败的根本原因。
//
// 共享内存是**内部实现**,不对插件开发者暴露:插件的公开 API 仍是
// 普通字符串/Map见 SDK 的 IOInjector / ToolHandler。模板运行时在
// 传输层按 payload 大小自动决定走内联 JSON 还是共享槽,开发者无感。
//
// ── 槽布局 ────────────────────────────────────────────────────────
//
// ┌──────────────────────────────────────────────┐
// │ Arena Header 64B │
// │ magic / version / slotCount / slotSize │
// │ bitmapOff / slotsOff / reserved │
// ├──────────────────────────────────────────────┤
// │ Bitmapceil(slotCount/32) 个 uint32bit=占用│
// ├──────────────────────────────────────────────┤
// │ Slot 0: [dataLen(4) | owner(4) | data(...)] │
// │ ... │
// └──────────────────────────────────────────────┘
//
// ── 所有权与回收 ──────────────────────────────────────────────────
//
// 每个插件进程从内核领一个不透明 ownerID。槽头记录 owner。
// - 内核 → 插件:内核直接 Alloc把 ref 放进 RPC 参数(插件不分配)
// - 插件 → 内核:插件 RPC arena.alloc 申请,用完 RPC arena.free 归还
// - 插件退出:内核 ReclaimOwner 回收其残留槽,避免崩溃泄漏耗尽池
//
// 插件归还时校验 owner防止一个插件释放另一个插件的槽。
import (
"fmt"
"sync"
"sync/atomic"
"unsafe"
)
const (
arenaMagic uint32 = 0x41524E41 // "ARNA"
arenaVersion uint32 = 1
arenaHeaderSize = 64
// Arena Header 字段偏移(相对 arena 起始)。
arOffMagic = 0
arOffVersion = 4
arOffSlotCount = 8
arOffSlotSize = 12
arOffBitmapOff = 16
arOffSlotsOff = 20
arOffReserved = 24
// 槽头:数据长度 + 所有者。数据紧随其后。
slotHeaderSize = 8
slotOffDataLen = 0
slotOffOwner = 4
// OwnerHost 标记由内核分配的槽。插件用 Host.NextOwnerID 分配的值。
OwnerHost uint32 = 0
// 槽池默认规格32 槽 × 16KB ≈ 512KB。
arenaDefaultSlotCount = 32
arenaDefaultSlotSize = 16 * 1024
)
// arenaRequiredSize 返回给定规格的槽池所需字节数(含头、位图对齐与槽数组)。
func arenaRequiredSize(slotCount, slotSize uint32) uint32 {
bitmapBytes := ((slotCount+31)/32)*4 + 7
bitmapBytes &^= 7 // 位图按 8 字节对齐
slotsOff := (arenaHeaderSize + bitmapBytes + 7) &^ 7
return slotsOff + slotCount*slotSize
}
// arenaPublishSize 是统一区域里预留给槽池的字节数。
var arenaPublishSize = arenaRequiredSize(arenaDefaultSlotCount, arenaDefaultSlotSize)
// arenaRegion 是槽池视图。
//
// region 始终是**完整**统一区域 mmapSharedRef.Offset 是相对区域起始的
// 绝对偏移,因此读写都直接落在 region 上,不需要再换算。
//
// mu 只在**内核进程内**使用——分配器完全由内核持有(见文件头所有权模型)。
type arenaRegion struct {
mu sync.Mutex
region []byte
base uint32 // arena 在 region 内的起始偏移
cap uint32 // arena 可用字节数
slots uint32
slotSize uint32
bmOff uint32 // 位图在 arena 内的偏移
slotsOff uint32 // 槽数组在 arena 内的偏移
}
// initArena 在统一区域的 arena 段上初始化槽池并返回句柄。
func initArena(region []byte, base, size uint32, slotCount, slotSize uint32) (*arenaRegion, error) {
if slotCount == 0 || slotSize <= slotHeaderSize {
return nil, fmt.Errorf("arena: 非法规格slots=%d slotSize=%d", slotCount, slotSize)
}
need := arenaRequiredSize(slotCount, slotSize)
if need > size {
return nil, fmt.Errorf("arena: 段太小(需要 %d实际 %d", need, size)
}
if uint64(base)+uint64(need) > uint64(len(region)) {
return nil, fmt.Errorf("arena: 越出映射base=%d need=%d total=%d", base, need, len(region))
}
bitmapBytes := ((slotCount+31)/32)*4 + 7
bitmapBytes &^= 7
slotsOff := (arenaHeaderSize + bitmapBytes + 7) &^ 7
ap := region[base : base+need]
putU32(ap[arOffMagic:], arenaMagic)
putU32(ap[arOffVersion:], arenaVersion)
putU32(ap[arOffSlotCount:], slotCount)
putU32(ap[arOffSlotSize:], slotSize)
putU32(ap[arOffBitmapOff:], arenaHeaderSize)
putU32(ap[arOffSlotsOff:], slotsOff)
// 显式清零位图与槽头,保证复用已存在区域时状态干净。
for i := uint32(0); i < bitmapBytes; i++ {
ap[arenaHeaderSize+i] = 0
}
for i := uint32(0); i < slotCount; i++ {
sb := slotsOff + i*slotSize
putU32(ap[sb+slotOffDataLen:], 0)
putU32(ap[sb+slotOffOwner:], 0)
}
return &arenaRegion{
region: region,
base: base,
cap: need,
slots: slotCount,
slotSize: slotSize,
bmOff: arenaHeaderSize,
slotsOff: slotsOff,
}, nil
}
// attachArena 从已初始化的区域解析槽池(诊断用;模板不再需要解析位图)。
func attachArena(region []byte, base, size uint32) (*arenaRegion, error) {
if size < arenaHeaderSize {
return nil, fmt.Errorf("arena: 段太小(%d", size)
}
if uint64(base)+uint64(arenaHeaderSize) > uint64(len(region)) {
return nil, fmt.Errorf("arena: 头部越界base=%d total=%d", base, len(region))
}
ap := region[base : base+size]
if got := getU32(ap[arOffMagic:]); got != arenaMagic {
return nil, fmt.Errorf("arena: 魔数不匹配0x%x", got)
}
if got := getU32(ap[arOffVersion:]); got != arenaVersion {
return nil, fmt.Errorf("arena: 版本不匹配(%d期望 %d", got, arenaVersion)
}
slotCount := getU32(ap[arOffSlotCount:])
slotSize := getU32(ap[arOffSlotSize:])
bmOff := getU32(ap[arOffBitmapOff:])
slotsOff := getU32(ap[arOffSlotsOff:])
if slotCount == 0 || slotSize <= slotHeaderSize {
return nil, fmt.Errorf("arena: 非法规格slots=%d slotSize=%d", slotCount, slotSize)
}
if uint64(slotsOff)+uint64(slotCount)*uint64(slotSize) > uint64(size) {
return nil, fmt.Errorf("arena: 槽数组越出段slotsOff=%d slots=%d slotSize=%d size=%d",
slotsOff, slotCount, slotSize, size)
}
if uint64(bmOff)+uint64(((slotCount+31)/32)*4) > uint64(size) {
return nil, fmt.Errorf("arena: 位图越出段bmOff=%d slots=%d", bmOff, slotCount)
}
return &arenaRegion{
region: region,
base: base,
cap: size,
slots: slotCount,
slotSize: slotSize,
bmOff: bmOff,
slotsOff: slotsOff,
}, nil
}
// SlotCount 返回槽总数。
func (a *arenaRegion) SlotCount() uint32 { return a.slots }
// SlotPayloadCap 返回单个槽可承载的最大 payload 字节数。
func (a *arenaRegion) SlotPayloadCap() int { return int(a.slotSize) - slotHeaderSize }
func (a *arenaRegion) bitmapWord(i uint32) *uint32 {
off := a.base + a.bmOff + i*4
return (*uint32)(unsafe.Pointer(&a.region[off]))
}
func (a *arenaRegion) slotBase(slot uint32) uint32 {
return a.base + a.slotsOff + slot*a.slotSize
}
// dataOffset 返回槽数据区的绝对偏移SharedRef.Offset 即此值)。
func (a *arenaRegion) dataOffset(slot uint32) uint32 {
return a.slotBase(slot) + slotHeaderSize
}
// Alloc 预留一个槽并写入 owner返回共享引用Flags 即槽号Length 为 n
//
// 必须由内核调用:分配器由内核独占(见文件头所有权模型)。
func (a *arenaRegion) Alloc(owner uint32, n int) (SharedRef, error) {
if n < 0 || n > a.SlotPayloadCap() {
return SharedRef{}, fmt.Errorf("arena: payload %d 超出槽容量 %d", n, a.SlotPayloadCap())
}
a.mu.Lock()
defer a.mu.Unlock()
for slot := uint32(0); slot < a.slots; slot++ {
if a.testAndSetLocked(slot) {
base := a.slotBase(slot)
putU32(a.region[base+slotOffDataLen:], uint32(n))
putU32(a.region[base+slotOffOwner:], owner)
return SharedRef{
Offset: a.dataOffset(slot),
Length: uint32(n),
Flags: slot,
}, nil
}
}
return SharedRef{}, fmt.Errorf("arena: 槽池已满(%d 槽全部占用)", a.slots)
}
// Put 分配并写入 payload返回共享引用内核方向发送用
func (a *arenaRegion) Put(owner uint32, payload []byte, gen uint64) (SharedRef, error) {
ref, err := a.Alloc(owner, len(payload))
if err != nil {
return SharedRef{}, err
}
copy(a.region[ref.Offset:ref.Offset+ref.Length], payload)
ref.Generation = uint32(gen)
return ref, nil
}
// MakeRef 为已分配槽构造引用Length 为 n
func (a *arenaRegion) MakeRef(slot uint32, n int, gen uint64) SharedRef {
return SharedRef{
Offset: a.dataOffset(slot),
Length: uint32(n),
Generation: uint32(gen),
Flags: slot,
}
}
// SlotOf 从 SharedRef 解出槽号,并校验它与 Offset 自洽。
func (a *arenaRegion) SlotOf(ref SharedRef) (uint32, bool) {
slot := ref.Flags
if slot >= a.slots {
return 0, false
}
if uint32(ref.Offset) != a.dataOffset(slot) {
return 0, false
}
if int(ref.Length) > a.SlotPayloadCap() {
return 0, false
}
return slot, true
}
// Read 按 SharedRef 读取槽数据。
//
// 校验四件事,任一不满足都返回 error而不是像早期版本那样返回 nil
// 让调用方分不清"空数据"和"非法引用"
// 1. generation 与当前区域一致remap 后旧引用失效)
// 2. 槽号在范围内,且 Offset 与槽数据区自洽
// 3. Length 不超过槽容量
// 4. 槽当前处于占用状态(已被释放的槽不可再读)
func (a *arenaRegion) Read(ref SharedRef, gen uint64) ([]byte, error) {
if ref.IsZero() {
return nil, nil
}
if ref.Generation != uint32(gen) {
return nil, fmt.Errorf("arena: 引用 generation 过期ref=%d now=%d", ref.Generation, gen)
}
slot, ok := a.SlotOf(ref)
if !ok {
return nil, fmt.Errorf("arena: 非法引用offset=%d len=%d slot=%d", ref.Offset, ref.Length, ref.Flags)
}
if !a.IsBusy(slot) {
return nil, fmt.Errorf("arena: 槽 %d 已被释放,引用失效", slot)
}
start := a.dataOffset(slot)
return a.region[start : start+ref.Length], nil
}
// Free 释放 owner 名下的槽。
//
// 校验 owner 是关键安全边界:一个插件不能释放另一个插件(或内核)的槽。
func (a *arenaRegion) Free(owner uint32, slot uint32) error {
a.mu.Lock()
defer a.mu.Unlock()
if slot >= a.slots {
return fmt.Errorf("arena: 槽号越界 %d", slot)
}
if !a.isBusyLocked(slot) {
return fmt.Errorf("arena: 槽 %d 未分配(重复释放?)", slot)
}
base := a.slotBase(slot)
got := getU32(a.region[base+slotOffOwner:])
if got != owner {
return fmt.Errorf("arena: 槽 %d 不属于调用者owner=%d caller=%d", slot, got, owner)
}
a.clearLocked(slot)
return nil
}
// ReclaimOwner 释放 owner 名下所有槽,返回回收数量。
//
// 用于插件进程退出:崩溃的插件无法归还自己申请的槽,若不管会把池慢慢
// 耗尽,最终让所有走共享内存的调用退化成内联 RPC。
func (a *arenaRegion) ReclaimOwner(owner uint32) int {
if owner == OwnerHost {
return 0 // 内核自己的槽由正常路径释放,不在此回收
}
a.mu.Lock()
defer a.mu.Unlock()
reclaimed := 0
for slot := uint32(0); slot < a.slots; slot++ {
if !a.isBusyLocked(slot) {
continue
}
base := a.slotBase(slot)
if getU32(a.region[base+slotOffOwner:]) != owner {
continue
}
a.clearLocked(slot)
reclaimed++
}
return reclaimed
}
// Stats 返回 (已用槽数, 总槽数),供诊断与测试断言。
func (a *arenaRegion) Stats() (used, total uint32) {
for slot := uint32(0); slot < a.slots; slot++ {
if a.IsBusy(slot) {
used++
}
}
return used, a.slots
}
// OwnerOf 返回槽当前的 owner诊断用槽空闲时返回 OwnerHost
func (a *arenaRegion) OwnerOf(slot uint32) uint32 {
if slot >= a.slots {
return OwnerHost
}
return getU32(a.region[a.slotBase(slot)+slotOffOwner:])
}
// IsBusy 报告槽当前是否被占用。
func (a *arenaRegion) IsBusy(slot uint32) bool {
if slot >= a.slots {
return false
}
return atomic.LoadUint32(a.bitmapWord(slot/32))&(1<<(slot%32)) != 0
}
// ---- 位图内部操作(调用方必须持有 a.mu----
func (a *arenaRegion) isBusyLocked(slot uint32) bool {
return atomic.LoadUint32(a.bitmapWord(slot/32))&(1<<(slot%32)) != 0
}
// testAndSetLocked 尝试占用 slot已占用返回 false。
func (a *arenaRegion) testAndSetLocked(slot uint32) bool {
w := a.bitmapWord(slot / 32)
bit := uint32(1) << (slot % 32)
for {
cur := atomic.LoadUint32(w)
if cur&bit != 0 {
return false
}
if atomic.CompareAndSwapUint32(w, cur, cur|bit) {
return true
}
}
}
func (a *arenaRegion) clearLocked(slot uint32) {
w := a.bitmapWord(slot / 32)
bit := uint32(1) << (slot % 32)
for {
cur := atomic.LoadUint32(w)
if cur&bit == 0 {
return
}
if atomic.CompareAndSwapUint32(w, cur, cur&^bit) {
base := a.slotBase(slot)
putU32(a.region[base+slotOffDataLen:], 0)
putU32(a.region[base+slotOffOwner:], 0)
return
}
}
}