mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 17:38:10 +00:00
推翻前两版跨进程分配器设计,根因是"共享内存里放了只被单一进程 更新的可变游标": - 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 回传→归还→内核读回一致且池归零)
257 lines
9.2 KiB
Go
257 lines
9.2 KiB
Go
package proc
|
||
|
||
// 统一共享内存区域布局(§13.1)。
|
||
//
|
||
// 之前的两块独立 memfd(StageContext 256KB + EvtRing ~320KB,fd 3/4)
|
||
// 合并为单一 memfd(fd 3),eventfd 独占 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) generation(atomic uint64,resize 时 bump)
|
||
// [16,20) capacity(区域总字节数)
|
||
// [20,24) ctxOff(StageContext segment 偏移)
|
||
// [24,28) ctxSize(StageContext segment 字节数)
|
||
// [28,32) evtOff(EvtRing segment 偏移)
|
||
// [32,36) evtSize(EvtRing segment 字节数)
|
||
// [36,40) arenaOff(Exchange Arena 起点,8 字节对齐)
|
||
// [40,44) arenaCap(Exchange 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.go),unifiedRegion 只负责布局与 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,不存在"整体重置"语义。
|