refactor(agent/core): 中断队列归还调度器,中断管理器只留现场保护

用户裁定:「中断队列就该调度器管理啊,中断管理器只负责进入中断的任务」。

上一版我把 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)。
This commit is contained in:
JianFeeeee
2026-10-25 13:37:29 +08:00
parent b829968ac6
commit c5f236a569
4 changed files with 157 additions and 92 deletions

View File

@ -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:中断栈状态钩子必须被接上,且栈变化会通告 ──
// 中断管理器与调度器唯一的联系就是这个钩子。它若为空,栈的压入/弹出