0/2 已展开

LLM 分析

sched_ext:修复 local DSQ 切锁期间的陈旧 rq 跟踪

系列概况

  • 标题:[PATCH sched_ext/for-7.2-fixes] sched_ext: Preserve rq tracking across local DSQ dispatch
  • 作者:Andrea Righi <arighi@nvidia.com>
  • 版本:未标版本(单 patch)
  • 规模:1 个实际 patch
  • 修改文件:1 个,kernel/sched/ext/ext.c
  • 代码统计:+13/-0
  • Message-ID:20260707135854.1379730-1-arighi@nvidia.com
  • 完整性:本地页面与原始邮件标题一致;b4 得到 2 封邮件、1/1 patch,无 Thread incomplete、缺件或 tip-bot2 污染;但 b4 报 BADSIG: DKIM/nvidia.com,邮件签名真实性未获工具确认

一句话总结:在 dispatch_to_local_dsq() 临时切换 rq 锁时暂停 scx_locked_rq 跟踪,避免嵌套回调恢复一个实际已解锁的 rq 并触发 lockdep。

补丁目的

SCX_CALL_OP() 会把当前持锁 rq 记入每 CPU 的 scx_locked_rq_state,让 BPF kfunc 和嵌套 SCX 回调知道当前受哪个 rq 锁保护。此前为支持嵌套 sub-scheduler,提交 7fb39e4eb4c3 又让内层回调在退出时恢复外层记录。

问题在于,外层 ops.dispatch() 可调用 scx_bpf_dsq_move_to_local(),其后 dispatch_to_local_dsq() 为跨 rq 移动任务会放下原 rq 锁、改持源或目标 rq 锁。真实锁已经变化,外层记录却仍指向原 rq;同步触发的 ops.dequeue() 因而可能在返回时恢复陈旧记录,并被 update_locked_rq() 内的 lockdep_assert_rq_held() 捕获。

补丁只修正锁上下文记账,不改变任务选择、DSQ 顺序、BPF ABI 或调度策略;Cc: stable@vger.kernel.org # 7.1+ 表明作者希望回补受影响的 7.1 及以后版本。

旧流程的问题

旧路径的关键矛盾是“记录的锁”和“实际持有的锁”短暂分离:

  1. SCX_CALL_OP(dispatch, rq) 记录原始 rq
  2. dispatch_to_local_dsq() 解锁该 rq,转而锁住 src_rq/dst_rq
  3. scx_dispatch_enqueue()local_dsq_post_enq() 同步调用 call_task_dequeue()
  4. 内层 SCX_CALL_OP_TASK(dequeue, locked_rq, ...) 保存陈旧的外层记录。
  5. 内层退出并恢复该记录时,原 rq 尚未重新加锁,lockdep 报警。

邮件给出的现场是 kernel/sched/sched.h 中的断言告警,栈从 call_task_dequeue() 回溯至 dispatch_to_local_dsq()scx_flush_dispatch_buf() 和 BPF ops.dispatch()

新流程

SCX_CALL_OP(dispatch, rq)
  │  scx_locked_rq_state = rq
  ▼
dispatch_to_local_dsq()
  ├─ 无需切锁的两个早退路径 ───────────────► 直接返回(记录不变)
  │
  ├─ tracked_rq = scx_locked_rq()
  ├─ WARN_ON_ONCE(tracked_rq != rq)
  ├─ update_locked_rq(NULL)                 暂停外层 rq 记账
  │
  ├─ rq → src_rq/dst_rq 锁切换
  ├─ nested ops.dequeue()
  │    └─ 保存/恢复的是 NULL 或其真实 locked_rq,不再恢复旧 rq
  │
  ├─ 重新获取原始 rq 锁
  └─ update_locked_rq(tracked_rq)           恢复外层上下文

旧:rq 指针的“账面状态”跨越了实际解锁窗口。
新:切锁窗口把账面状态置空,只在重新持有原 rq 后恢复。

Patch 概览

Patch核心改动
1/1dispatch_to_local_dsq() 保存 tracked_rq,切锁前清空跟踪,重新锁回原 rq 后恢复

关键实现

1. 快速路径保持原样

tracked_rq 虽在函数入口读取,但清空动作位于两个早退检查之后:任务已在目标本地 rq,或目标 CPU 不可运行而回退 global DSQ 时,都没有 rq lock dancing,因此无需改动跟踪状态。

2. 清空的是元数据,不是锁

核心新增逻辑等价于:

if (tracked_rq) {
	WARN_ON_ONCE(tracked_rq != rq);
	update_locked_rq(NULL);
}

update_locked_rq(NULL) 只把每 CPU 的 scx_locked_rq_state 置空,并不执行解锁;真正的 raw_spin_rq_unlock()/raw_spin_rq_lock() 顺序仍由原代码控制。这样嵌套回调不会继承已经过期的外层锁声明。

3. 恢复时机受真实锁约束

函数尾部原逻辑先把 locked_rq 切回参数 rq,补丁随后才调用 update_locked_rq(tracked_rq)。由于非空更新会执行 lockdep_assert_rq_held(rq),顺序反过来仍会复现告警。

4. 并发协议没有被绕开

任务仍先以 SCX_OPSS_DISPATCHING 获得独占所有权,再设置 holding_cpu,以 release 语义清除 ops state,然后按既有协议锁定 src_rq、移动任务并返回原 rq。补丁没有改变 ops_stateholding_cpu 或 rq 锁序,只校正嵌套 callback 可见的锁上下文。

当前工作树 v7.2-rc2-22-g0e35b9b6ec0f 尚未包含该提交。原始邮件基于 blob b6a635ba269cc,当前文件是 691d53fe0f648,但上下文仍通过 git apply --check,说明投稿与当前树有基线差异而非补丁缺失。

类比

把 rq 锁看成机房钥匙,把 scx_locked_rq_state 看成门口的值班牌。原实现临时归还 A 机房钥匙、改拿 B 的钥匙时,牌子仍写“持有 A”;嵌套回调照牌子办事便被门禁发现不符。补丁在换钥匙期间先把牌子擦空,真正重新拿回 A 的钥匙后再写回。

Highlight:风险与注意点

  • WARN_ON_ONCE(tracked_rq != rq) 是不变量检查而非恢复策略;若它触发,末尾恢复 tracked_rq 仍可能再次撞上 lockdep,意味着调用方锁上下文本身需要修复。
  • 清空后到函数尾部没有新增早退;未来若在此区间加入 return 或错误路径,必须先恢复原 rq 锁及跟踪,否则会泄漏 NULL 状态。
  • sashiko-bot 指出 scx_dsq_move()in_balance 且跨 remote rq 时也会放下 this_rq,却未暂停同一跟踪,可能存在同类陈旧状态。当前源码可见该切锁窗口,但这只是 AI review 提出的既有风险,本 patch 未处理,也没有人工维护者结论。
  • 每次受影响调用增加一次每 CPU 读取和至多两次写入;相对跨 rq 锁切换与任务移动,预计开销很小,且不改变用户可见接口。
  • 线程只有 sashiko-bot 回复:无 Reviewed-by/Acked-by,无人工回复不能表述为已获认可;也未发现 v2/v3 或最终合入提交。
  • b4 的 NVIDIA DKIM 校验为 BADSIG,影响邮件来源认证判断,不改变 diff 完整性校验结果。

一句话总结

该补丁以“切锁前清空、锁回后恢复”的最小改动,使 scx_locked_rq_state 始终与真实 rq 锁同步,修复 local DSQ 派发中的嵌套回调 lockdep 告警;但 scx_dsq_move() 的相似窗口仍待人工确认和后续处理。