sched-ext discussion
[PATCH v3 sched_ext/for-7.2-fixes] sched_ext: Preserve rq tracking across local DSQ dispatch
LLM 分析
sched_ext: 在 local DSQ dispatch 过程中保持 rq 跟踪
系列基线信息
| 字段 | 值 |
|---|---|
| 标题 | [PATCH v3 sched_ext/for-7.2-fixes] sched_ext: Preserve rq tracking across local DSQ dispatch |
| 作者 | Andrea Righi <arighi@nvidia.com> |
| 版本 | v3(v1→v2→v3 三次迭代) |
| 目标分支 | sched_ext/for-7.2-fixes(进入 7.2 候选修复) |
| 规模 | 1 file changed, 20 insertions(+), 16 deletions(-) |
| Message-ID | 20260708084327.1088186-1-arighi@nvidia.com |
| 来源 | sched-ext 邮件列表 |
| Reply | Tejun Heo: Applied to sched_ext/for-7.2-fixes. Thanks. |
| Fixes tag | 7fb39e4eb4c3 ("sched_ext: Save and restore scx_locked_rq across SCX_CALL_OP") |
| Cc stable | stable@vger.kernel.org # 7.1+ |
明确目的
sched_ext 子系统用 scx_locked_rq() 记录"当前真正持有 rq 锁的 runqueue",目的是让嵌套的 SCX_CALL_OP* 回调(如 ops.dispatch() 内调用 scx_bpf_dsq_move_to_local() 时再回调 ops.dequeue())能找到正确的 rq 上下文。
但 dispatch_to_local_dsq()、move_remote_task_to_local_dsq()、scx_dsq_move() 这些路径会在持锁状态下切到另一把 rq 锁(raw_spin_rq_unlock + raw_spin_rq_lock),而 scx_locked_rq() 还指向旧 rq。这会让恢复 update_locked_rq() 时,把"一把已经不再持有的 rq"重新注册为当前持锁的 rq,从而触发 sched.h:1641 的 lockdep 警告。
本 patch 的目标:把 rq 锁的切换和 scx_locked_rq 的迁移绑定起来,保证"持锁的 rq"和"跟踪的 rq"永远一致。
遍历代码
新增 helper:switch_rq_lock()
static void switch_rq_lock(struct rq *from, struct rq *to)
{
bool tracked = scx_locked_rq() == from;
if (tracked)
update_locked_rq(NULL);
raw_spin_rq_unlock(from);
raw_spin_rq_lock(to);
if (tracked)
update_locked_rq(to);
}
它封装了"换锁"操作:先看现在跟踪的 rq 是不是要释放的那一把;如果是,先把跟踪置 `NULL`(避免 lockdep 看到自相矛盾的"我以为我持着 from 但其实没持"状态),再真正解锁、拿新锁;拿到之后,如果之前是在跟踪,就把跟踪更新到新的 `to`。这把 helper 同时承担了 lockdep 一致性 + 实际持锁切换两件事。
### 三个调用点
1. `move_remote_task_to_local_dsq()`:把远程 rq 的任务迁到本地 DSQ 路径上。
2. `dispatch_to_local_dsq()`:两个分支都换成了 helper:
- `locked_rq` → `src_rq`(flush dispatch buf 期间挪任务到本地 DSQ)。
- `locked_rq` → `rq`(当 `locked_rq != rq` 时)。
3. `scx_dsq_move()`:BPF iter 路径的 in-balance / 非 balance 两个分支都换成 `switch_rq_lock()`。
所有原本"裸" `raw_spin_rq_unlock()`+`raw_spin_rq_lock()` 配对被替换为单次 `switch_rq_lock()`,从源头把"换锁"和"换跟踪"绑定成原子操作。
### 触发栈(commit message 中的 lockdep 警告)
scx_dispatch_enqueue
-> dispatch_to_local_dsq
-> scx_flush_dispatch_buf
-> scx_bpf_dsq_move_to_local___v2
-> bpf__sched_ext_ops_dispatch (ops.dispatch 回调)
-> do_pick_task_scx
-> ... 在 dispatch 回调里又调用了 dsq_move_to_local
外层 `ops.dispatch()` 内部通过 `scx_bpf_dsq_move_to_local()` 又走到 `dispatch_to_local_dsq()`,在 `call_task_dequeue()` 之前需要切锁。如果此时不更新 `scx_locked_rq()`,恢复 `update_locked_rq()` 就会写回一把已经不再持有的 rq,lockdep 报 `sched.h:1641`。
### v3 关键改进
v2 的做法是在函数入口清空跟踪、出口恢复。Tejun 指出这种"清空再恢复"的策略脆弱——任何中途路径漏恢复都会再次踩雷。v3 改用 **helper 跟随每次锁切换**,让"跟踪状态紧跟持锁状态"成为机械不变量,等于把不变量推到了最细粒度的锁操作上。
---
## ASCII 流程图
### 1) 问题场景:dispatch 回调内的换锁
ops.dispatch() <-- update_locked_rq(rq_a)
|
v
scx_bpf_dsq_move_to_local()
|
v
dispatch_to_local_dsq()
| raw_spin_rq_unlock(rq_a) <-- 释放 rq_a
| raw_spin_rq_lock(rq_b) <-- 持有 rq_b
| scx_locked_rq() 仍是 rq_a <-- 跟踪已经过时 !!
v
call_task_dequeue()
|
v
update_locked_rq(rq_a) <-- 试图"恢复"一把不持的锁
|
v
WARNING: sched.h:1641 <-- lockdep 警告
### 2) 修复后:helper 锁绑跟踪
switch_rq_lock(from, to)
-----------------------------------
持有 from? --- 是 ---> update_locked_rq(NULL)
(tracked?) \ raw_spin_rq_unlock(from)
否 raw_spin_rq_lock(to)
\ update_locked_rq(to)
\ --- 否 ---> raw_spin_rq_unlock(from)
raw_spin_rq_lock(to)
-----------------------------------
持锁与跟踪永远一致
### 3) 修复前后对比
v1/v2 (clear/restore): v3 (helper):
enter: scx_locked_rq = NULL switch_rq_lock(a, b):
do_unlock(a) + lock(b) clear_track (if was a)
... 任何路径漏恢复就翻车 unlock(a) + lock(b)
exit: restore scx_locked_rq set_track (b)
^ 不变量在每次换锁处
---
## 概念类比
把 `scx_locked_rq()` 想成 **酒店房卡**:你进房间(持锁)前台会刷一张房卡给你;你换房(换锁)时,必须先把旧房卡还回去,再领新房的卡。如果还卡和领卡分开做(先还卡不领新卡,或领新卡前没人知道你换房),要么走廊里短暂无卡(warning),要么前台的记录和实际住客对不上(lockdep 自相矛盾)。`switch_rq_lock()` 就是把"还旧卡 → 换房 → 领新卡"打包成一步前台流程,**让卡片状态和真实住客状态永远同步**。
另一个类比:仓库里只有一辆叉车(rq 锁),多班次共用。前台小黑板(`scx_locked_rq`)上写着"现在谁在用"。换班时如果只交接叉车但不擦黑板,下一班白板还指向老员工,新员工来取车时调度系统发现"白板说 A 在用,但 A 早下班了",于是报警。`switch_rq_lock()` 把"交车 + 擦白板 + 写新名字"做成一个原子动作。
---
## Highlight 突出问题
1. **lockdep 警告的"假阴性"风险**:lockdep 是动态检查,如果某次切锁路径上没真正触发 nested callback,warning 不一定每次都出现;这是一个在特定调度拓扑下才暴露的隐患,CI 不容易稳态复现。
2. **不变量必须下沉到最细粒度**:v2 的"入口清空 + 出口恢复"风格是脆弱的——任何新增路径忘了恢复就翻车。v3 把它绑到锁切换上,是设计上更稳的选择。后续在 `ext.c` 里加新的 rq 锁切换代码时,**必须**使用 `switch_rq_lock()`,否则 bug 会回归。
3. **`Fixes:` 指向的具体 commit**:`7fb39e4eb4c3`("Save and restore scx_locked_rq across SCX_CALL_OP")——这是引入 `scx_locked_rq` 跟踪机制的 commit,也意味着凡是使用了 `scx_locked_rq()` 的下游代码点都要重新审视是否需要改用 `switch_rq_lock()`。
4. **stable 标记 `7.1+`**:本 patch 同时进 stable,说明问题对生产 sched_ext 用户有实际影响(`call_task_dequeue` 是 dequeue 路径,命中频率不低)。
5. **后续观察点**:`scx_dsq_move()` 的 in-balance 分支虽然也换锁,但属于 iterator 路径,访问频率相对低;要留意是否还有其他 iter 内部换锁但未使用 helper 的位置。
---
## 版本演进
| 版本 | 关键改动 |
|---|---|
| v1 | 首次提出修复 dispatch_to_local_dsq + move_remote_task_to_local_dsq 的 rq 跟踪丢失问题 |
| v2 | 据 sashiko AI 反馈,把同样的修复扩展到 `scx_dsq_move()`(BPF iter 路径) |
| v3 | 据 Tejun 反馈,弃用"函数边界 clear/restore"风格,改为 helper 在每次锁切换处原子更新跟踪 |
最终 v3 被 Tejun 接收(`Applied to sched_ext/for-7.2-fixes`),并加上 `Fixes:` 与 `Cc: stable` 标签。
---
## 一句话总结
v3 把 sched_ext 里所有"裸切 rq 锁"的代码点都收敛到 `switch_rq_lock()` 这一个 helper,保证 `scx_locked_rq()` 的跟踪状态与实际持锁状态永远同步,从根本上消除 nested SCX_CALL_OP 路径上的 lockdep 误报。