mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 14:57:38 +00:00
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:
@ -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()` 在中断管理器内完成——
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
// ── 中断登记(抢占或入队)──
|
||||
|
||||
|
||||
@ -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:中断栈状态钩子必须被接上,且栈变化会通告 ──
|
||||
|
||||
// 中断管理器与调度器唯一的联系就是这个钩子。它若为空,栈的压入/弹出
|
||||
|
||||
@ -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` /
|
||||
|
||||
Reference in New Issue
Block a user