mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 15:53:56 +00:00
refactor(shm): 变长块分配器 + 工具调用 funccall 调用帧(§13.2/§13.3)
按架构约束推进两步:
1) 分配器收回内核后,定长槽失去了唯一存在理由
(定长槽只是为了绕开"跨进程无法安全变长分配"),
于是 arena 换成**变长块分配器**:first-fit + 邻块合并,
块头 16B(size/state/owner/prevSize)。内核可以按需标定每块大小,
payload 不再受固定槽容量限制(旧上限 16KB)。
- Alloc/Put/Read/Free/ReclaimOwner 全在内核进程内,一把 sync.Mutex
- Free 走块链校验 offset 是已分配块的数据起点 + owner 匹配,
伪造引用不能改动分配器状态
- Read 允许块内偏移(调用帧的结果区就在帧块中间),但不许跨块边界
- arena 4MB,底层 memfd 惰性分配:未触碰的页不占物理内存
2) 工具调用 payload **始终**走共享内存,取消按大小切内联的分支。
按 funccall 模型,内核(caller)标定调用帧交给插件(callee):
[0, ArgsLen) 参数 JSON
[ArgsLen, Frame.Length) 结果区(内核预留的预算)
结果超出预算时插件才 `arena.alloc` 扩容块,引用上打
sharedRefFlagExpand 让内核单独归还。Cleaner 复用同一帧模型。
ToolInvokeParams.Args / ToolInvokeResult.Result 仅剩给直连 RPC 的
测试(process/bench 不建 Host);生产路径永远走帧。
测试:
- TestArena_*(分配/归属/回收/并发唯一/耗尽/超限/伪造 offset/
相邻合并/对齐/布局校验)
- TestPlugin_ToolInvokeArgsResultViaArena:大**小** payload 都经帧往返,
结果一致且 arena 归零("小 payload 同样走调用帧"是本轮行为变更)
- TestPlugin_ArenaAllocFreeAcrossProcess、TestE2E_* 保持通过
- 顺手修 process_test.go 一处预存 doc-comment 分段
This commit is contained in:
@ -6,13 +6,12 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// newTestArena 构造一个独立槽池(不经 Host),保持单测快速。
|
||||
func newTestArena(t *testing.T, slots, slotSize uint32) *arenaRegion {
|
||||
// newTestArena 构造一个独立块分配器(不经 Host),保持单测快速。
|
||||
func newTestArena(t *testing.T, size uint32) *arenaRegion {
|
||||
t.Helper()
|
||||
const base = 64
|
||||
need := arenaRequiredSize(slots, slotSize)
|
||||
region := make([]byte, base+need)
|
||||
a, err := initArena(region, base, need, slots, slotSize)
|
||||
const base = 64 // 8 字节对齐
|
||||
region := make([]byte, base+size)
|
||||
a, err := initArena(region, base, size)
|
||||
if err != nil {
|
||||
t.Fatalf("initArena: %v", err)
|
||||
}
|
||||
@ -20,17 +19,17 @@ func newTestArena(t *testing.T, slots, slotSize uint32) *arenaRegion {
|
||||
}
|
||||
|
||||
func TestArena_AllocFreeRoundTrip(t *testing.T) {
|
||||
a := newTestArena(t, 4, 64)
|
||||
if got, total := a.Stats(); got != 0 || total != 4 {
|
||||
t.Fatalf("初始 Stats: got (%d,%d), want (0,4)", got, total)
|
||||
a := newTestArena(t, 8*1024)
|
||||
if used, total := a.Stats(); used != 0 || total != 8*1024 {
|
||||
t.Fatalf("初始 Stats: got (%d,%d), want (0,%d)", used, total, 8*1024)
|
||||
}
|
||||
|
||||
ref, err := a.Put(OwnerHost, []byte("hello"), 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
}
|
||||
if used, _ := a.Stats(); used != 1 {
|
||||
t.Fatalf("Put 后 used=%d, want 1", used)
|
||||
if used, _ := a.Stats(); used == 0 {
|
||||
t.Fatal("Put 后 used 应大于 0")
|
||||
}
|
||||
got, err := a.Read(ref, 0)
|
||||
if err != nil {
|
||||
@ -40,29 +39,29 @@ func TestArena_AllocFreeRoundTrip(t *testing.T) {
|
||||
t.Fatalf("Read=%q, want hello", got)
|
||||
}
|
||||
|
||||
if err := a.Free(OwnerHost, ref.Flags); err != nil {
|
||||
if err := a.Free(OwnerHost, ref); err != nil {
|
||||
t.Fatalf("Free: %v", err)
|
||||
}
|
||||
if used, _ := a.Stats(); used != 0 {
|
||||
t.Fatalf("Free 后 used=%d, want 0", used)
|
||||
}
|
||||
// 归还后再读必须失败(槽已回收)
|
||||
// 归还后再读必须失败(块已回收)
|
||||
if _, err := a.Read(ref, 0); err == nil {
|
||||
t.Fatal("已释放槽的引用应读取失败")
|
||||
t.Fatal("已释放块的引用应读取失败")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArena_ConcurrentAllocUnique(t *testing.T) {
|
||||
const slots = 32
|
||||
a := newTestArena(t, slots, 64)
|
||||
const workers = 64
|
||||
a := newTestArena(t, 256*1024)
|
||||
|
||||
refs := make(chan SharedRef, slots)
|
||||
refs := make(chan SharedRef, workers)
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < slots; i++ {
|
||||
for i := 0; i < workers; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
ref, err := a.Alloc(OwnerHost, 8)
|
||||
ref, err := a.Alloc(OwnerHost, 1024, 0)
|
||||
if err != nil {
|
||||
t.Errorf("Alloc: %v", err)
|
||||
return
|
||||
@ -73,105 +72,121 @@ func TestArena_ConcurrentAllocUnique(t *testing.T) {
|
||||
wg.Wait()
|
||||
close(refs)
|
||||
|
||||
seen := make(map[uint32]bool, slots)
|
||||
seen := make(map[uint32]bool, workers)
|
||||
for ref := range refs {
|
||||
if seen[ref.Flags] {
|
||||
t.Fatalf("并发分配拿到重复槽 %d", ref.Flags)
|
||||
if seen[ref.Offset] {
|
||||
t.Fatalf("并发分配拿到重复 offset=%d", ref.Offset)
|
||||
}
|
||||
seen[ref.Flags] = true
|
||||
seen[ref.Offset] = true
|
||||
}
|
||||
if len(seen) != slots {
|
||||
t.Fatalf("唯一槽数=%d, want %d", len(seen), slots)
|
||||
}
|
||||
if used, _ := a.Stats(); used != slots {
|
||||
t.Fatalf("used=%d, want %d", used, slots)
|
||||
if len(seen) != workers {
|
||||
t.Fatalf("唯一块数=%d, want %d", len(seen), workers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArena_ExhaustionReturnsError(t *testing.T) {
|
||||
a := newTestArena(t, 2, 64)
|
||||
for i := 0; i < 2; i++ {
|
||||
if _, err := a.Alloc(OwnerHost, 8); err != nil {
|
||||
t.Fatalf("第 %d 次 Alloc: %v", i, err)
|
||||
}
|
||||
a := newTestArena(t, 4*1024)
|
||||
// 第一次分配吃掉几乎整块
|
||||
if _, err := a.Alloc(OwnerHost, 3*1024, 0); err != nil {
|
||||
t.Fatalf("首次 Alloc: %v", err)
|
||||
}
|
||||
if _, err := a.Alloc(OwnerHost, 8); err == nil {
|
||||
t.Fatal("槽池耗尽时 Alloc 应返回错误")
|
||||
// 再申请一大块必然失败
|
||||
if _, err := a.Alloc(OwnerHost, 3*1024, 0); err == nil {
|
||||
t.Fatal("空间不足时 Alloc 应返回错误")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArena_RejectsOversizePayload(t *testing.T) {
|
||||
a := newTestArena(t, 2, 64)
|
||||
if _, err := a.Alloc(OwnerHost, a.SlotPayloadCap()+1); err == nil {
|
||||
t.Fatal("超出槽容量的 payload 应被拒绝")
|
||||
a := newTestArena(t, 4*1024)
|
||||
if _, err := a.Alloc(OwnerHost, a.MaxPayload()+1, 0); err == nil {
|
||||
t.Fatal("超出 arena 容量的请求应被拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArena_FreeEnforcesOwnership(t *testing.T) {
|
||||
a := newTestArena(t, 4, 64)
|
||||
a := newTestArena(t, 8*1024)
|
||||
const ownerA, ownerB = 7, 9
|
||||
|
||||
refA, err := a.Alloc(ownerA, 8)
|
||||
refA, err := a.Alloc(ownerA, 128, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 另一个插件不能释放 A 的槽
|
||||
if err := a.Free(ownerB, refA.Flags); err == nil {
|
||||
t.Fatal("跨 owner 释放应被拒绝")
|
||||
// 另一个插件不能释放 A 的块
|
||||
if err := a.Free(ownerB, refA); err == nil {
|
||||
t.Fatal("跨 owner 归还应被拒绝")
|
||||
}
|
||||
if used, _ := a.Stats(); used != 1 {
|
||||
t.Fatalf("拒绝释放后 used=%d, want 1", used)
|
||||
if used, _ := a.Stats(); used == 0 {
|
||||
t.Fatal("拒绝归还后块应仍然占用")
|
||||
}
|
||||
if a.OwnerOf(refA.Flags) != ownerA {
|
||||
t.Fatalf("槽 %d 的 owner 应为 %d", refA.Flags, ownerA)
|
||||
}
|
||||
// 正确的 owner 可以释放
|
||||
if err := a.Free(ownerA, refA.Flags); err != nil {
|
||||
t.Fatalf("同 owner 释放: %v", err)
|
||||
// 正确的 owner 可以归还
|
||||
if err := a.Free(ownerA, refA); err != nil {
|
||||
t.Fatalf("同 owner 归还: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArena_ReclaimOwnerReleasesOnlyItsSlots(t *testing.T) {
|
||||
a := newTestArena(t, 8, 64)
|
||||
func TestArena_FreeRejectsForgedOffset(t *testing.T) {
|
||||
a := newTestArena(t, 8*1024)
|
||||
ref, err := a.Alloc(OwnerHost, 256, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 未对齐到块数据起点的 offset 必须被拒绝——否则会直接破坏块链。
|
||||
forged := ref
|
||||
forged.Offset += 8
|
||||
if err := a.Free(OwnerHost, forged); err == nil {
|
||||
t.Fatal("伪造 offset 应被拒绝")
|
||||
}
|
||||
// 合法引用仍能正常归还(分配器状态未被破坏)
|
||||
if err := a.Free(OwnerHost, ref); err != nil {
|
||||
t.Fatalf("合法引用不应受影响: %v", err)
|
||||
}
|
||||
if used, _ := a.Stats(); used != 0 {
|
||||
t.Fatalf("归还后 used=%d, want 0", used)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArena_ReclaimOwnerReleasesOnlyItsBlocks(t *testing.T) {
|
||||
a := newTestArena(t, 64*1024)
|
||||
const ownerA, ownerB = 7, 9
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
if _, err := a.Alloc(ownerA, 8); err != nil {
|
||||
if _, err := a.Alloc(ownerA, 1024, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
if _, err := a.Alloc(ownerB, 8); err != nil {
|
||||
if _, err := a.Alloc(ownerB, 1024, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if n := a.ReclaimOwner(ownerA); n != 3 {
|
||||
t.Fatalf("ReclaimOwner(A)=%d, want 3", n)
|
||||
}
|
||||
if used, _ := a.Stats(); used != 2 {
|
||||
t.Fatalf("回收 A 后 used=%d, want 2(B 的槽必须保留)", used)
|
||||
// B 的两块必须保留:仍能读回
|
||||
if n := a.ReclaimOwner(ownerB); n != 2 {
|
||||
t.Fatalf("ReclaimOwner(B)=%d, want 2(A 的回收不能误伤 B)", n)
|
||||
}
|
||||
// 内核自己的槽不参与回收
|
||||
if n := a.ReclaimOwner(OwnerHost); n != 0 {
|
||||
t.Fatalf("ReclaimOwner(host)=%d, want 0", n)
|
||||
if used, _ := a.Stats(); used != 0 {
|
||||
t.Fatalf("全部回收后 used=%d, want 0", used)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArena_ReclaimHostIsNoop(t *testing.T) {
|
||||
a := newTestArena(t, 4, 64)
|
||||
if _, err := a.Alloc(OwnerHost, 8); err != nil {
|
||||
a := newTestArena(t, 4*1024)
|
||||
if _, err := a.Alloc(OwnerHost, 128, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n := a.ReclaimOwner(OwnerHost); n != 0 {
|
||||
t.Fatalf("内核槽不应被 ReclaimOwner 回收,实际回收 %d", n)
|
||||
t.Fatalf("内核块不应被 ReclaimOwner 回收,实际回收 %d", n)
|
||||
}
|
||||
if used, _ := a.Stats(); used != 1 {
|
||||
t.Fatalf("used=%d, want 1", used)
|
||||
if used, _ := a.Stats(); used == 0 {
|
||||
t.Fatal("used 应仍大于 0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArena_ReadRejectsInvalidRefs(t *testing.T) {
|
||||
a := newTestArena(t, 4, 256)
|
||||
a := newTestArena(t, 8*1024)
|
||||
ref, err := a.Put(OwnerHost, []byte("payload"), 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@ -184,31 +199,24 @@ func TestArena_ReadRejectsInvalidRefs(t *testing.T) {
|
||||
t.Fatal("generation 不匹配应被拒绝")
|
||||
}
|
||||
|
||||
// offset 与槽不自洽
|
||||
// offset 不是块数据起点
|
||||
badOffset := ref
|
||||
badOffset.Offset++
|
||||
badOffset.Offset += 8
|
||||
if _, err := a.Read(badOffset, 0); err == nil {
|
||||
t.Fatal("offset 与槽不自洽应被拒绝")
|
||||
t.Fatal("offset 不是块数据起点应被拒绝")
|
||||
}
|
||||
|
||||
// 槽号越界
|
||||
badSlot := ref
|
||||
badSlot.Flags = 999
|
||||
if _, err := a.Read(badSlot, 0); err == nil {
|
||||
t.Fatal("槽号越界应被拒绝")
|
||||
}
|
||||
|
||||
// Length 超容量
|
||||
// Length 超出块容量
|
||||
tooLong := ref
|
||||
tooLong.Length = uint32(a.SlotPayloadCap() + 1)
|
||||
tooLong.Length = 1 << 20
|
||||
if _, err := a.Read(tooLong, 0); err == nil {
|
||||
t.Fatal("Length 超容量应被拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArena_PutReadLargePayload(t *testing.T) {
|
||||
a := newTestArena(t, 4, 4096)
|
||||
payload := bytes.Repeat([]byte("abcdefgh"), 400) // 3200 字节
|
||||
a := newTestArena(t, 512*1024)
|
||||
payload := bytes.Repeat([]byte("abcdefgh"), 8192) // 64KB
|
||||
ref, err := a.Put(OwnerHost, payload, 3)
|
||||
if err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
@ -220,44 +228,80 @@ func TestArena_PutReadLargePayload(t *testing.T) {
|
||||
if !bytes.Equal(got, payload) {
|
||||
t.Fatalf("读回数据不一致:len(got)=%d len(want)=%d", len(got), len(payload))
|
||||
}
|
||||
// 大 payload 不能被 16KB 定长槽时代的容量假设卡住
|
||||
if len(got) <= 16*1024 {
|
||||
t.Fatalf("本用例应验证超过旧 16KB 槽容量的 payload,实际 %d", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestArena_FreeCoalescesAdjacentBlocks(t *testing.T) {
|
||||
a := newTestArena(t, 64*1024)
|
||||
|
||||
refs := make([]SharedRef, 0, 8)
|
||||
for i := 0; i < 8; i++ {
|
||||
r, err := a.Alloc(OwnerHost, 4*1024, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("第 %d 次 Alloc: %v", i, err)
|
||||
}
|
||||
refs = append(refs, r)
|
||||
}
|
||||
for _, r := range refs {
|
||||
if err := a.Free(OwnerHost, r); err != nil {
|
||||
t.Fatalf("Free: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 全部归还并合并后,应能再分配一个接近整块的大块。
|
||||
big, err := a.Alloc(OwnerHost, 48*1024, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("合并后应能分配大块: %v", err)
|
||||
}
|
||||
if err := a.Free(OwnerHost, big); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n := a.BlockCount(); n != 1 {
|
||||
t.Fatalf("全部归还并合并后应只剩 1 块,实际 %d", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArena_AttachRejectsCorruptLayout(t *testing.T) {
|
||||
a := newTestArena(t, 4, 256)
|
||||
a := newTestArena(t, 4*1024)
|
||||
|
||||
// 魔数被改
|
||||
putU32(a.region[a.base+arOffMagic:], 0xDEADBEEF)
|
||||
if _, err := attachArena(a.region, a.base, a.cap); err == nil {
|
||||
t.Fatal("魔数错误应被拒绝")
|
||||
}
|
||||
putU32(a.region[a.base+arOffMagic:], arenaMagic)
|
||||
|
||||
// 版本被改
|
||||
putU32(a.region[a.base+arOffVersion:], 99)
|
||||
if _, err := attachArena(a.region, a.base, a.cap); err == nil {
|
||||
t.Fatal("版本不匹配应被拒绝")
|
||||
}
|
||||
putU32(a.region[a.base+arOffVersion:], arenaVersion)
|
||||
|
||||
// 槽数组越界
|
||||
putU32(a.region[a.base+arOffSlotCount:], 1<<20)
|
||||
putU32(a.region[a.base+arOffCapacity:], 1<<30)
|
||||
if _, err := attachArena(a.region, a.base, a.cap); err == nil {
|
||||
t.Fatal("槽数组越界应被拒绝")
|
||||
t.Fatal("capacity 不匹配应被拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArenaRequiredSizeAlignment(t *testing.T) {
|
||||
// 位图按 8 字节对齐、槽数组 8 字节对齐,保证 4 字节原子操作不跨页/不越界。
|
||||
for _, tc := range []struct{ slots, slotSize uint32 }{
|
||||
{1, 16}, {8, 64}, {32, 16384}, {33, 1024}, {64, 512}, {100, 256},
|
||||
} {
|
||||
need := arenaRequiredSize(tc.slots, tc.slotSize)
|
||||
if need%8 != 0 {
|
||||
t.Errorf("slots=%d slotSize=%d: arenaRequiredSize=%d 不是 8 的倍数", tc.slots, tc.slotSize, need)
|
||||
func TestArena_InitRejectsTooSmall(t *testing.T) {
|
||||
const base = 64
|
||||
region := make([]byte, base+arenaMinSize()-8)
|
||||
if _, err := initArena(region, base, arenaMinSize()-8); err == nil {
|
||||
t.Fatal("过小的段应被拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArena_DataOffsetsAligned(t *testing.T) {
|
||||
a := newTestArena(t, 64*1024)
|
||||
for i, size := range []uint32{1, 7, 8, 9, 100, 4096} {
|
||||
ref, err := a.Alloc(OwnerHost, int(size), 0)
|
||||
if err != nil {
|
||||
t.Fatalf("第 %d 次 Alloc(size=%d): %v", i, size, err)
|
||||
}
|
||||
region := make([]byte, 64+need)
|
||||
if _, err := initArena(region, 64, need, tc.slots, tc.slotSize); err != nil {
|
||||
t.Errorf("slots=%d slotSize=%d: initArena 失败: %v", tc.slots, tc.slotSize, err)
|
||||
if ref.Offset%8 != 0 {
|
||||
t.Fatalf("size=%d 的数据起点 %d 未 8 字节对齐", size, ref.Offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user