mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-23 02:18:06 +00:00
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 回传→归还→内核读回一致且池归零)
This commit is contained in:
263
internal/plugin/proc/arena_test.go
Normal file
263
internal/plugin/proc/arena_test.go
Normal file
@ -0,0 +1,263 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user