sched-ext discussion
[PATCHSET sched_ext/for-7.3] sched_ext: NMI-safe exit handling
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-ID | 20260725005019.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 下做两件事:
- 设置
->aborting(紧急)——打破 dispatch 路径的 live-lock - 传播
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_buf 和 scx_exit_bstr_buf_lock。scx_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 突出问题
-
smp_store_mb()与smp_mb()的配对正确性:exit 路径用smp_store_mb()写 aborting 再读 children,link 路径先插入再smp_mb()读 aborting。这对屏障的配对是整个无锁同步的核心——任何架构上的弱序问题都可能导致 link 路径错过 aborting,造成子调度器逃逸。 -
stack_trace_save()的 NMI 安全性不确定:patch 1 选择在 NMI 中跳过 backtrace,因为stack_trace_save()的 NMI 安全性是架构相关且未文档化的。未来如果某架构需要 NMI 中的 backtrace,需要单独处理。 -
trace_sched_ext_exit的回调延迟:v2 将 tracepoint 移到->aborting设置之后,避免回调阻塞 live-lock 恢复。但回调本身仍可能执行较慢的操作,需确保不影响 NMI 退出路径的时效性。 -
scx_exit_bstr()格式化失败不回退:格式化失败时仍保留 exit claim,只是用 fallback 消息替代。提供错误格式的调度器(fmt_blame)会被 abort,但当前调度器的退出不受影响——这是一个有意的设计决策,但需确保不会掩盖 BPF prog 的格式化 bug。 -
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 自检测变为可恢复。