跳到主要内容

检查机制总览

检查是与测试并列的持续保障内核质量的机制。与测试这种事后验证方式不同,检查通常是预先建立规则,以更主动的方式提前防止或发现问题。项目开发中最常见的检查机制是 assert 断言。

从 2026-04-01 以来,项目在检查机制上做了一批增强。主线是参照 Linux lockdep 的思路,在 ArceOS / StarryOS 中建立锁规则检查机制,用于主动发现多核并发条件下的锁违例,弥补事后测试机制的不足。

在实现 lockdep 的过程中,也陆续暴露出一些会影响内核并发可靠性、并阻碍 lockdep 自身落地的问题。因此,已经提前引入了一些静态检查、运行时检查和异常路径防护机制,并分别合并到 dev 分支。

以下先讨论这些相对简单的检查机制,再说明 lockdep 本身的实现情况。

1. might_sleep 原子上下文检查​

might_sleep 用来检查当前代码是否在原子上下文中执行了可能睡眠或重调度的操作。

当前原子上下文主要包括:

  • IRQ 已关闭。
  • 显式 IRQ context。
  • preempt 已禁用。

如果在这类上下文中调用可能阻塞的路径,系统会 panic,并打印调用点、结构化原因、IRQ enabled 状态、显式 IRQ context、preempt 计数、CPU、任务 ID 和任务状态。启用 lockdep 时,还会打印当前 held-lock stack,包括 kind、sleep_forbidden、class、addr 和 acquire 位置。

当前实现还没有完整覆盖所有“不能睡眠”的语义来源。特别是:

  • raw RawSpinLock / RawSpinRwLock 这类 non-sleep lock 不一定改变 IRQ 或 preempt 状态;当前 lockdep build 已能在其他 atomic 条件触发时打印 held-lock stack,但还没有把 held non-sleep lock 本身作为直接触发条件。
  • 用户内存 fault、可能触发 reclaim 的分配、必须原子执行的 hook 入口还缺少独立语义注解。
  • preempt-disable 来源仍需后续阶段补充到诊断中。

典型覆盖路径包括:

  • ax_task::yield_current_cpu
  • ax_task::sleep_until
  • ax_task::exit_current_thread
  • WaitQueue::wait*
  • ax_runtime::task::join_thread
  • ax_runtime::task::block_on
  • ax-sync::Mutex::lock
  • Starry 用户内存访问和 page fault slow path

ax-sync::Mutex::try_lock 不属于覆盖路径。它是单次 CAS,不会阻塞或睡眠,因此保持可在原子上下文中调用,语义接近 Linux mutex_trylock。

主要入口:

  • components/ax-task/src/facade.rs
  • components/ax-task/src/wait_queue.rs
  • components/ax-task/src/runtime.rs
  • os/arceos/modules/axruntime/src/guard.rs
  • os/arceos/modules/axruntime/src/task.rs
  • os/arceos/modules/axsync/src/mutex.rs
  • os/arceos/modules/axhal/src/irq.rs
  • platforms/ax-plat/src/irq.rs
  • os/StarryOS/kernel/src/mm/access.rs

默认 CI 的 Test with std job 会通过 cargo xtask test 运行 ax-task 的 host-test profile,并精确校验 task 初始化、timer 与 might_sleep 测试确实被发现; ax-hal 的 host-test profile 同样校验 IRQ entry 测试集合。IRQ 与多任务调度已是 基础能力,不再维护 no-IRQ 或单任务 host profile。host-test dummy 不代表真实平台; 真实 IRQ context 的 QEMU 回归仍由对应系统测试覆盖。

