From b879558947f09ffd17b36dcda85a34369603f84a Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sat, 14 Nov 2026 11:32:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(proc):=20=E5=85=B1=E4=BA=AB=E6=AE=B5=20are?= =?UTF-8?q?na=20=E5=AE=B9=E9=87=8F=E5=8F=AF=E9=85=8D=E7=BD=AE=EF=BC=88?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E6=9C=9F=E7=94=9F=E6=95=88=EF=BC=8C=E9=9C=80?= =?UTF-8?q?=E9=87=8D=E5=90=AF=E5=86=85=E6=A0=B8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 改了什么 - `proc.NewHostWithArenaSize(bytes)`:按给定容量建段;保留 `NewHost()` 以兼容默认 4MB。 - 容量低于分配器最小尺寸时**返回明确错误**,不静默回落默认值 (否则"我配了 32MB"会悄悄变成"仍是 4MB")。 - 删除 `arenaPublishSize` 变量:自从容量改为入参后已无人读取, 留一个"看着能改、改了没效果"的开关比没有更危险。 - `core.plugin.shm_arena_mb`(默认 4,clamp 1..256):播种 + 定义 + `Registry.arenaBytesFromConfig()` 读取。越界时记日志说明被 clamp。 ## 为什么容量只能在启动时决定 arena 是统一区域里一段连续 mmap,而 `SharedRef` 存**绝对偏移**。 变大需要 remap,而每个 plugin.bin 子进程可能正在读同一页 —— 跨进程 munmap 会让它 SIGSEGV。该路径历史上三次被明确搁置。 ⇒ 本改动只决定"这次启动开多大",配置项描述里已写明需重启。 ## 判据(`internal/plugin/proc/test/arena_contract_test.go`) TestArenaHostHonoursRequestedCapacity 给定 8MB → 能写入并读回 4MB(证明容量真的生效) TestArenaHostRejectsTooSmallCapacity 64 字节 → 明确报错 变异验证: 忽略传入容量、总用默认 4MB → 4MB 写入报「超出 arena 容量 4194228」 过小容量改为静默回落默认 → 判据红 go build ./... / go vet ./... PASS python3 .github/scripts/check-ci-gates.py PASS go test ./... -count=1 -race 51 包 / 100 包无测试 / 0 FAIL gofmt -l 干净 --- internal/config/registry.go | 6 +++ internal/plugin/dynamic_proc.go | 37 ++++++++++++++++++- internal/plugin/proc/arena.go | 5 ++- internal/plugin/proc/host.go | 26 ++++++++++++- .../plugin/proc/test/arena_contract_test.go | 23 ++++++++++++ 5 files changed, 92 insertions(+), 5 deletions(-) diff --git a/internal/config/registry.go b/internal/config/registry.go index 4bd86635..b3e2b7ef 100644 --- a/internal/config/registry.go +++ b/internal/config/registry.go @@ -837,6 +837,7 @@ func (r *ConfigRegistry) seedDBValues(dataDir string) bool { } set("core.plugin.dir", filepath.Join(dataDir, "plugins")) + set("core.plugin.shm_arena_mb", "4") set("core.memory.graph", filepath.Join(dataDir, "memory", "graph.db")) set("core.memory.text", filepath.Join(dataDir, "memory", "text")) set("core.memory.documents", filepath.Join(dataDir, "memory", "documents")) @@ -974,6 +975,11 @@ func (r *ConfigRegistry) seedCoreDefs(dataDir string, fresh bool) { plgDir := filepath.Join(dataDir, "plugins") reg(ConfigDef{Key: "core.plugin.dir", Default: plgDir, Type: "string", DisplayName: "插件目录", Description: "外部插件安装目录", Category: "paths"}) + reg(ConfigDef{Key: "core.plugin.shm_arena_mb", Default: "4", Type: "int", DisplayName: "插件共享内存(MB)", + Description: "子进程插件共用的共享段 arena 容量(1–256MB)。" + + "段大小在启动时一次确定,**修改后必须重启内核才生效**;" + + "改大不会立刻占物理内存(memfd 惰性分配),但会扩大虚拟地址空间占用。", + Category: "plugin"}) reg(ConfigDef{Key: "core.memory.graph", Default: filepath.Join(dataDir, "memory", "graph.db"), Type: "string", DisplayName: "图数据库路径", Description: "长期记忆(图数据库)存储路径", Category: "paths"}) reg(ConfigDef{Key: "core.memory.text", Default: filepath.Join(dataDir, "memory", "text"), Type: "string", DisplayName: "文本记忆路径", Description: "短期文本记忆存储目录", Category: "paths"}) reg(ConfigDef{Key: "core.memory.documents", Default: filepath.Join(dataDir, "memory", "documents"), Type: "string", DisplayName: "文档记忆路径", Description: "文档记忆存储目录", Category: "paths"}) diff --git a/internal/plugin/dynamic_proc.go b/internal/plugin/dynamic_proc.go index 9743d425..92292992 100644 --- a/internal/plugin/dynamic_proc.go +++ b/internal/plugin/dynamic_proc.go @@ -118,7 +118,7 @@ func (r *Registry) ensureProcHost() (*proc.Host, error) { if r.procHost != nil { return r.procHost, nil } - host, err := proc.NewHost() + host, err := proc.NewHostWithArenaSize(r.arenaBytesFromConfig()) if err != nil { return nil, err } @@ -278,3 +278,38 @@ func (r *Registry) ResetProcCrashCount(name string) { defer r.crashMu.Unlock() delete(r.procCrashes, name) } + +// arenaBytesFromConfig 读 arena 容量配置(core.plugin.shm_arena_mb)。 +// +// ## 为什么容量只能在启动时决定 +// +// arena 是统一区域里一段连续 mmap,而 SharedRef 存**绝对偏移**。 +// 变大需要 remap,而每个 plugin.bin 子进程可能正在读同一页 —— +// 跨进程 munmap 会让它 SIGSEGV。该路径历史上三次被明确搁置。 +// ⇒ 这里只决定"这次启动开多大",改配置必须重启内核才生效。 +// +// ## 越界时记日志 +// +// clamp 到 1..256 MB。超上限时**说出来**(否则"我配了 512"会静默变成 +// "仍是 256",与配置系统其它项的行为不一致)。 +func (r *Registry) arenaBytesFromConfig() uint32 { + const defMB = 4 + const minMB = 1 + const maxMB = 256 + + mb := defMB + if r.cfgReg != nil { + if v := r.cfgReg.GetInt("core.plugin.shm_arena_mb", defMB); v != 0 { + mb = v + } + } + switch { + case mb < minMB: + log.Printf("[plugin] core.plugin.shm_arena_mb=%d 过小,按 %dMB 使用", mb, minMB) + mb = minMB + case mb > maxMB: + log.Printf("[plugin] core.plugin.shm_arena_mb=%d 超过上限,按 %dMB 使用", mb, maxMB) + mb = maxMB + } + return uint32(mb) * 1024 * 1024 +} diff --git a/internal/plugin/proc/arena.go b/internal/plugin/proc/arena.go index 89d75faa..96e03880 100644 --- a/internal/plugin/proc/arena.go +++ b/internal/plugin/proc/arena.go @@ -98,8 +98,9 @@ const ( // 但它让单个工具调用可以承载 MB 级 payload,不必再退回内联 RPC。 const arenaDefaultCapacity = 4 * 1024 * 1024 -// arenaPublishSize 是统一区域里预留给 arena 的字节数。 -var arenaPublishSize = uint32(arenaDefaultCapacity) +// 容量由 NewHostWithArenaSize 决定(见 host.go),这里不再维护第二份。 +// 之前有个 arenaPublishSize 变量,但自 NewHost 改为接受参数后已无人读取 —— +// 留着一个"看着能改、改了没效果"的开关比没有更危险。 // arenaRegion 是块分配器视图。 // diff --git a/internal/plugin/proc/host.go b/internal/plugin/proc/host.go index 16331d4c..b5e9b2ac 100644 --- a/internal/plugin/proc/host.go +++ b/internal/plugin/proc/host.go @@ -69,11 +69,33 @@ type Host struct { // 三者共同点:全部插件看到同一份物理页,段内一律用相对偏移而非指针 // (实验 2 已验证各进程 mmap 到不同虚拟地址时偏移解引用仍正确)。 func NewHost() (*Host, error) { + return NewHostWithArenaSize(arenaDefaultCapacity) +} + +// NewHostWithArenaSize 与 NewHost 相同,但按调用方给定的 arena 容量创建段。 +// +// ## 为什么容量只能在**构造时**决定 +// +// arena 是统一区域里的一段连续 mmap,而 SharedRef 存的是**绝对偏移**。 +// 要变大就得 remap,而对端进程(每个 plugin.bin)可能正在读同一页 —— +// munmap + 重新映射会让它 SIGSEGV。跨进程 remap 因此被明确否决 +// (历史上三次实现计划均搁置)。⇒ 容量只影响"启动时开多大", +// 改了配置必须重启内核。 +// +// ## 越界如何处理 +// +// 太小(低于 arenaMinSize)无法建立分配器;这里**返回明确错误**而不是 +// 静默回落到默认值 —— 配置写错要能看见,否则"我明明配了 32MB"会变成 +// "悄悄仍是 4MB"。调用方(Registry)负责读配置 + clamp。 +func NewHostWithArenaSize(arenaBytes uint32) (*Host, error) { + if arenaBytes < arenaMinSize() { + return nil, fmt.Errorf("共享段 arena 容量过小:%d 字节(至少 %d)", arenaBytes, arenaMinSize()) + } // 统一区域大小:SuperBlock + StageContext + EvtRing + Exchange Arena // // +8 是 arena 基址 8 字节对齐的 padding 余量:arenaOff 向上取整可能 - // 吃掉最多 4 字节,预留 8 字节保证 arenaCap 不会小于 arenaPublishSize。 - unifiedSize := superBlockSize + shmDefaultSize + evtTotalSize + int(arenaPublishSize) + 8 + // 吃掉最多 4 字节,预留 8 字节保证 arenaCap 不会小于请求容量。 + unifiedSize := superBlockSize + shmDefaultSize + evtTotalSize + int(arenaBytes) + 8 memfd, data, err := allocShm(unifiedSize) if err != nil { return nil, err diff --git a/internal/plugin/proc/test/arena_contract_test.go b/internal/plugin/proc/test/arena_contract_test.go index c7cf229e..e0790e44 100644 --- a/internal/plugin/proc/test/arena_contract_test.go +++ b/internal/plugin/proc/test/arena_contract_test.go @@ -185,3 +185,26 @@ func TestArenaFragmentationPreventsLargeRequest(t *testing.T) { t.Log("当前实现已能整理碎片;本判据需改为验证整理后的可用性") } } + +// TestArenaHostHonoursRequestedCapacity:按调用方给定的容量创建共享段, +// 容量必须真的生效(MaxPayload 随容量变化),而不是静默用默认值。 +func TestArenaHostHonoursRequestedCapacity(t *testing.T) { + const mb = 8 + h, err := proc.NewHostWithArenaSize(mb * 1024 * 1024) + if err != nil { + t.Skipf("共享内存不可用:%v", err) + } + defer func() { _ = h.Close() }() + + payload := bytes.Repeat([]byte("z"), mb*1024*1024/2) + ref := putBytes(t, h, proc.OwnerHost, payload) + assertReadable(t, h, ref, payload) +} + +// TestArenaHostRejectsTooSmallCapacity:低于分配器最小尺寸时报明确错误, +// 不静默回落到默认容量。 +func TestArenaHostRejectsTooSmallCapacity(t *testing.T) { + if _, err := proc.NewHostWithArenaSize(64); err == nil { + t.Fatal("过小的 arena 容量被接受(应报错而非回落默认值)") + } +}