Files
HomeAgent/internal/plugin/proc/unified.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

257 lines
9.2 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
// 统一共享内存区域布局§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,40) arenaOffExchange Arena 起点8 字节对齐)
// [40,44) arenaCapExchange Arena 字节数)
// [44,64) reserved
const (
sbOffMagic = 0
sbOffVersion = 4
sbOffGeneration = 8
sbOffCapacity = 16
sbOffCtxOff = 20
sbOffCtxSize = 24
sbOffEvtOff = 28
sbOffEvtSize = 32
sbOffArenaOff = 36 // Exchange Arena 起点
sbOffArenaCap = 40 // Exchange Arena 容量
sbOffReserved1 = 44
sbOffReserved5 = 48
sbOffReserved6 = 52
sbOffReserved7 = 56
sbOffReserved8 = 60
)
// 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 区域。
//
// 注意:这是**无校验**的原始切片,仅供已知安全的路径使用。
// 跨进程引用一律走 arenaRegion.Read校验 generation / 槽号 / 占用状态)。
func (r SharedRef) Slice(data []byte) []byte {
end := uint64(r.Offset) + uint64(r.Length)
if r.IsZero() || end > uint64(len(data)) {
return nil
}
return data[r.Offset:uint32(end)]
}
// unifiedRegion 统一共享内存区域的内核侧视图。
//
// arenaOff/arenaCap 指向 Exchange Arena 段;槽池句柄由 Host 持有
// (见 arena.gounifiedRegion 只负责布局与 generation。
type unifiedRegion struct {
data []byte
ctxOff uint32
ctxSize uint32
evtOff uint32
evtSize uint32
arenaOff uint32 // Exchange Arena 起点8 字节对齐)
arenaCap uint32 // Exchange Arena 容量
}
// 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)
// arena 基址必须 8 字节对齐:位图用 4 字节原子操作,槽头含 uint32。
arenaOff := (evtOff + uint32(evtTotal) + 7) &^ 7
arenaCap := cap - arenaOff
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))
putU32(data[sbOffArenaOff:], arenaOff)
putU32(data[sbOffArenaCap:], arenaCap)
return &unifiedRegion{
data: data,
ctxOff: ctxOff,
ctxSize: uint32(ctxTotal),
evtOff: evtOff,
evtSize: uint32(evtTotal),
arenaOff: arenaOff,
arenaCap: arenaCap,
}, 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:])
capacity := getU32(data[sbOffCapacity:])
arenaOff := getU32(data[sbOffArenaOff:])
arenaCap := getU32(data[sbOffArenaCap:])
if capacity != uint32(len(data)) {
return nil, fmt.Errorf("unified: capacity 不匹配header=%d mapped=%d", capacity, len(data))
}
if uint64(ctxOff)+uint64(ctxSize) > uint64(len(data)) {
return nil, fmt.Errorf("unified: StageContext 越界off=%d size=%d total=%d", ctxOff, ctxSize, len(data))
}
if uint64(evtOff)+uint64(evtSize) > uint64(len(data)) {
return nil, fmt.Errorf("unified: EvtRing 越界off=%d size=%d total=%d", evtOff, evtSize, len(data))
}
if uint64(arenaOff)+uint64(arenaCap) > uint64(len(data)) {
return nil, fmt.Errorf("unified: arena 越界off=%d cap=%d total=%d", arenaOff, arenaCap, len(data))
}
return &unifiedRegion{
data: data,
ctxOff: ctxOff,
ctxSize: ctxSize,
evtOff: evtOff,
evtSize: evtSize,
arenaOff: arenaOff,
arenaCap: arenaCap,
}, 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
}
// arenaReset 已移除。
//
// 旧实现把共享游标重置为 1会覆盖其他进程/goroutine 仍在使用的分配。
// 现设计用槽位图:释放是精确的 per-slot CAS不存在"整体重置"语义。