might_sleep 后续增强计划
本文档记录 might_sleep 原子上下文检查的后续增强计划,用作逐项讨论和拆分实现任务的基础。
当前实现已经覆盖基础睡眠入口:调度让出、定时睡眠、任务退出、WaitQueue::wait*、future::block_on、ax_sync::Mutex::lock、Starry 用户内存访问和 page fault slow path。核心判断位于 os/arceos/modules/axtask/src/api.rs,当前主要把以下状态视为原子上下文:
- IRQ 已关闭。
- 显式 IRQ context。
preempt_count != 0。
这个基础机制已经能发现一批典型错误,但还没有覆盖所有“不能睡眠”的语义来源。后续增强重点不是机械增加更多 might_sleep() 调用点,而是让上下文判断、诊断信息和注解能力更完整。
当前边界
当前机制的主要入口:
os/arceos/modules/axtask/src/api.rsos/arceos/modules/axtask/src/wait_queue.rsos/arceos/modules/axtask/src/future/mod.rsos/arceos/modules/axsync/src/mutex.rsos/arceos/modules/axhal/src/irq.rsplatforms/ax-plat/src/irq.rsos/StarryOS/kernel/src/mm/access.rs
当前需要保留的语义:
ax_sync::Mutex::lock会调用might_sleep()。ax_sync::Mutex::try_lock不调用might_sleep();它是单次 CAS,不会阻塞,语义接近 Linuxmutex_trylock。SpinNoIrq/SpinNoPreempt/SpinRaw/SpinRwLock自身不睡眠,但持有这些锁时进入睡眠路径应被发现。future::sleep()是 async future,本身不直接阻塞;当前由future::block_on()在进入阻塞式 executor 时检查。
计划清单
| 编号 | 优先级 | 项目 | 当前问题 | 讨论状态 |
|---|---|---|---|---|
| MS-1 | P0 | 纳入显式 IRQ context | 已通过 ax_hal::irq::in_irq_context() 纳入 might_sleep() / in_atomic_context() 判定。 | 已完成 |
| MS-2 | P0 | 识别 held non-sleep lock | Phase 1 已在 lockdep build 下输出 held-lock stack;直接把 held non-sleep lock 作为触发条件仍待 Phase 2。 | Phase 1 已完成 |
| MS-3 | P0 | 改进 panic 诊断 | Phase A/B 已输出 caller、reason、IRQ context、preempt count、CPU、task id、task state、held locks;preempt-disable 来源待后续阶段。 | Phase A/B 已完成 |
| MS-4 | P1 | 增加 might_fault() 注解 | 用户内存访问和 page fault slow path 已手动调用 might_sleep(),但缺少表达“这里可能 fault”的独立注解。 | 方向已确认 |
| MS-5 | P1 | 增加 might_alloc() 注解 | 页分配失败后可能进入 page cache reclaim,reclaim 可能调用复杂路径;当前没有统一标注“阻塞式分配/回收风险”。 | 方向已确认 |
| MS-6 | P1 | 增加 cant_sleep() / 原子断言 | IRQ、kprobe、perf、stop_machine 等必须原子执行的入口缺少统一“这里必须不能睡眠”的反向断言。 | 方向已确认 |
| MS-7 | P2 | 明确启动阶段 sleepability | rootfs mount、pseudofs init、tmpfs root_dir 等路径需要区分早期启动限制和真实运行期 atomic sleep bug。 | 方向已确认 |
| MS-8 | P2 | 补充针对性回归 | 现有回归主要验证功能路径,缺少专门触发 might_sleep() 违例的最小用例矩阵。 | 方向已确认 |
| MS-9 | P3 | 文档和审计命令更新 | 现有文档需同步当前源码 语义,并沉淀复查命令。 | 方向已确认 |
MS-1:纳入显式 IRQ context
平台 IRQ 框架已经维护显式 IRQ 上下文:
platforms/ax-plat/src/irq.rs中IN_IRQ_CONTEXT记录当前 CPU 是否正在 IRQ dispatch。dispatch_irq()进入时置位,返回前恢复。IrqOps::in_irq_context()已被 IRQ 注册、释放、同步等路径使用。
建议后续暴露一个 ax_hal::irq::in_irq_context() 或等价接口,并在 axtask::in_atomic_context() 中纳入判断。
实现状态:
- 已在
platforms/ax-plat/src/irq.rs暴露in_irq_context()。 - 已在
os/arceos/modules/axhal/src/irq.rsre-export。 - 已在
os/arceos/modules/axtask/src/api.rs的in_atomic_context()/might_sleep()判定中纳入显式 IRQ context。
已确认方向:
- 在
platforms/ax-plat/src/irq.rs暴露in_irq_context(),返回当前 CPU 的IN_IRQ_CONTEXT状态。 - 在
os/arceos/modules/axhal/src/irq.rsre-export 该接口,保持axtask只依赖ax-hal边界。 - 在
axtask::in_atomic_context()的irqfeature 路径中纳入ax_hal::irq::in_irq_context()。 might_sleep()panic 信息同步打印显式 IRQ context。- 不用
NoPreempt语义替代 IRQ context;NoPreempt只是当前 IRQ handler 外层实现细节。
讨论点:
- 是否需要为非
irqfeature 提供恒为false的 stub,还是只在调用侧cfg(feature = "irq")。 - VM exit 转发 IRQ、IPI handler、timer IRQ 的覆盖范围验证仍需后续 QEMU 回归覆盖。
完成标准:
might_sleep()在显式 IRQ context 中必然触发,即使局部 IRQ 状态未来发生变化。- 新增最小回归覆盖 IRQ handler 内调用睡眠入口的错误路径。
MS-2:识别 held non-sleep lock
当前 lockdep feature 下,task 已经有 held-lock stack,ax-kspin 和 ax-sync 会在加锁/解锁时维护 held lock。这个信息可以用于增强 might_sleep() 诊断和判定。
实现状态:
ax-lockdep::HeldLock已记录 kind、sleep_forbidden、class、addr、acquired_at。ax-kspin的 spin / spin-rwlock 记录为sleep_forbidden=true。ax-sync::Mutex记录为sleep_forbidden=false,避免把 sleepable mutex 本身误标成 non-sleep lock。might_sleep()在lockdepfeature 下 panic 时会打印当前 held-lock snapshot。- 当前完成的是诊断增强;
SpinRaw/SpinRwLock<NoOp>持锁睡眠的直接判定仍留给第二阶段non_sleep_lock_depth或等价状态。
需要重点覆盖的锁:
SpinNoPreemptSpinNoIrqSpinRawSpinRwLockSpinNoIrqRwLock- 后续可能新增的项目内 non-sleep rwlock
建议方向:
- 在 held lock 记录中增加锁 kind 或 sleepability 标记。
- 或在
axtask中维护轻量non_sleep_lock_depth,由 kspin acquire/release 更新。 try_lock失败不应留下 held 状态;try 成功后与普通 lock 一样记录。
已确认方向:
- 第一阶段已完成:增强
lockdep构建下的诊断,复用现有 task held-lock stack,在might_sleep()失败时打印当前 held locks。 - 第一阶段已完成:给 held lock 补充最少语义字段,能区分
spin/mutex/spin-rwlock以及该锁是否sleep_forbidden。 - 第二阶段再增加可选的轻量
non_sleep_lock_depth或等价状态,由ax-kspinacquire/release 通过 crate interface 通知axtask。 - 第二阶段应通过 feature 控制,避免无条件增加所有 spin lock 快路径成本。
- 不把
SpinRwLockread guard 直接机械塞进 lockdep dependency stack 来解决睡眠检查。读写锁依赖检查和“持锁禁止睡眠”是相关但不同的语义,应共享诊断信息而不是强行共用同一个判定模型。
讨论点:
- 第一阶段 held-lock 输出格式如何和
ax-lockdep现有格式复用。 - 第二阶段 feature 名称和默认启用范围。
- raw lock、读锁和 IRQ-only 短锁的误报边界如何控制。
完成标准:
- 持有 non-sleep lock 后调用
might_sleep()能报告问题。 - 报告能指出至少一个持有锁的 acquire 位置。
- 不改变正常锁快路径的默认开销,或开销可通过 feature 控制。
- 已新增 host 单测覆盖持
SpinNoPreempt时might_sleep()输出 held-lock stack。
MS-3:改进 panic 诊断
当前 panic 信息定位成本仍偏高。Linux __might_resched() 会输出调用点、in_atomic、IRQ 状态、non-block 计数、preempt count、held locks、preempt-disable 位置和栈。
本项目建议分阶段补齐:
- 第一阶段:输出
#[track_caller]caller、task name、task state、IRQ context、IRQ enabled、preempt count、CPU id、task id。 - 第二阶段:在
lockdepfeature 下输出 held-lock stack。 - 第三阶段:记录第一次
disable_preempt()的 caller,输出 preempt-disabled 来源。
已确认方向:
- 阶段 A 已完成:输出不依赖额外锁路径的上下文快照,包括 caller、IRQ enabled、显式 IRQ context、preempt count、CPU id、task id、task state。
- 阶段 A 已完成:输出结构化 reason 列表,目前覆盖
irq_disabled、irq_context、preempt_disabled,避免只打印“atomic context”这个总称。 - 阶段 B 已完成:在
lockdepfeature 下打印 held-lock stack,复用ax-lockdep字段,包含 kind、sleepability、class、addr、acquired_at。 - 阶段 C 在 preempt count 从 0 变成 1 时记录 preempt-disable caller,在降回 0 时清除;
might_sleep()因 preempt disabled 触发时输出该位置。 - 阶段 C 如果
#[track_caller]不能完整穿透NoPreempt::new()/NoPreemptIrqSave::new()到axtask::disable_preempt(),先记录 guard 创建点,不伪造更精确的位置。 - 阶段 A 可以与 MS-1 同一 PR 完成;阶段 B 跟随 MS-2 第一阶段;阶段 C 单独拆分。
讨论点:
- 是否使用普通
panic!输出,还是在 oops 状态下走更底层 console fast path。 - held-lock stack 输出格式复用
ax-lockdep还是在axtask层定义轻量格式。 - panic 路径是否需要 rate limit 或 one-shot。
完成标准:
- 单看
might_sleep()panic 日志能判断是哪类原子上下文触发。 - 若由持锁导致,能看到相关锁 acquire 位置。
- 已新增 host 单测覆盖 preempt-disabled 场景下的 reason、caller、preempt count 和 task state。
- 已新增 host 单测覆盖 lockdep held-lock stack 输出。
MS-4:增加 might_fault() 注解
Starry 用户内存访问和 page fault slow path 目前直接调用 might_sleep()。建议增加语义更明确的 might_fault(),内部先复用 might_sleep()。
might_fault() 的含义:
- 它标注“当前位置接下来可能访问 faultable memory,并可能因为处理 page fault 进入会阻塞或重调度的 slow path”。
- 它不是普通调度 API,而是用户内存访问、copy helper、page fault slow path 这类路径的语义注解。
- 第一版实现仍然只复用
might_sleep()的原子上下文检查,不引入完整 Linuxpagefault_disable()/faulthandler_disabled()模型。
might_fault() 的作用:
- 让代码审计时能直接区分“这里可能主动睡眠”和“这里可能因 fault 间接睡眠”。
- 把 faultable user memory access 的检查入口统一起来,后续如果要增加 pagefault-disabled 状态、地址空间锁诊断或用户内存访问策略,可以从这个注解点扩展。
- 保持现有
might_sleep()诊断能力,避免每个用户访问路径重复写低层 atomic-context 检查。
已确认方向:
- 在
ax_task中提供#[track_caller] pub fn might_fault(),第一版内部只调用might_sleep()。 - Starry 用户内存访问入口改用
might_fault(),包括access_user_memory()和 page fault slow path。 might_fault()不替代access_user_memory()对 IRQ enabled、thread context、地址空间锁等 Starry 边界条件的检查。- 第一版不实现
pagefault_disabled()计数,也不把 Starry 特定的 aspace/thread 判断塞回ax_task。
适合接入的路径:
os/StarryOS/kernel/src/mm/access.rs- 用户态 copy helper。
- page fault slow path。
讨论点:
- 后续是否需要引入 pagefault-disabled 状态,以及它应放在 Starry 还是通用 task 层。
- 地址空间锁递归、当前线程不允许 user fault 等 Starry 特定条件应继续返回错误、warn 还是 panic。
完成 标准:
- faultable user memory access 的检查入口语义统一。
- 文档和注释不再把所有 faultable 场景都泛化成普通 sleep。
MS-5:增加 might_alloc() 注解
页分配失败后可能触发 page cache reclaim,例如 axalloc 的 alloc_pages() 会在失败后调用 try_page_reclaim()。这类路径不一定立即睡眠,但可能进入文件系统、page cache、回调和释放路径。
might_alloc() 的含义:
- 它标注“当前位置可能执行阻塞式或 reclaim 型分配,调用点必须处于允许睡眠/重调度的上下文”。
- 第一版目标是覆盖可能进入 reclaim、文件系统、回调或等待路径的分配,不是捕捉所有普通堆分配。
- 它接近 Linux
might_alloc(GFP_KERNEL)的用途,但当前项目还没有统一 GFP/flags 模型,因此第一版不引入复杂参数。
建议先只覆盖“可能进入复杂 reclaim 的页分配”:
os/arceos/modules/axalloc/src/buddy_slab.rsos/arceos/modules/axalloc/src/tlsf_impl.rsos/arceos/modules/axfs-ng/src/file/page.rs
已确认方向:
- 在
ax_task中提供#[track_caller] pub fn might_alloc(),第一版内部只调用might_sleep()。 - 先接入页分配失败后可能触发 reclaim 的路径,以及明确用于 page cache / fs page 的分配入口。
- 不全局 hook Rust allocator 的每次
alloc()。 - 不在普通
Vec/BTreeMap/Arc分配点到处手工添加。 - 不把
try_reserve这类局部 OOM 处理点都标成 blocking allocation,除非该路径会进入 reclaim 或 wait path。 - 后续如果引入
AllocFlags::{Atomic, Blocking}或等价模型,再考虑把might_alloc()扩展为带参数版本。
讨论点:
- 页分配失败后触发 reclaim 的入口应放在第一次分配前、失败后重试前,还是只放在进入 reclaim 前。
- 分配器在早期启动和 IRQ 路径里的合法非阻塞用法如何标注。
- 后续分配 flag 模型是否需要和 axalloc API 一起设计。
完成标准:
- 原子上下文中触发可能 reclaim 的分配能被提前报告。
- IRQ-safe、预分配或明确非阻塞的分配路径不被误伤。
- 文档明确
might_alloc()第一版不是通用内存分配检查。
MS-6:增加 cant_sleep() / 原子断言
might_sleep() 是“这里可能睡眠”的正向注解;还需要“这里必须不能睡眠”的反向断言,用于高风险入口。
需要区分两个概念:
cant_sleep()是断言。它表示“当前位置应该已经处在不能睡眠的上下文”,如果当前仍是普通 sleepable task context,则说明调用边界错误。non_block_start()/non_block_end()是状态。它表示“从现在开始临时禁止阻塞”,后续might_sleep()需要检查 task 上的 non-block 计数。
已确认方向:
- 第一版只实现
cant_sleep(),暂不实现non_block_start()/non_block_end()。 cant_sleep()复用当前 atomic-context 判断;在 MS-1 后,该判断包含显式 IRQ context。cant_sleep()第一版检查当前是否满足以下任一条件:显式 IRQ context、IRQ disabled、preempt disabled,后续可加入non_sleep_lock_depth > 0。- 如果都不满足,
cant_sleep()报告调用点并 panic 或进入项目统一 fatal 路径。 non_block_start()/non_block_end()延后设计,因为它需要 task 字段、嵌套计数和异常退出恢复策略。
候选入口:
- IRQ handler。
- kprobe / retprobe handler。
- perf sampling hook。
- stop_machine callback。
- scheduler switch hook。
讨论点:
cant_sleep()失败时使用 panic、warn,还是现有 oops/fatal 路径。- 第一批接入入口选择:通用 IRQ handler、kprobe、perf、stop_machine、scheduler switch hook 中哪些先做。
non_block_start()/non_block_end()后续是否需要和 task 状态、panic guard 绑定。
完成标准:
- 必须原子执行的入口可以显式表达约束。
- 后续审计时不再只依赖注释说明“这里不能睡眠”。
- 文档明确
cant_sleep()和未来non_block_start()/non_block_end()的区别。