sched-ext discussion
[PATCHSET v12 sched_ext/for-7.3] sched: Make proxy execution compatible with sched_ext
LLM 分析
Sched: 让 proxy execution 与 sched_ext 共存(v12, 17 patches)
系列概况
- 标题: [PATCHSET v12 sched_ext/for-7.3] sched: Make proxy execution compatible with sched_ext
- 作者: Andrea Righi arighi@nvidia.com(基于 John Stultz 早期工作)
- 版本: v12(for-7.3),发布 2026-08-16,前序 v1–v11 共 12 个 in-reply-to URL
- 规模: cover + 17 patch + 1 kselftest(enq_blocked)+ scx_qmap 用户态改动 + 2 个独立 v2 修复
- 修改文件: kernel/sched/{core.c,fair.c,sched.h,syscalls.c}, kernel/sched/ext/{ext.c,ext.h,internal.h,sub.c,sub.h}, include/linux/sched/ext.h, init/Kconfig, Documentation/scheduler/sched-ext.rst, tools/sched_ext/scx_qmap.{bpf.c,c,h}, tools/testing/selftests/sched_ext/{enq_blocked.bpf.c,enq_blocked.c,Makefile,config,.gitignore}, 新建 test_modules/{scx_enq_blocked_test.c,Makefile}
- 代码统计: 主体约 +300 / −120;reject DSQ +49/−47;scx_qmap +50/−4;selftest +900 行
- Message-ID:
20260816173732.17162-1-arighi@nvidia.com - 完整性: 35 封(cover + 17 patch + 14 回复 + 2 独立 v2)
补丁目的
CONFIG_SCHED_PROXY_EXEC=y 与 CONFIG_SCHED_CLASS_EXT=y 当前在 Kconfig 层互斥(17 patch 才放开),发行版与终端用户很难用一个 kernel 同时启用。v12 把二者统一为"调度上下文 vs 执行上下文"模型,让 BPF 调度器可选接管 mutex donor 的接纳:新增 SCX_OPS_ENQ_BLOCKED / SCX_ENQ_BLOCKED,并修复 NOHZ tick、migration warning、ops.running 配对、跨 rq DSQ 迁移竞争等一组衍生问题。
旧流程的问题
- proxy-exec 可能让 BPF 没"派遣"过的 blocked donor 跑在 rq,BPF 看到的
current与内核视角不一致,DSQ/vtime失同步。 - NOHZ tick 检查用
rq->nr_running == 1+rq->curr,对受限 FAIR donor 会误判可停 tick。 set_task_cpu()对 migration-disabled 任务无脑 WARN,proxy 搬迁 blocked donor 时必然误报。proxy_needs_return()在blocked_lock下调proxy_reset_donor()后立即block_task(),形成多余 raw-spinlock 嵌套 + "刚 put_prev 又 dequeue" 瞬态。sched_change_begin()收不到 next_class,无法在类切换前解 retained proxy 状态。find_proxy_task()的return proxy_resched_idle(rq)在 wait_lock/blocked_lock 内求值,导致调度类回调持锁运行。- reject DSQ 被绑死 sub-scheduler cap reject,无法表达"proxy 抢占冲突 reject"。
新流程
- 01 把
find_proxy_task()的return proxy_resched_idle()改为goto resched_idle,统一在锁外调度类回调。 - 02 把
block_task()拆成dequeue_block_task()+__block_task(),donor 先出队再 reset 引用。 - 03 (bugfix) NOHZ CFS bandwidth 改用
rq->donor,用cfs.h_nr_queued == 1替代nr_running == 1;__schedule()检测 donor 变化后刷新 tick dependency(Fixesaf0c8b2bf67b)。 - 04 (bugfix)
set_task_cpu()对"blocked + migration-disabled + proxy"组合不再 WARN。 - 05
sched_change_begin(p, next_class, flags)增 next_class 参数,all callers 改造。 - 06 新增
sched_proxy_block_task(),在持pi_lock+ rq lock 下 reset active donor + block。 - 07 定义
scx_allow_proxy_exec()/scx_proxy_donor_start()/scx_proxy_resolved()桩(SCHED_PROXY_EXEC仍!SCHED_CLASS_EXT,所以不可达)。 - 08
scx_allow_proxy_exec()默认p->sched_class != &ext_sched_class;sched_change_begin()在类切换时一律sched_proxy_block_task()(注释明示 RT/DL PI 兼容留给未来)。 - 09 (bugfix) 新增
SCX_TASK_RUN_TRACKED,dequeue_task_scx()/put_prev_task_scx()用它判断是否触发ops.stopping();set_next_task_scx()延后开 session 直到 proxy 解析成功。 - 10 把
scx_reenq_reject()从 sub.c 移到 ext.c(无功能变更)。 - 11 reject DSQ 改为无条件存在,reason 直接写入
p->scx.flags,新scx_finish_reenqueue()仅在未被再次 reject 时清 reason。 - 12 (bugfix) 在
move_remote_task_to_local_dsq/move_task_between_dsqs/dispatch_to_local_dsq路径识别 proxy 还活着被搬动,新增task_proxy_move_active()/task_move_proxy_raced()/scx_reject_task(),写入SCX_TASK_REENQ_PROXY,drain 时用SCX_RQ_PROXY_REENQ推迟到on_cpu清掉后再让 BPF 重选。 - 13 把 ext 内几乎所有
rq->curr的统计/记账/调度判定切到rq->donor(rq_is_open、apply_slice_vtime、rq_owned_post_enq、enqueue/dequeue、dispatch_to_local_dsq、set_next_task_scx、dispatch_core_pick、task_tick_scx、reenq_local、run_deferred、scx_dump_cpu、kick_one_cpu、scx_bypass);tick 抑制显式化为can_stop_tick = slice==INF && !is_blocked;Documentation 同步。 - 14 引入
SCX_OPS_ENQ_BLOCKED(要求ops.enqueue())、SCX_ENQ_BLOCKED、SCX_TASK_ENQ_WAKEUP、scx_prepare_task_sched_change();put_prev_task_scx()在同 donor 时跳过重复ops.enqueue();task_can_run_on_remote_rq()拦截is_blocked && task_cpu != wake_cpu的 donor。 - 15 新增 enq_blocked selftest(构造 nice +19 owner、nice -20 donor、每 CPU 一个 nice 0 contender 的优先级反转,同/跨 CPU × 关/开 × 统计 enqueue 与 hold/wait 时间差)。
- 16 scx_qmap 加
-X启用SCX_OPS_ENQ_BLOCKED;SCX_OPS_ALWAYS_ENQ_IMMED下遇SCX_ENQ_BLOCKED|REENQ落回 SHARED_DSQ;新增nr_enq_blocked。 - 17 去掉
SCHED_PROXY_EXEC对!SCHED_CLASS_EXT的依赖。
关键实现
bool scx_allow_proxy_exec(const struct task_struct *p)
{
struct scx_sched *sch;
if (p->sched_class != &ext_sched_class)
return true;
sch = scx_task_sched(p);
return !sch || (sch->ops.flags & SCX_OPS_ENQ_BLOCKED);
}
void sched_proxy_block_task(struct rq *rq, struct task_struct *p)
{
if (!p->is_blocked || !task_on_rq_queued(p))
return;
if (task_current_donor(rq, p))
proxy_reset_donor(rq);
if (!p->se.sched_delayed)
block_task(rq, p, READ_ONCE(p->__state));
}
类比
把 BPF scheduler 比作餐厅经理:安排桌位(调度上下文)、决定 VIP 客人优先级。proxy execution 让 VIP(donor)因同桌(mutex owner)还在点菜而"借"出优先级,经理只看到 VIP,VIP 的账单(slice/vtime)继续累积,最终归 VIP。SCX_OPS_ENQ_BLOCKED 是 VIP 告诉经理"借出去时请接手重新安排";reject DSQ 是候补名单,SCX_TASK_REENQ_PROXY 备注"因代理冲突暂缓",下轮 drain 再问经理一次。
流程图
mutex donor (no proxy-exec)
---------------------------
dequeue -> off rq -> sleep
proxy-exec + SCX_OPS_ENQ_BLOCKED
--------------------------------
donor blocks -> scx_allow_proxy_exec()==true
-> scx_do_enqueue_task(SCX_ENQ_BLOCKED)
-> BPF enqueue(): dsq_insert + PREEMPT
__schedule() picks donor
-> find_proxy_task() walks owner chain
-> rq->donor=donor, rq->curr=owner
-> scx_proxy_donor_start() -> ops.running(donor)
-> owner code runs, NO scx callback
remote DSQ race ?
-> task_proxy_move_active()==true
-> scx_reject_task() to reject_dsq (REENQ_PROXY)
-> deferred drain -> BPF chooses again
+-----------+ SCX_ENQ_BLOCKED +-------------+
| mutex | --------------->| BPF ops. |
| donor | | enqueue() |
+-----------+ +-------------+
| wakeup | dsq_insert+PREEMPT
v v
+-----------+ proxy handoff +---------------------+
| ttwu_ | ------------->| rq->donor=donor |
| runnable | | rq->curr =owner |
+-----------+ +---------------------+
| race ?
v
+---------------------------+
| scx_reject_task() |
| reject_dsq+REENQ_PROXY |
+---------------------------+
Highlight:风险与注意点
- wake_cpu vs task_cpu 双轨:跨 CPU 拓扑下 donor 调度上下文搬到 owner rq,但执行上下文仍在
wake_cpu;BPF 若按wake_cpu选 placement 会反复抖动,14 patch 在task_can_run_on_remote_rq()拦截。 - ops.running/stopping 配对:donor 在代理执行期间始终
RUN_TRACKED,但 BPF 若假定 "current == running" 会读错;13 patch 已把dispatch_core_pick切到rq->donor。 - RT/DL PI 跨类切换被保守 reset:08 patch 一律
sched_proxy_block_task(),放弃 RT 提升 donor 继续代理 owner,注释明示"留给未来"。 - 跨 rq 锁切换 REENQ_PROXY 队列:12 patch 把 race 任务暂存 reject_dsq,下轮 drain仍可能再 race,需 BPF 端
SCX_OPS_ALWAYS_ENQ_IMMED+ 共享 DSQ 回退(16 patch scx_qmap 实现)。 - selftest 覆盖有限:仅在 weighted-vtime BPF 与 scx_qmap(-X) 跑过;scx_bpfland、scx_rusty 等真实 BPF 调度器需各自适配
SCX_OPS_ENQ_BLOCKED。 - DONOR vs CURR 在 NOHZ tick:03 patch 用
cfs.h_nr_queued替代nr_running,但必须保证rq->donor确实是受限 FAIR donor,FAIR↔RT/DL 跨类切换 reset proxy 后需重新评估。 - tejun 反复追问:08/09/12/14多个 patch 被 review 中反复挑出 race/RT PI/reject reason 问题,02 与 09 各自推出独立 v2。
版本变化(v11 → v12)
- 02 patch:采纳 tejun 建议,把
reset_donor的 if 提到外层,让dequeued与reset_donor互不耦合;独立[PATCH v2 02/17]已发(20260816215611)。 - 09 patch:独立
[PATCH v2 09/17](20260816222920)调整scx_start_task_running/set_next_task_scx判断顺序。 - 13 patch:ext 内
rq->curr全部记账显式切到rq->donor,dump 新增donor=行。 - 14 patch:新增
SCX_TASK_ENQ_WAKEUP,区分 wakeup_preempt 路径"已 enqueue 过的 wakeup"。 - 10/11 patch:reject_dsq 从
CONFIG_EXT_SUB_SCHED限定搬到通用ext.c,支持任意REENQ_REASON。
一句话总结
v12 把 proxy-exec 的 donor/owner 双上下文模型与 sched_ext 完整对齐,让 BPF 通过 SCX_OPS_ENQ_BLOCKED 显式接管 donor 接纳,并修复 NOHZ tick、migration warning、ops.running 配对、跨 rq 迁移竞争等正确性问题,最终解除 SCHED_PROXY_EXEC 与 SCHED_CLASS_EXT 的构建期互斥。