mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-23 02:18:06 +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:
179
internal/plugin/proc/testdata/stageplugin.go
vendored
179
internal/plugin/proc/testdata/stageplugin.go
vendored
@ -57,8 +57,58 @@ func arenaSlotSize() uint32 {
|
||||
return binary.LittleEndian.Uint32(region[arenaOff+arOffSlotSize:])
|
||||
}
|
||||
|
||||
// arenaPayloadCap 返回槽可承载的最大 payload。
|
||||
func arenaPayloadCap() int { return int(arenaSlotSize()) - slotHeaderSize }
|
||||
// ---- 调用帧(funccall 模型)辅助 ----
|
||||
|
||||
// frameInput 返回帧内的输入段(内核写入的参数/输入文本)。
|
||||
func frameInput(frame SharedRef, inputLen uint32) []byte {
|
||||
if frame.IsZero() || int(inputLen) > len(frame.Slice(region)) {
|
||||
return nil
|
||||
}
|
||||
return frame.Slice(region)[:inputLen]
|
||||
}
|
||||
|
||||
// frameOutput 尝试把 payload 写进帧的结果区(帧内 [inputLen, frame.Length))。
|
||||
// 放不下时返回错误,由调用方决定是否申请扩容块。
|
||||
func frameOutput(frame SharedRef, inputLen uint32, payload []byte, jsonFlag bool) (SharedRef, error) {
|
||||
if frame.IsZero() {
|
||||
return SharedRef{}, fmt.Errorf("无调用帧")
|
||||
}
|
||||
area := frame.Slice(region)
|
||||
start := int(inputLen)
|
||||
if start > len(area) || len(payload) > len(area)-start {
|
||||
return SharedRef{}, fmt.Errorf("帧内空间不足(需 %d,剩 %d)", len(payload), len(area)-start)
|
||||
}
|
||||
copy(region[frame.Offset+uint32(start):], payload)
|
||||
ref := SharedRef{
|
||||
Offset: frame.Offset + uint32(start),
|
||||
Length: uint32(len(payload)),
|
||||
Generation: frame.Generation,
|
||||
}
|
||||
if jsonFlag {
|
||||
ref.Flags |= sharedRefFlagJSON
|
||||
}
|
||||
return ref, nil
|
||||
}
|
||||
|
||||
// arenaPut 申请一块扩容块并写入 payload,引用上打 sharedRefFlagExpand
|
||||
// 告知内核该块需单独归还(插件只申请,回收由内核做)。
|
||||
func arenaPut(payload []byte, jsonFlag bool) (SharedRef, error) {
|
||||
ref, err := arenaAlloc(uint32(len(payload)))
|
||||
if err != nil {
|
||||
return SharedRef{}, err
|
||||
}
|
||||
if len(payload) > int(ref.Length) {
|
||||
arenaFree(ref)
|
||||
return SharedRef{}, fmt.Errorf("扩容块容量不足(需 %d,得 %d)", len(payload), ref.Length)
|
||||
}
|
||||
copy(region[ref.Offset:ref.Offset+uint32(len(payload))], payload)
|
||||
ref.Length = uint32(len(payload))
|
||||
ref.Flags |= sharedRefFlagExpand
|
||||
if jsonFlag {
|
||||
ref.Flags |= sharedRefFlagJSON
|
||||
}
|
||||
return ref, nil
|
||||
}
|
||||
|
||||
// ---- 共享槽池的插件侧接口(内核 RPC,内部实现)----
|
||||
|
||||
@ -75,6 +125,19 @@ type SharedRef struct {
|
||||
|
||||
func (r SharedRef) IsZero() bool { return r.Offset == 0 && r.Length == 0 }
|
||||
|
||||
func (r SharedRef) Slice(data []byte) []byte {
|
||||
if r.IsZero() || int(r.Offset)+int(r.Length) > len(data) {
|
||||
return nil
|
||||
}
|
||||
return data[r.Offset : r.Offset+r.Length]
|
||||
}
|
||||
|
||||
// SharedRef.Flags 语义位(须与内核 internal/plugin/proc/arena.go 一致)。
|
||||
const (
|
||||
sharedRefFlagJSON = 1 << 0 // 载荷是 JSON
|
||||
sharedRefFlagExpand = 1 << 1 // 引用指向插件申请的扩容块
|
||||
)
|
||||
|
||||
// arenaAlloc 向内核申请一块共享内存,内核返回偏移与大小。
|
||||
func arenaAlloc(size uint32) (SharedRef, error) {
|
||||
raw := callKernel("arena.alloc", map[string]interface{}{"size": size})
|
||||
@ -310,6 +373,11 @@ func main() {
|
||||
"def": map[string]interface{}{"name": "demo_inject", "description": "共享内存注入"},
|
||||
"has_cleaner": false,
|
||||
})
|
||||
callKernel("tool.register", map[string]interface{}{
|
||||
"name": "demo_big",
|
||||
"def": map[string]interface{}{"name": "demo_big", "description": "报告参数量及结果回传路径"},
|
||||
"has_cleaner": false,
|
||||
})
|
||||
callKernel("stage.register", map[string]interface{}{
|
||||
"stage": "after_toolcall",
|
||||
"scope": "global",
|
||||
@ -336,11 +404,29 @@ func main() {
|
||||
// 模板对每个内核请求都 `go handleKernelRequest` 的原因。
|
||||
go func(id uint64, raw json.RawMessage) {
|
||||
var p struct {
|
||||
Name string `json:"name"`
|
||||
Args map[string]interface{} `json:"args"`
|
||||
Name string `json:"name"`
|
||||
Args map[string]interface{} `json:"args"`
|
||||
Frame SharedRef `json:"frame"`
|
||||
ArgsLen uint32 `json:"args_len"`
|
||||
}
|
||||
json.Unmarshal(raw, &p)
|
||||
text, _ := p.Args["text"].(string)
|
||||
|
||||
// 参数:内核标定帧的前段。只有直连 RPC 的调用方(无帧)才走
|
||||
// 内联 Args——生产路径永远走帧。
|
||||
args := p.Args
|
||||
argsFrom := "inline"
|
||||
if !p.Frame.IsZero() {
|
||||
argsFrom = "shared"
|
||||
if blob := frameInput(p.Frame, p.ArgsLen); len(blob) > 0 {
|
||||
var decoded map[string]interface{}
|
||||
if err := json.Unmarshal(blob, &decoded); err != nil {
|
||||
send(response{ID: id, Error: "解析共享参数: " + err.Error()})
|
||||
return
|
||||
}
|
||||
args = decoded
|
||||
}
|
||||
}
|
||||
text, _ := args["text"].(string)
|
||||
|
||||
// demo_inject 走插件侧共享内存路径:
|
||||
// 申请 → 写入 → 随业务 RPC 回传 → 归还。
|
||||
@ -356,30 +442,51 @@ func main() {
|
||||
"source": "plugin", "channel": "demo", "text_ref": ref,
|
||||
})
|
||||
arenaFree(ref)
|
||||
send(response{ID: id, Result: map[string]interface{}{"result": "injected"}})
|
||||
|
||||
blob, _ := json.Marshal("injected")
|
||||
outRef, err := frameOutput(p.Frame, p.ArgsLen, blob, true)
|
||||
if err != nil {
|
||||
outRef, err = arenaPut(blob, true)
|
||||
if err != nil {
|
||||
send(response{ID: id, Error: "结果扩容失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
send(response{ID: id, Result: map[string]interface{}{"result_ref": outRef}})
|
||||
return
|
||||
}
|
||||
|
||||
send(response{ID: id, Result: map[string]interface{}{
|
||||
"result": strings.ToUpper(text),
|
||||
}})
|
||||
var out interface{}
|
||||
if p.Name == "demo_big" {
|
||||
// 把参数来路编进结果,让测试能验证真的走了共享内存而非只看返回值。
|
||||
out = argsFrom + ":" + strings.ToUpper(text)
|
||||
} else {
|
||||
out = strings.ToUpper(text)
|
||||
}
|
||||
|
||||
blob, err := json.Marshal(out)
|
||||
if err != nil {
|
||||
send(response{ID: id, Error: "序列化结果失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
// 结果优先写进内核标定的帧;放不下才申请扩容块。
|
||||
ref, err := frameOutput(p.Frame, p.ArgsLen, blob, true)
|
||||
if err != nil {
|
||||
ref, err = arenaPut(blob, true)
|
||||
if err != nil {
|
||||
send(response{ID: id, Error: "结果扩容失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
send(response{ID: id, Result: map[string]interface{}{"result_ref": ref}})
|
||||
}(req.ID, req.Params)
|
||||
|
||||
case "cleaner.invoke":
|
||||
var p struct {
|
||||
Scope string `json:"scope"`
|
||||
Name string `json:"name"`
|
||||
Text string `json:"text"`
|
||||
TextRef struct {
|
||||
Offset uint32 `json:"offset"`
|
||||
Length uint32 `json:"length"`
|
||||
Flags uint32 `json:"flags"`
|
||||
} `json:"text_ref"`
|
||||
RespRef struct {
|
||||
Offset uint32 `json:"offset"`
|
||||
Length uint32 `json:"length"`
|
||||
Flags uint32 `json:"flags"`
|
||||
} `json:"resp_ref"`
|
||||
Scope string `json:"scope"`
|
||||
Name string `json:"name"`
|
||||
Frame SharedRef `json:"frame"`
|
||||
InputLen uint32 `json:"input_len"`
|
||||
}
|
||||
json.Unmarshal(req.Params, &p)
|
||||
valid := (p.Scope == "tool" && p.Name == "demo_upper") ||
|
||||
@ -390,27 +497,19 @@ func main() {
|
||||
continue
|
||||
}
|
||||
|
||||
// 读输入:优先共享槽,否则内联。
|
||||
input := p.Text
|
||||
if p.TextRef.Length > 0 {
|
||||
input = string(region[p.TextRef.Offset : p.TextRef.Offset+p.TextRef.Length])
|
||||
}
|
||||
input := string(frameInput(p.Frame, p.InputLen))
|
||||
output := p.Scope + "-cleaned:" + input
|
||||
|
||||
// 写结果:有预分配槽且放得下就写槽,否则内联。
|
||||
// 插件不分配任何槽(“谁分配谁释放”全部在内核侧)。
|
||||
if p.RespRef.Offset > 0 && len(output) <= arenaPayloadCap() {
|
||||
copy(region[p.RespRef.Offset:], output)
|
||||
send(response{ID: req.ID, Result: map[string]interface{}{
|
||||
"text_ref": map[string]interface{}{
|
||||
"offset": p.RespRef.Offset,
|
||||
"length": uint32(len(output)),
|
||||
"flags": p.RespRef.Flags,
|
||||
},
|
||||
}})
|
||||
continue
|
||||
// 结果优先写进内核标定的帧;放不下才申请扩容块。
|
||||
ref, err := frameOutput(p.Frame, p.InputLen, []byte(output), false)
|
||||
if err != nil {
|
||||
ref, err = arenaPut([]byte(output), false)
|
||||
if err != nil {
|
||||
send(response{ID: req.ID, Error: "结果扩容失败: " + err.Error()})
|
||||
continue
|
||||
}
|
||||
}
|
||||
send(response{ID: req.ID, Result: map[string]interface{}{"text": output}})
|
||||
send(response{ID: req.ID, Result: map[string]interface{}{"text_ref": ref}})
|
||||
|
||||
case "stage.invoke":
|
||||
go func(id uint64) {
|
||||
|
||||
Reference in New Issue
Block a user