mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 18:08:04 +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:
@ -647,88 +647,70 @@ func (h *coreHandler) cleanerProxy(scope, name string, enabled bool) (func(strin
|
||||
}, nil
|
||||
}
|
||||
|
||||
// invokeCleanerText 完成一次 Cleaner 往返:payload 走共享槽,超限时整条链路退回内联。
|
||||
// invokeCleanerText 完成一次 Cleaner 往返,使用与工具调用相同的 funccall 帧模型。
|
||||
//
|
||||
// 槽的申请与归还全部由内核负责(**谁分配谁释放**):
|
||||
//
|
||||
// reqSlot ──写入 input──▶ 随 RPC 把 TextRef 发给插件
|
||||
// respSlot ──预分配────▶ 随 RPC 把 RespRef 发给插件,插件把结果写进来
|
||||
// 读取结果后两个槽一起归还
|
||||
//
|
||||
// 插件侧不申请任何槽,因此不存在跨进程分配器的竞争。
|
||||
// 内核(caller)标定帧:输入段 + 结果预算段,插件在帧内写结果;
|
||||
// 只有结果超出预算时插件才向内核申请扩容块(插件只申请,回收由内核做)。
|
||||
func (h *coreHandler) invokeCleanerText(scope, name, text string) (string, error) {
|
||||
arena := h.host.Arena()
|
||||
gen := h.host.Generation()
|
||||
|
||||
params := CleanerInvokeParams{Scope: scope, Name: name}
|
||||
|
||||
// 请求槽:放得下就走共享内存,否则内联。
|
||||
// 注意“放不下”不是错误,只是退化成原有正确路径。
|
||||
var reqRef SharedRef
|
||||
if len(text) <= arena.SlotPayloadCap() {
|
||||
if ref, err := arena.Put(OwnerHost, []byte(text), gen); err == nil {
|
||||
params.TextRef = ref
|
||||
reqRef = ref
|
||||
}
|
||||
}
|
||||
if reqRef.IsZero() {
|
||||
params.Text = text
|
||||
frame, err := arena.Alloc(OwnerHost, len(text)+cleanerResultBudget, gen)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s %s Cleaner 分配调用帧失败: %w", scope, name, err)
|
||||
}
|
||||
defer func() { _ = arena.Free(OwnerHost, frame) }()
|
||||
|
||||
// 响应槽:预分配满容量。插件只写,不申请。
|
||||
var respRef SharedRef
|
||||
if ref, err := arena.Alloc(OwnerHost, arena.SlotPayloadCap()); err == nil {
|
||||
params.RespRef = ref
|
||||
respRef = ref
|
||||
area, err := arena.Read(frame, gen)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
copy(area[:len(text)], text)
|
||||
|
||||
defer func() {
|
||||
if !respRef.IsZero() {
|
||||
_ = arena.Free(OwnerHost, respRef.Flags)
|
||||
}
|
||||
if !reqRef.IsZero() {
|
||||
_ = arena.Free(OwnerHost, reqRef.Flags)
|
||||
}
|
||||
}()
|
||||
params := CleanerInvokeParams{
|
||||
Scope: scope,
|
||||
Name: name,
|
||||
Frame: frame,
|
||||
InputLen: uint32(len(text)),
|
||||
}
|
||||
|
||||
res, err := h.invokeCleaner(params)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 结果优先取共享槽;插件放不下时会内联返回。
|
||||
if !res.TextRef.IsZero() {
|
||||
data, err := arena.Read(res.TextRef, gen)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
// 插件申请了扩容块:内核负责归还(插件只会申请,回收由内核做)。
|
||||
if res.TextRef.IsZero() {
|
||||
return "", fmt.Errorf("%s %s Cleaner 未返回结果引用", scope, name)
|
||||
}
|
||||
return res.Text, nil
|
||||
if res.TextRef.Flags&sharedRefFlagExpand != 0 {
|
||||
defer func() { _ = arena.Free(OwnerHost, res.TextRef) }()
|
||||
}
|
||||
data, err := arena.Read(res.TextRef, gen)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// ---- 共享槽池的 syscall 风格接口(插件 RPC)----
|
||||
// ---- 共享内存的 syscall 风格接口(插件 RPC)----
|
||||
//
|
||||
// 共享内存是内部实现,不向插件开发者暴露;模板运行时在传输层调用它们,
|
||||
// 公开 SDK 仍是普通字符串/Map。
|
||||
|
||||
// arenaAlloc 给本插件分配一块共享槽并返回描述符。
|
||||
// arenaAlloc 给本插件分配一块共享内存。
|
||||
//
|
||||
// 用途:结果超出内核标定帧的预算时,插件据此申请扩容块。
|
||||
func (h *coreHandler) arenaAlloc(size uint32) (SharedRef, error) {
|
||||
if int(size) > h.host.Arena().SlotPayloadCap() {
|
||||
return SharedRef{}, fmt.Errorf("arena.alloc: 请求 %d 字节超出槽容量 %d",
|
||||
size, h.host.Arena().SlotPayloadCap())
|
||||
}
|
||||
return h.host.Arena().Alloc(h.owner, int(size))
|
||||
return h.host.Arena().Alloc(h.owner, int(size), h.host.Generation())
|
||||
}
|
||||
|
||||
// arenaFree 归还本插件申请的槽。内核校验归属,防止释放他人的槽。
|
||||
// arenaFree 归还本插件申请的块。
|
||||
//
|
||||
// 内核会走块链校验 offset 确实是某个已分配块的数据起点、owner 匹配,
|
||||
// 伪造引用不能改动分配器状态。
|
||||
func (h *coreHandler) arenaFree(ref SharedRef) error {
|
||||
slot, ok := h.host.Arena().SlotOf(ref)
|
||||
if !ok {
|
||||
return fmt.Errorf("arena.free: 非法引用(offset=%d len=%d slot=%d)",
|
||||
ref.Offset, ref.Length, ref.Flags)
|
||||
}
|
||||
return h.host.Arena().Free(h.owner, slot)
|
||||
return h.host.Arena().Free(h.owner, ref)
|
||||
}
|
||||
|
||||
// toolRegister 注册插件工具,handler 反向调用插件执行(原 case 1)。
|
||||
|
||||
Reference in New Issue
Block a user