0/10 已展开

LLM 分析

sched_ext: NMI-safe exit handling

系列基线信息

字段
标题[PATCHSET sched_ext/for-7.3] sched_ext: NMI-safe exit handling
作者Tejun Heo
版本v1 → v2(仅 PATCH 1/5 更新)
规模5 个 patch,3 个文件,+156 / -132 行
Message-ID20260725005019.1297049-1-tj@kernel.org
来源sched-ext
目标分支sched_ext/for-7.3

明确目的

sched_ext 的 exit 路径存在 NMI 安全性问题,可能导致整机死锁。

核心问题scx_claim_exit()scx_sched_lock 保护下遍历子调度器层次结构,bstr exit kfuncs 在 raw_spinlock 保护下格式化消息到共享缓冲区。而 "any" 类别的 kfunc 可被 NMI 上下文的 BPF tracing prog 调用——如果它在 NMI 中触发 scx_error(),就会尝试获取已被当前 CPU 持有的锁,造成自死锁。

附带问题:hardlockup handler 在 NMI 中运行,但被迫将 abort 延迟到 irq_work,而 irq_work 无法在检测到 lockup 的 CPU 上运行(该 CPU IRQ 关闭),导致自检测的 lockup 不可恢复。

本系列使 exit 处理端到端 NMI 安全:scx_bpf_error()scx_bpf_exit() 可从任何上下文调用,包括 NMI。

遍历代码

PATCH 1/5: Make exit claiming lock-free

scx_claim_exit() 原来在 scx_sched_lock 下做两件事:

  1. 设置 ->aborting(紧急)——打破 dispatch 路径的 live-lock
  2. 传播 SCX_EXIT_PARENT(不紧急)——让子调度器各自 disable

拆分思路:紧急的做无锁同步,不紧急的延迟到 irq_work。

// 新的 scx_claim_exit() 核心逻辑
if (kind == SCX_EXIT_PARENT) {
    WRITE_ONCE(sch->aborting, true);  // 祖先已在扫,只标记自己
} else {
    // 无锁扫子树:smp_store_mb 保证 aborting 先于 children 读取
    scoped_guard(rcu) {
        scx_for_each_descendant_pre(pos, sch)
            smp_store_mb(pos->aborting, true);
    }
    // 延迟传播 SCX_EXIT_PARENT
    irq_work_queue(&sch->propagate_exit_irq_work);
}

scx_link_sched() 的同步:插入子节点后检查 parent->aborting,通过 smp_mb() 配对——要么 link 看到 aborting 并回退,要么 exit 路径看到新子节点并传播 exit。

新增 sch->linked 布尔字段替代 list_empty(&sch->sibling) 判断,因为 list_del_rcu() 不会清空 sibling。

PATCH 2/5: Format bstr exit messages after claiming the exit

反转顺序:先 claim,再格式化。claim 赢家独占 exit_info 缓冲区,无需共享锁。

// 新的 scx_exit_bstr() — 替代 scx_bstr_format()
bool scx_exit_bstr(sch, kind, exit_code, fmt_blame, fmt, data, data__sz) {
    guard(preempt)();
    if (!scx_claim_exit(sch, kind))   // 先 claim
        return false;
    ret = __bstr_format(fmt_blame, data_buf, ei->msg, ...);  // 直接写入 winner 的 ei->msg
    if (ret < 0)
        scnprintf(ei->msg, ..., "exit message formatting failed (%d)", ret);
    scx_finish_exit(sch, kind, exit_code, raw_smp_processor_id());
    return true;
}

删除了全局共享的 scx_exit_bstr_bufscx_exit_bstr_buf_lockscx_bpf_exit_bstr()scx_bpf_error_bstr() 改为调用 scx_exit_bstr(),RCU 保护替代了原来的 spinlock。

PATCH 3/5: Report NMI kicks with scx_error()

原来 NMI 中调用 scx_bpf_kick_cpu() 只打一次 pr_warn 然后静默丢弃 kick。现在 scx_error() 可从 NMI 调用,直接 abort 调度器,确保正确性问题被确定性地暴露。

PATCH 4/5: Abort directly from the hardlockup handler

scx_hardlockup() 不再延迟到 irq_work,直接从 NMI 调用 handle_lockup()。设置 ->aborting 正是打破 hard-lock CPU dispatch 路径 live-lock 的手段。

关键改进:自检测的 lockup(CPU 自己的 perf watchdog触发)现在可恢复——之前 irq_work 无法在 IRQ 关闭的 CPU 上运行。

修复返回值:原来只要 sched_ext 加载就返回 true(抑制内核报告),现在只在本次调用发起 abort 时才返回 true。

PATCH 5/5: Report scx_link_sched() failures inline

scx_error() 不再持 scx_sched_lock,可在锁内直接调用。每个失败点直接 scx_error() + return,消除了 err_msg/ret 延迟报告模式。

