mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 09:28:14 +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 回传→归还→内核读回一致且池归零)
264 lines
6.7 KiB
Go
264 lines
6.7 KiB
Go
package proc
|
||
|
||
import (
|
||
"bytes"
|
||
"sync"
|
||
"testing"
|
||
)
|
||
|
||
// newTestArena 构造一个独立槽池(不经 Host),保持单测快速。
|
||
func newTestArena(t *testing.T, slots, slotSize uint32) *arenaRegion {
|
||
t.Helper()
|
||
const base = 64
|
||
need := arenaRequiredSize(slots, slotSize)
|
||
region := make([]byte, base+need)
|
||
a, err := initArena(region, base, need, slots, slotSize)
|
||
if err != nil {
|
||
t.Fatalf("initArena: %v", err)
|
||
}
|
||
return a
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
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)
|
||
}
|
||
got, err := a.Read(ref, 0)
|
||
if err != nil {
|
||
t.Fatalf("Read: %v", err)
|
||
}
|
||
if string(got) != "hello" {
|
||
t.Fatalf("Read=%q, want hello", got)
|
||
}
|
||
|
||
if err := a.Free(OwnerHost, ref.Flags); 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("已释放槽的引用应读取失败")
|
||
}
|
||
}
|
||
|
||
func TestArena_ConcurrentAllocUnique(t *testing.T) {
|
||
const slots = 32
|
||
a := newTestArena(t, slots, 64)
|
||
|
||
refs := make(chan SharedRef, slots)
|
||
var wg sync.WaitGroup
|
||
for i := 0; i < slots; i++ {
|
||
wg.Add(1)
|
||
go func() {
|
||
defer wg.Done()
|
||
ref, err := a.Alloc(OwnerHost, 8)
|
||
if err != nil {
|
||
t.Errorf("Alloc: %v", err)
|
||
return
|
||
}
|
||
refs <- ref
|
||
}()
|
||
}
|
||
wg.Wait()
|
||
close(refs)
|
||
|
||
seen := make(map[uint32]bool, slots)
|
||
for ref := range refs {
|
||
if seen[ref.Flags] {
|
||
t.Fatalf("并发分配拿到重复槽 %d", ref.Flags)
|
||
}
|
||
seen[ref.Flags] = 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)
|
||
}
|
||
}
|
||
|
||
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)
|
||
}
|
||
}
|
||
if _, err := a.Alloc(OwnerHost, 8); 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 应被拒绝")
|
||
}
|
||
}
|
||
|
||
func TestArena_FreeEnforcesOwnership(t *testing.T) {
|
||
a := newTestArena(t, 4, 64)
|
||
const ownerA, ownerB = 7, 9
|
||
|
||
refA, err := a.Alloc(ownerA, 8)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// 另一个插件不能释放 A 的槽
|
||
if err := a.Free(ownerB, refA.Flags); err == nil {
|
||
t.Fatal("跨 owner 释放应被拒绝")
|
||
}
|
||
if used, _ := a.Stats(); used != 1 {
|
||
t.Fatalf("拒绝释放后 used=%d, want 1", used)
|
||
}
|
||
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)
|
||
}
|
||
}
|
||
|
||
func TestArena_ReclaimOwnerReleasesOnlyItsSlots(t *testing.T) {
|
||
a := newTestArena(t, 8, 64)
|
||
const ownerA, ownerB = 7, 9
|
||
|
||
for i := 0; i < 3; i++ {
|
||
if _, err := a.Alloc(ownerA, 8); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
for i := 0; i < 2; i++ {
|
||
if _, err := a.Alloc(ownerB, 8); 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)
|
||
}
|
||
// 内核自己的槽不参与回收
|
||
if n := a.ReclaimOwner(OwnerHost); n != 0 {
|
||
t.Fatalf("ReclaimOwner(host)=%d, want 0", n)
|
||
}
|
||
}
|
||
|
||
func TestArena_ReclaimHostIsNoop(t *testing.T) {
|
||
a := newTestArena(t, 4, 64)
|
||
if _, err := a.Alloc(OwnerHost, 8); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if n := a.ReclaimOwner(OwnerHost); n != 0 {
|
||
t.Fatalf("内核槽不应被 ReclaimOwner 回收,实际回收 %d", n)
|
||
}
|
||
if used, _ := a.Stats(); used != 1 {
|
||
t.Fatalf("used=%d, want 1", used)
|
||
}
|
||
}
|
||
|
||
func TestArena_ReadRejectsInvalidRefs(t *testing.T) {
|
||
a := newTestArena(t, 4, 256)
|
||
ref, err := a.Put(OwnerHost, []byte("payload"), 0)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// generation 过期
|
||
stale := ref
|
||
stale.Generation = 99
|
||
if _, err := a.Read(stale, 0); err == nil {
|
||
t.Fatal("generation 不匹配应被拒绝")
|
||
}
|
||
|
||
// offset 与槽不自洽
|
||
badOffset := ref
|
||
badOffset.Offset++
|
||
if _, err := a.Read(badOffset, 0); err == nil {
|
||
t.Fatal("offset 与槽不自洽应被拒绝")
|
||
}
|
||
|
||
// 槽号越界
|
||
badSlot := ref
|
||
badSlot.Flags = 999
|
||
if _, err := a.Read(badSlot, 0); err == nil {
|
||
t.Fatal("槽号越界应被拒绝")
|
||
}
|
||
|
||
// Length 超容量
|
||
tooLong := ref
|
||
tooLong.Length = uint32(a.SlotPayloadCap() + 1)
|
||
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 字节
|
||
ref, err := a.Put(OwnerHost, payload, 3)
|
||
if err != nil {
|
||
t.Fatalf("Put: %v", err)
|
||
}
|
||
got, err := a.Read(ref, 3)
|
||
if err != nil {
|
||
t.Fatalf("Read: %v", err)
|
||
}
|
||
if !bytes.Equal(got, payload) {
|
||
t.Fatalf("读回数据不一致:len(got)=%d len(want)=%d", len(got), len(payload))
|
||
}
|
||
}
|
||
|
||
func TestArena_AttachRejectsCorruptLayout(t *testing.T) {
|
||
a := newTestArena(t, 4, 256)
|
||
|
||
// 魔数被改
|
||
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)
|
||
if _, err := attachArena(a.region, a.base, a.cap); err == nil {
|
||
t.Fatal("槽数组越界应被拒绝")
|
||
}
|
||
}
|
||
|
||
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)
|
||
}
|
||
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)
|
||
}
|
||
}
|
||
}
|