后续改进方向:

  • 继续补 QEMU 级 IRQ handler 回归,验证显式 IRQ context 路径。
  • 继续实现 held non-sleep lock 的直接判定,特别是 raw RawSpinLock、RawSpinRwLock 和后续项目内 non-sleep rwlock。
  • 继续改进 panic 信息,输出 preempt-disable 来源。
  • 增加 might_fault()、might_alloc()、cant_sleep() / non-block scope 等语义注解,减少跨模块间接阻塞路径的盲区。
  • 明确启动阶段 sleepability,区分早期启动限制和真实运行期 atomic sleep bug。
  • 补充针对性回归,覆盖 IRQ handler、持 non-sleep lock、faultable user copy、阻塞式分配和 try_lock 非阻塞语义。

2. sync-lint 原子内存序静态检查​

sync-lint 是仓库内的静态检查工具,入口命令是:

cargo xtask sync-lint

它检查承担同步语义但仍使用 Ordering::Relaxed 的高风险模式。

当前规则包括:

  • suspicious_relaxed_wait_condition:在等待条件或阻塞循环条件中使用 Relaxed load。
  • suspicious_relaxed_publish_before_notify:Relaxed 写入状态后立刻 notify / wake。
  • suspicious_relaxed_mixed_ordering:同一个同步原子变量混用强序和 Relaxed。

这类检查的目标不是禁止 Relaxed,而是筛出“这个原子变量已经在控制任务/线程/CPU 推进,但内存序仍过弱”的可疑代码。

主要入口:

后续改进方向:

  • 扩展跨函数和跨模块的同步变量识别,减少只在单文件内判断的局限。
  • 增加更多高置信模式,例如 publish 后通过 IPI、signal 或其他调度事件唤醒观察者。
  • 改进忽略注释的审计能力,让长期保留的 sync-lint: ignore 更容易被复查。

3. Task Stack Canary 与 Guard Page​

task stack canary 用来发现任务栈溢出或栈底被破坏。

启用 stack canary 后,任务栈底会写入固定 magic 值。每次任务切换时,调度器检查上一个任务的 canary 是否仍完整;如果 magic 被覆盖,说明栈可能已经越界或被破坏,系统会 panic 并打印任务名、栈范围和期望 magic。

ax-task 的基础多任务实现会启用 stack canary。启用运行时 paging 能力后,axruntime::thread::resources::allocate_runtime_stack() 会为动态任务栈保留一页未映射的 guard page,并在栈向下越界触达该页时触发 page fault 诊断。paging 是运行时 MMU 能力;平台启动层的 somehal/mmu 只负责底层启动实现,不是 axbuild 的公共能力名称。

stack-guard-page 和 stack-protector 已从 ax-runtime、ax-std、ax-libc、starry-kernel、Starry 与 AxVisor 的 feature 图中删除。ArceOS、Starry 和 AxVisor 的软件包清单声明或转发 paging;axbuild 根据最终 Cargo 特性图生成编译器栈保护标志,不改写调用方的 feature 列表。旧名称会在 axbuild 配置校验阶段报迁移错误。

canary 覆盖范围包括:

  • 动态分配的普通任务栈。
  • 主 CPU 的 boot stack。
  • secondary CPU 的 boot/idle stack。
  • 由平台提供的 secondary boot stack。

guard page 当前覆盖范围更窄,只覆盖 TaskStack::alloc() 创建并由 ax-task 拥有生命周期的动态任务栈。它不覆盖 TaskStack::borrowed() 包装的 boot/current 栈,也不覆盖未来可能引入的独立 IRQ stack、exception stack 或 overflow stack。这个边界与动态平台无直接绑定:动态任务栈覆盖,borrowed 栈暂不覆盖。

Linux 的栈保护包含两层不同机制。STACK_END_MAGIC 用于检查任务栈底是否 被覆盖,作用与当前 stack canary 接近;CONFIG_STACKPROTECTOR / CONFIG_STACKPROTECTOR_STRONG 则依赖编译器在函数栈帧中插入 canary, 函数返回前比较保存值和运行时 guard,失败时调用 __stack_chk_fail()。 后者可以发现尚未一路覆盖到任务栈底的函数局部栈溢出,是当前机制尚未覆盖 的方向。

