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

@ -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` /