ASCII 流程图

=== 修复前:NMI 中调用 scx_error() 的死锁路径 ===

  NMI ──> scx_bpf_error_bstr()
            │
            ├─ raw_spin_lock(&scx_exit_bstr_buf_lock)  ← 死锁!
            │     (可能被中断的 CPU 持有)
            │
            └─ scx_claim_exit()
                 └─ raw_spin_lock(&scx_sched_lock)     ← 死锁!
                       (可能被中断的 CPU 持有)

=== 修复后:NMI-safe 路径 ===

  NMI ──> scx_bpf_error_bstr()
            │
            ├─ scx_claim_exit()          ← 无锁: atomic_try_cmpxchg
            │     │
            │     ├─ RCU 下 smp_store_mb(->aborting)  ← 无锁扫子树
            │     │
            │     └─ irq_work_queue(propagate_exit)    ← 延迟传播
            │
            └─ __bstr_format() → ei->msg  ← 写入 winner 独占缓冲区
                                        无需共享锁

=== scx_claim_exit() 与 scx_link_sched() 的同步 ===

  Exit 路径 (NMI)              Link 路径
  ──────────────              ──────────
  smp_store_mb(aborting)      list_add_tail_rcu(sibling)
        |                           |
        v                           v
  读 children 列表              smp_mb(); 读 aborting
        |                           |
  (看到新子节点 → 传播)       (看到 aborting → 回退插入)

  配对保证: 一方必能看到另一方的操作

概念类比

想象一栋大楼的紧急疏散系统:

  • 修复前:火灾警报响起时,必须先到物业办公室拿钥匙(scx_sched_lock),然后逐层通知居民撤离。但如果物业办公室正在处理其他事务,钥匙被锁在里面,你只能干等——如果警报恰好发生在物业正在用钥匙的时候,整个大楼就卡死了(NMI 死锁)。另外,广播撤离消息时,所有人共用一个喇叭(共享缓冲区 + spinlock),必须排队用,NMI 中无法排队。

  • 修复后:火灾警报直接拉响每层的紧急铃声(无锁 ->aborting 扫描),所有人立刻知道要撤离。后续的正式通知(SCX_EXIT_PARENT 传播)由专人稍后处理(irq_work)。撤离消息不再共享喇叭,而是每个楼层自己写通知贴到自己的公告栏(exit_info 缓冲区),谁先抢到"起火"声明权,谁就独占自己的公告栏写原因。

Highlight 突出问题

  1. smp_store_mb()smp_mb() 的配对正确性:exit 路径用 smp_store_mb() 写 aborting 再读 children,link 路径先插入再 smp_mb() 读 aborting。这对屏障的配对是整个无锁同步的核心——任何架构上的弱序问题都可能导致 link 路径错过 aborting,造成子调度器逃逸。

  2. stack_trace_save() 的 NMI 安全性不确定:patch 1 选择在 NMI 中跳过 backtrace,因为 stack_trace_save() 的 NMI 安全性是架构相关且未文档化的。未来如果某架构需要 NMI 中的 backtrace,需要单独处理。

  3. trace_sched_ext_exit 的回调延迟:v2 将 tracepoint 移到 ->aborting 设置之后,避免回调阻塞 live-lock 恢复。但回调本身仍可能执行较慢的操作,需确保不影响 NMI 退出路径的时效性。

  4. scx_exit_bstr() 格式化失败不回退:格式化失败时仍保留 exit claim,只是用 fallback 消息替代。提供错误格式的调度器(fmt_blame)会被 abort,但当前调度器的退出不受影响——这是一个有意的设计决策,但需确保不会掩盖 BPF prog 的格式化 bug。

  5. hardlockup handler 返回值语义变更:现在只在本次调用发起 abort 时返回 true,之前只要 sched_ext 加载就返回 true。这会改变内核 hardlockup 报告的抑制行为,需确认上游 hardlockup 框架能正确处理。

版本演进

版本变更
v1 → v2 (PATCH 1/5)trace_sched_ext_exit() 移到 ->aborting 设置之后,避免 tracepoint 回调延迟 live-lock 恢复(Andrea Righi 反馈)

与其他相关 patch 系列的关联

  • PATCH 4/5 的 Fixes 标签指向 bd2d76455b65 ("sched_ext: Defer scx_hardlockup() out of NMI"),即之前将 hardlockup 处理延迟到 irq_work 的那个 commit,本系列撤销了那个延迟。
  • 依赖 sched_ext/for-7.3 基线(3a21e34eb258),基于子调度器层次结构已存在的代码。

一句话总结

通过将 ->aborting 扫描改为无锁 + 延迟传播、将 bstr exit 改为 claim-first 格式化,使 sched_ext 的 exit 处理端到端 NMI 安全,消除了 NMI 上下文调用 scx_error() 的死锁风险,并使 hardlockup 自检测变为可恢复。