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:
JianFeeeee
2026-09-10 18:54:36 +08:00
parent f6f92a8a6e
commit b096e8ba2c
10 changed files with 921 additions and 516 deletions

View File

@ -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) {