axruntime 在 paging 构建中提供 __stack_chk_guard 和 __stack_chk_fail()。scripts/axbuild/src/build/info.rs::package_mmu_enabled() 通过 package_enables_mmu() 按工作区 Cargo 特性图判断目标软件包是否最终可达 ax-runtime/paging,可达时只追加一次 -Zstack-protector=strong。普通 Cargo 直构不会自行推导该 rustflag,应使用 cargo xtask 或 axbuild 入口。axlibc/paging 只作为 axbuild 的历史输入兼容标记,在生成 Cargo 参数前归一化为目标软件包的 paging 能力,不能直接传给 Cargo。

没有 paging 的构建仍使用普通堆栈,也不会提供编译器栈保护。guard page 的 保护边界只覆盖运行时管理的动态任务栈,不改变 boot stack、独立异常栈或 Starry 用户栈的边界;编译器 stack protector 则保护被插桩函数的栈帧,并依赖 运行时提供的 __stack_chk_* 符号。

当前 nightly 对项目使用的 x86_64-unknown-none、riscv64gc-unknown-none-elf、 aarch64-unknown-none-softfloat、loongarch64-unknown-none-softfloat 四个目标都接受 -Z stack-protector=strong,生成对象也统一依赖 __stack_chk_guard / __stack_chk_fail,因此该方案覆盖四架构共同的最小闭环。第二阶段再评估 Linux 风格 per-task 或 per-cpu guard:x86_64、riscv64、aarch64 可结合各自 percpu / thread pointer / 系统寄存器约定逐步设计;loongarch64 在 Linux 6.12 中也主要体现为全局 __stack_chk_guard 路径,建议放在全局方案稳定后再单独评估。

平台栈边界需要来自平台事实。linker script 中的 boot_stack / boot_stack_top 符号只是兼容占位,并不表示真实栈空间。动态平台 的主 CPU 和 secondary CPU boot stack 都应通过平台提供的 boot_stack_bounds(cpu_id) 获取,否则 stack canary 写入可能落到内核镜像 映射边界之外,在真实板卡上触发 page fault。

当前 primary idle task 栈大小仍保留一个过渡策略:非 lockdep 构建保持 原来的 16 KiB 栈,lockdep 构建使用 TASK_STACK_SIZE,以便为额外的 检查路径保留栈空间。后续可以考虑统一 idle task 栈大小配置。

主要入口:

  • os/arceos/modules/axruntime/src/task.rs
  • components/ax-task/src/system/task_system.rs
  • os/arceos/modules/axruntime/src/mp.rs
  • platforms/axplat-dyn/src/boot.rs

后续改进方向:

  • 在更多边界点触发检查,例如任务退出、panic 前诊断或长时间运行的 idle 路径。
  • 持续完善动态任务栈 guard page 的 SMP shootdown、跨架构 QEMU 回归和 fault 诊断。
  • 持续扩大并验证编译器栈帧级 stack protector 的覆盖,先采用四架构通用的 全局 __stack_chk_guard / __stack_chk_fail 方案,再评估 per-task 或 per-cpu guard。
  • 后续在 axmm 上补 kernel vmap allocator,把 guard page 从额外物理页演进为仅占虚拟地址空间的空洞。
  • 在 vmap-style 栈和 stack metadata 稳定后,再评估 borrowed boot/current 栈、secondary boot 栈以及专用 IRQ/exception/overflow 栈的 guard page 接入。
  • 完善不同架构和不同平台栈布局的文档,明确 canary 写入位置和误报边界。

4. Panic/Oops 递归保护​

panic/oops 递归保护用于提升异常路径健壮性,避免主故障之后在 panic 打印、backtrace 或输出锁路径中继续触发次生故障。

