Files
HomeAgent/internal/plugin/proc/arena.go
JianFeeeee ca584bda3f 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 回传→归还→内核读回一致且池归零)
2026-09-10 17:56:50 +08:00

412 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
}
}