From c5f236a569ff2df931093b1e6355deb5f1a70e87 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sun, 25 Oct 2026 13:37:29 +0800 Subject: [PATCH] =?UTF-8?q?refactor(agent/core):=20=E4=B8=AD=E6=96=AD?= =?UTF-8?q?=E9=98=9F=E5=88=97=E5=BD=92=E8=BF=98=E8=B0=83=E5=BA=A6=E5=99=A8?= =?UTF-8?q?=EF=BC=8C=E4=B8=AD=E6=96=AD=E7=AE=A1=E7=90=86=E5=99=A8=E5=8F=AA?= =?UTF-8?q?=E7=95=99=E7=8E=B0=E5=9C=BA=E4=BF=9D=E6=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户裁定:「中断队列就该调度器管理啊,中断管理器只负责进入中断的任务」。 上一版我把 interruptQueues/immediate 也搬进了中断管理器,这是错的: 排队是**调度职责**——判断该不该打断、打断不了先排队、按什么次序再取出来, 全是调度策略,不是"进入中断"本身。 ## 本次改动 中断管理器(interrupt.go)现在只有: - 中断栈 suspendStack(**唯一操作者**:压栈/弹栈都在其内) - 中断屏蔽 critical(对应 CPU 关中断) - 让位信号原语 preemptArmed/preemptLevel - 栈状态钩子 stackNotify 迁回调度器(scheduler.go): - interruptQueues[5] / immediate 两个字段 - highestInterruptLocked / popInterruptLocked / interruptCountLocked - enqueueInterruptLocked / installImmediateLocked ## 新增边界判据 TestInvariant_InterruptManagerHoldsOnlyStackState 用**反射白名单**钉住边界: interruptState 只允许 6 个字段(栈/屏蔽/让位×2/上界/钩子)。 反向确认 interruptQueues 与 immediate 确实在 scheduler 上——避免"两边都没有"的假绿。 变异验证:把 interruptQueues 塞回 interruptState ⇒ 判据立刻变红 ("中断管理器不应持有这些字段:[interruptQueues]")。 ## 验证 - go build ./... 通过 - go test ./internal/agent/core/ 通过 - go test -race ./internal/agent/core/ 通过 - 12 条不变量判据全绿 ## 顺带排查:/tmp 是 tmpfs 且已 91% `df` 显示 /tmp 是 8.8G tmpfs,Go 链接期临时文件把它填满, 导致并发构建出现 write ... no space left on device(表现为 某包 `FAIL 0.008s`——不是代码问题)。已改用 TMPDIR=/var/tmp/gobuild(根分区 112G)。 --- docs/zh/interrupt-subsystem-split-plan.md | 18 ++- internal/agent/core/interrupt.go | 111 +++++------------- .../agent/core/interrupt_invariant_test.go | 40 +++++++ internal/agent/core/scheduler.go | 80 ++++++++++++- 4 files changed, 157 insertions(+), 92 deletions(-) diff --git a/docs/zh/interrupt-subsystem-split-plan.md b/docs/zh/interrupt-subsystem-split-plan.md index 54b92de0..cc631d7d 100644 --- a/docs/zh/interrupt-subsystem-split-plan.md +++ b/docs/zh/interrupt-subsystem-split-plan.md @@ -355,14 +355,24 @@ internal/agent/core/ | 3 验证 | ✅ 完成 | 全仓 `go test -p 1 ./...` 通过;`-race` 三关键包绿;变异验证通过 | | 中断向量表 | ⏳ 未做 | §7 已设计,按 §7.6 顺序应在拆分完成后单独做 | -### 实际划定的边界(比原计划更严格) +### 最终边界(用户第四次裁定:队列归调度器) -用户三次裁定后确定的最终边界: +> 「中断队列就该调度器管理啊,中断管理器只负责进入中断的任务」 + +这条纠正了我第一版的错误:我把中断队列(`interruptQueues` / `immediate`) +也搬进了中断管理器。**排队是调度职责**——判断该不该打断、打断不了先排队、 +按什么次序再取出来,全是调度策略。 + +最终边界: | 归属 | 内容 | | --- | --- | -| **中断子系统**(`interrupt.go`) | 中断队列、immediate 槽、**中断栈(唯一操作者)**、中断屏蔽(critical)、让位信号原语、**栈状态钩子** | -| **调度器**(`scheduler.go`) | 任务优先级语义(`Level`/`effectiveLevel`/`canPreempt`/`clampPluginLevel`)、**恢复决策**(`nextInterruptSelection`)、就绪队列、停止配额、背压 | +| **中断管理器**(`interrupt.go`) | **中断栈(唯一操作者)**、中断屏蔽(critical)、让位信号原语、**栈状态钩子** —— 只负责"进入中断"这件事本身 | +| **调度器**(`scheduler.go`) | **中断队列**(`interruptQueues`/`immediate`)、优先级语义(`Level`/`effectiveLevel`/`canPreempt`/`clampPluginLevel`)、**恢复决策**(`nextInterruptSelection`)、就绪队列、停止配额、背压 | + +字段级判据:`TestInvariant_InterruptManagerHoldsOnlyStackState` 用反射白名单 +钉住这一点——`interruptState` 只允许 6 个字段(栈/屏蔽/让位/上界/钩子), +把队列塞回去立刻变红(已变异验证)。 关键设计:**中断栈只有中断管理器能操作**。调度器通过 `stackTopLocked()` **只读查询**栈顶做优先级比较,真正的弹栈由 `restoreTopLocked()` 在中断管理器内完成—— diff --git a/internal/agent/core/interrupt.go b/internal/agent/core/interrupt.go index c16cc3d2..a890074d 100644 --- a/internal/agent/core/interrupt.go +++ b/internal/agent/core/interrupt.go @@ -1,36 +1,34 @@ package core -// 中断子系统 —— 只做三件事:**按优先级打断、保护被打断者的现场、恢复它**。 +// 中断管理器 —— 只负责"进入中断"这件事本身:**现场保护**与**中断屏蔽**。 // -// ## 边界(2026-10-25 用户裁定,OS 语义) +// ## 边界(2026-10-25 用户多次栽定,OS 语义) // -// 中断类似操作系统中的中断语义,只提供: +// 用户原话:「中断管理器只负责进入中断和中断栈的部分」、 +// 「中断队列就该调度器管理」。 // -// · 中断优先级 —— 谁打断谁的唯一依据(Level / effectiveLevel / canPreempt) -// · 中断队列 —— 待处理中断按优先级分组排队(interruptQueues / immediate) -// · 现场保护 —— 被打断任务的现场压栈、以及**恢复决策**(suspendStack) -// · 中断屏蔽 —— 不可抢占临界区(critical,对应 CPU 的关中断) +// 所以本文件**只有两样东西**: // -// **不含**:就绪队列、停止配额、任务 ID 分配、统计、背压、"执行任务"本身。 -// 调度器建立在本子系统之上——它问"中断侧该出谁",然后执行那个任务。 +// · 中断栈 —— 被打断任务的现场压栈/弹栈(**唯一操作者**) +// · 中断屏蔽 —— 不可抢占临界区(critical,对应 CPU 的关中断) +// · 让位信号 —— preemptArmed/preemptLevel 的读写原语 +// · 栈状态钩子 —— 栈变化时通告调度器 // -// ## 职责边界(不要越界) +// **不含**(全部归调度器): // -// 本文件**只提供机制,不做决策**: +// · 中断队列 —— interruptQueues / immediate(排队是调度职责) +// · 优先级语义 —— Level / effectiveLevel / canPreempt / clampPluginLevel +// · 该不该打断 —— registerInterrupt(含冷却期/临界区判定) +// · 该恢复谁 —— nextInterruptSelection(栈顶 vs 中断队列比较) +// · 就绪队列 / 停止配额 / 背压 / 任务 ID / 统计 // -// 进中断 —— enqueueInterruptLocked / setImmediateLocked -// 中断栈 —— suspend(压栈)/ 弹栈原语 -// 中断屏蔽 —— critical -// 让位信号 —— preemptArmed/preemptLevel 的读写原语 +// ## 依赖方向(单向) // -// 而**策略**住在 scheduler.go: +// 调度器 ──查询栈状态──> 中断管理器 +// 调度器 <──栈状态钩子── 中断管理器 // -// 谁比谁优先 —— effectiveLevel / canPreempt / clampPluginLevel -// 能不能打断 —— registerInterrupt(含冷却期/临界区判定) -// 该恢复谁 —— nextInterruptSelection(栈顶 vs 中断队列比较) -// -// 换句话说:调度器问"现在恢复谁",然后调用这里的原语执行。 -// 中断管理器**不自行比较优先级**,也不看就绪队列。 +// 中断管理器**不知道**调度器如何选取、如何比较优先级;它只负责保存与恢复现场。 +// 将来给中断子系统加向量表、换多栈策略,都不必改调度器。 // // ## 核心不变量 // @@ -40,14 +38,14 @@ package core // L4 之上没有更高级别,链到此为止。超限只可能是内核 bug,因此只做防御性计数, // **不降级、不丢弃帧**。 // -// 判据见 interrupt_invariant_test.go(I1..I6)。 +// 判据见 interrupt_invariant_test.go。 import ( "sync/atomic" "time" ) -// interruptState 是中断子系统的全部状态。 +// interruptState 是中断管理器的全部状态(只有现场保护与屏蔽,队列不在这里)。 // // 内嵌进 scheduler(字段提升),因此访问写法仍是 s.interruptQueues 等—— // 这样拆分对既有代码与测试透明,风险最低。 @@ -56,15 +54,6 @@ import ( // 中断判定需要看"当前运行任务/是否临界区",与调度状态在同一临界区内被 // 一起读写。拆锁是并发模型变更,不属于本次重构。 type interruptState struct { - // interruptQueues[level]:四条**中断队列**(level 1..4),同级 FIFO。 - // 未能立即抢占的中断(级别不足,或运行任务在临界区)按级别入队。 - interruptQueues [5][]*Task - - // immediate:已判定的最高优先待运行者。抢占必须**立即生效**, - // 所以它不经队列,在下一个安全点直接运行。 - // 这也消除了"抢占者与被抢占者同级"的比较问题——抢占者不需和栈顶比。 - immediate *Task - // suspendStack:**中断栈**。被抢占后保存现场的任务压栈(LIFO)。 // 只有**栈顶**参与恢复选择,栈内不做优先级重排。 suspendStack []*suspendedTask @@ -117,51 +106,16 @@ const ( // ── 中断队列操作 ── // highestInterruptLocked 返回当前最高级非空中断队列的队头及其级别。 -func (s *scheduler) highestInterruptLocked() (*Task, Level) { - for lv := LevelCritical; lv >= LevelBackground; lv-- { - if q := s.interruptQueues[lv]; len(q) > 0 { - return q[0], lv - } - } - return nil, 0 -} + // popInterruptLocked 弹出某级别中断队列的队头(调用方已确认非空)。 -func (s *scheduler) popInterruptLocked(lv Level) { - s.interruptQueues[lv] = s.interruptQueues[lv][1:] -} + // interruptCountLocked 统计所有待处理中断(含 immediate 槽)。 -func (s *scheduler) interruptCountLocked() int { - n := 0 - for lv := LevelBackground; lv <= LevelCritical; lv++ { - n += len(s.interruptQueues[lv]) - } - if s.immediate != nil { - n++ - } - return n -} + // enqueueInterruptLocked 把中断按级别入队。 -func (s *scheduler) enqueueInterruptLocked(t *Task) { - s.allocateIDLocked(t) - if s.interruptCountLocked() >= s.maxQueue { - for lv := LevelBackground; lv <= LevelCritical; lv++ { - if len(s.interruptQueues[lv]) > 0 { - s.popInterruptLocked(lv) - s.stats.Rejected++ - break - } - } - } - lv := t.Level - if lv < LevelBackground || lv > LevelCritical { - lv = DefaultLevel - } - s.interruptQueues[lv] = append(s.interruptQueues[lv], t) - s.stats.Enqueued++ -} + // installImmediateLocked 把 t 放进 immediate 槽(**纯机制**,不比较优先级)。 // @@ -169,18 +123,7 @@ func (s *scheduler) enqueueInterruptLocked(t *Task) { // 这里只做:把现任降级入队 → 新任务占槽。 // // 返回被顶掉的旧持有者(可能为 nil),调用方据此保持不变量的可观测性。 -func (s *scheduler) installImmediateLocked(t *Task) *Task { - var demoted *Task - if s.immediate != nil { - // 被顶掉的抢占者降级入队,**不丢**(不变量 I5)。 - demoted = s.immediate - s.enqueueInterruptLocked(demoted) - } - s.allocateIDLocked(t) - s.stats.Enqueued++ - s.immediate = t - return demoted -} + // ── 中断登记(抢占或入队)── diff --git a/internal/agent/core/interrupt_invariant_test.go b/internal/agent/core/interrupt_invariant_test.go index c8b78c15..e699595b 100644 --- a/internal/agent/core/interrupt_invariant_test.go +++ b/internal/agent/core/interrupt_invariant_test.go @@ -21,6 +21,7 @@ package core // 否则搬完只能靠"看起来还对"来判断,而这类不变量恰恰是肉眼看不出的。 import ( + "reflect" "testing" agentIO "github.com/JianFeeeee/HologramAgent/internal/agent/io" @@ -224,6 +225,45 @@ func TestInvariant_SuspendedEqualsResumed(t *testing.T) { } } +// ── I0:边界不变量 —— 队列归调度器,中断管理器只管现场保护 ── + +// 用户栽定:「中断管理器只负责进入中断和中断栈的部分」、 +// 「中断队列就该调度器管理」。 +// +// 这条判据用**结构事实**钉住边界,而不是靠注释: +// interruptState 里只允许出现现场保护/屏蔽相关字段; +// 一旦有人把队列或优先级策略塞回来,这里立刻变红。 +func TestInvariant_InterruptManagerHoldsOnlyStackState(t *testing.T) { + // 反射枚举 interruptState 的字段名,白名单校验。 + typ := reflect.TypeOf(interruptState{}) + allowed := map[string]bool{ + "suspendStack": true, // 中断栈 + "critical": true, // 中断屏蔽 + "preemptArmed": true, // 让位信号 + "preemptLevel": true, // 让位信号 + "maxInterruptFrames": true, // 栈上界 + "stackNotify": true, // 栈状态钩子 + } + var bad []string + for i := 0; i < typ.NumField(); i++ { + name := typ.Field(i).Name + if !allowed[name] { + bad = append(bad, name) + } + } + if len(bad) > 0 { + t.Fatalf("中断管理器不应持有这些字段(队列/优先级归调度器): %v", bad) + } + + // 反向确认:队列确实在 scheduler 上(而不是"两边都没有"的假绿)。 + st := reflect.TypeOf(scheduler{}) + for _, want := range []string{"interruptQueues", "immediate"} { + if _, ok := st.FieldByName(want); !ok { + t.Fatalf("中断队列 %s 应在 scheduler 上(排队是调度职责)", want) + } + } +} + // ── I7:中断栈状态钩子必须被接上,且栈变化会通告 ── // 中断管理器与调度器唯一的联系就是这个钩子。它若为空,栈的压入/弹出 diff --git a/internal/agent/core/scheduler.go b/internal/agent/core/scheduler.go index d747b82f..3542c7c8 100644 --- a/internal/agent/core/scheduler.go +++ b/internal/agent/core/scheduler.go @@ -215,9 +215,67 @@ func clampPluginLevel(l Level) Level { return l } +// ── 中断队列操作(归调度器)── + +func (s *scheduler) highestInterruptLocked() (*Task, Level) { + for lv := LevelCritical; lv >= LevelBackground; lv-- { + if q := s.interruptQueues[lv]; len(q) > 0 { + return q[0], lv + } + } + return nil, 0 +} + +func (s *scheduler) popInterruptLocked(lv Level) { + s.interruptQueues[lv] = s.interruptQueues[lv][1:] +} + +func (s *scheduler) interruptCountLocked() int { + n := 0 + for lv := LevelBackground; lv <= LevelCritical; lv++ { + n += len(s.interruptQueues[lv]) + } + if s.immediate != nil { + n++ + } + return n +} + +func (s *scheduler) enqueueInterruptLocked(t *Task) { + s.allocateIDLocked(t) + if s.interruptCountLocked() >= s.maxQueue { + for lv := LevelBackground; lv <= LevelCritical; lv++ { + if len(s.interruptQueues[lv]) > 0 { + s.popInterruptLocked(lv) + s.stats.Rejected++ + break + } + } + } + lv := t.Level + if lv < LevelBackground || lv > LevelCritical { + lv = DefaultLevel + } + s.interruptQueues[lv] = append(s.interruptQueues[lv], t) + s.stats.Enqueued++ +} + +func (s *scheduler) installImmediateLocked(t *Task) *Task { + var demoted *Task + if s.immediate != nil { + // 被顶掉的抢占者降级入队,**不丢**(不变量 I5)。 + demoted = s.immediate + s.enqueueInterruptLocked(demoted) + } + s.allocateIDLocked(t) + s.stats.Enqueued++ + s.immediate = t + return demoted +} + // ── 中断管理器(interrupt.go)── -// 只提供中断队列/中断栈/immediate 槽/让位信号等原语; -// 优先级比较由本文件的 canPreempt/effectiveLevel 提供,中断管理器不自行判断。 +// 只负责**进入中断**:现场压栈/弹栈(中断栈)、中断屏蔽、让位信号原语。 +// 中断队列、优先级比较、该不该打断、该恢复谁——全部在本文件(调度器)。 // setImmediateLocked 判断"该不该由 t 占 immediate 槽",判据是策略,故归调度器。 // @@ -236,8 +294,8 @@ func (s *scheduler) setImmediateLocked(t *Task) bool { // ── 中断登记与恢复决策(调度策略,归调度器)── // -// 这些函数决定"能不能打断、恢复谁"——是**策略**,住在这里。 -// 它们调用的入队/弹栈/压栈是**机制**,住在 interrupt.go。 +// 这些函数决定"能不能打断、排到哪条队列、恢复谁"——全是调度策略。 +// 它们只把"保存现场/恢复现场"这一件事交给 interrupt.go。 func (s *scheduler) registerInterrupt(t *Task) bool { s.mu.Lock() @@ -495,6 +553,20 @@ type scheduler struct { stats SchedulerStats maxQueue int + // ── 中断队列(归调度器)── + // + // 中断管理器只负责"进入中断"(现场压栈)与中断栈;**排队**是调度职责: + // 判断该不该打断、打断不了就先排队、按什么次序再取出来,全是调度策略。 + + // interruptQueues[level]:四条**中断队列**(level 1..4),同级 FIFO。 + // 未能立即抢占的中断(级别不足,或运行任务在临界区)按级别入队。 + interruptQueues [5][]*Task + + // immediate:已判定的最高优先待运行者。抢占必须**立即生效**, + // 所以它不经队列,在下一个安全点直接运行。 + // 这也消除了"抢占者与被抢占者同级"的比较问题——抢占者不需和栈顶比。 + immediate *Task + // interrupt:中断子系统的全部状态(见 interrupt.go)。 // // **内嵌**而非具名字段:内嵌后字段提升,所有 `s.interruptQueues` /