当前机制包括:

  • panic 主路径所有权:只有一个 CPU 执行完整 panic handler。
  • 递归/并发 panic 降级:同 CPU 递归 panic 或其他 CPU 并发 panic 不再进入完整打印和 backtrace 流程,而是本地 halt。
  • oops 状态标记:panic/oops 期间向日志和 backtrace 路径暴露全局状态。
  • panic backtrace one-shot:panic 路径最多尝试一次 backtrace。
  • panic/oops 输出降级:axlog 在 oops 状态下绕过普通 print lock。

它的目标不是隐藏 panic,而是让系统在已经失败时尽量保持输出路径可控,减少 lockdep 违例、page fault、串口卡死等次生问题。

主要入口:

后续改进方向:

  • 为 panic/oops 路径提供更底层的 console fast path,进一步减少对普通日志路径的依赖。
  • 细化 backtrace 策略,例如按平台、构建配置或异常类型选择是否打印完整 backtrace。
  • 将 BUG、die、fatal trap 等更多异常入口纳入统一的 oops 状态管理。

5. Target Backtrace map​

Target 在 QEMU 中直接输出 AXBT map 解析后的函数名和 file:line;测试只检查 target 回溯协议。

主要实现:components/axbacktrace/src/lib.rs、 scripts/axbuild/src/build/symbol_map.rs。

6. lockdep 锁依赖检查​

lockdep 用来检查锁使用是否违反依赖关系,是当前几类机制中最完整的运行时锁检查框架。

它检查的问题包括:

  • 递归加锁。
  • ABBA 锁顺序反转。
  • 乱序解锁。
  • held-lock 栈溢出。
  • spin lock 与 mutex 混合使用时的锁顺序反转。

当前实现把 lockdep 状态机内聚到 ax-sync,并通过 runtime capability 维护当前任务的 held-lock stack。lock class 与 lock instance 仍分别表示锁顺序关系和具体锁实例。

检查流程大致是:

  1. 加锁前生成 held-lock snapshot。
  2. 根据当前请求锁的 class 和已持有锁栈检查递归加锁或顺序反转。
  3. 加锁成功后记录依赖边,并把锁压入当前任务 held-lock 栈。
  4. 解锁时检查释放顺序是否与栈顶一致。
  5. 发现违例时打印 requested lock、conflicting held lock 和 held stack。

接入范围包括:

  • ax-sync spin lock、spin rwlock 和 sleep mutex。
  • POSIX pthread mutex lockdep-aware 布局。
  • ArceOS lockdep QEMU 回归用例。

主要入口:

  • components/lockdep/src/state.rs
  • components/lockdep/src/trace.rs
  • components/kspin/src/lockdep.rs
  • os/arceos/modules/axsync/src/lockdep.rs
  • components/ax-task/src/facade.rs
  • os/arceos/modules/axruntime/src/guard.rs
  • test-suit/arceos/rust/task/lockdep/

后续改进方向:

  • 增加更完整的 lock class 标注能力,支持同一代码位置创建的多类动态锁。
  • 扩展覆盖范围到更多同步原语,例如 rwlock、wait queue、futex 或文件系统内部锁。
  • 改进 CI 策略,保留默认关闭的同时增加按需 lockdep 回归矩阵或夜间检测。
  • 优化违例诊断输出,关联任务、CPU、锁类型和历史依赖路径,提升复杂 ABBA 问题的可读性。

外部 spin 迁移后留下的锁类型、锁范围和原子上下文 follow-up 统一记录在 锁使用问题跟踪。

CI 默认启用边界​

这些机制已进入默认 CI 覆盖范围:sync-lint 作为独立 CI job 运行,panic/oops 递归保护随 runtime 默认编译;might_sleep、task stack canary、IRQ entry 和 timer 由合并后的 host profile 校验测试发现与执行。所有 ArceOS、StarryOS 与 Axvisor 构建都具备多任务和 IRQ 基础能力。

lockdep 由于运行时开销、诊断输出和行为侵入性更强,当前仍不作为 runtime 或完整测试套件的全局默认 feature;默认 CI 仅在专项 host profile 和专门回归用例中显式启用。