0/35 已展开

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=yCONFIG_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 迁移竞争等一组衍生问题。

旧流程的问题

  1. proxy-exec 可能让 BPF 没"派遣"过的 blocked donor 跑在 rq,BPF 看到的 current 与内核视角不一致,DSQ/vtime失同步。
  2. NOHZ tick 检查用 rq->nr_running == 1 + rq->curr,对受限 FAIR donor 会误判可停 tick。
  3. set_task_cpu() 对 migration-disabled 任务无脑 WARN,proxy 搬迁 blocked donor 时必然误报。
  4. proxy_needs_return()blocked_lock 下调 proxy_reset_donor() 后立即 block_task(),形成多余 raw-spinlock 嵌套 + "刚 put_prev 又 dequeue" 瞬态。
  5. sched_change_begin() 收不到 next_class,无法在类切换前解 retained proxy 状态。
  6. find_proxy_task()return proxy_resched_idle(rq) 在 wait_lock/blocked_lock 内求值,导致调度类回调持锁运行。
  7. reject DSQ 被绑死 sub-scheduler cap reject,无法表达"proxy 抢占冲突 reject"。

新流程

  1. 01find_proxy_task()return proxy_resched_idle() 改为 goto resched_idle,统一在锁外调度类回调。
  2. 02block_task() 拆成 dequeue_block_task() + __block_task(),donor 先出队再 reset 引用。
  3. 03 (bugfix) NOHZ CFS bandwidth 改用 rq->donor,用 cfs.h_nr_queued == 1 替代 nr_running == 1__schedule() 检测 donor 变化后刷新 tick dependency(Fixes af0c8b2bf67b)。
  4. 04 (bugfix) set_task_cpu() 对"blocked + migration-disabled + proxy"组合不再 WARN。
  5. 05 sched_change_begin(p, next_class, flags) 增 next_class 参数,all callers 改造。
  6. 06 新增 sched_proxy_block_task(),在持 pi_lock + rq lock 下 reset active donor + block。
  7. 07 定义 scx_allow_proxy_exec() / scx_proxy_donor_start() / scx_proxy_resolved() 桩(SCHED_PROXY_EXEC!SCHED_CLASS_EXT,所以不可达)。
  8. 08 scx_allow_proxy_exec() 默认 p->sched_class != &ext_sched_classsched_change_begin() 在类切换时一律 sched_proxy_block_task()(注释明示 RT/DL PI 兼容留给未来)。
  9. 09 (bugfix) 新增 SCX_TASK_RUN_TRACKEDdequeue_task_scx() / put_prev_task_scx() 用它判断是否触发 ops.stopping()set_next_task_scx() 延后开 session 直到 proxy 解析成功。
  10. 10scx_reenq_reject() 从 sub.c 移到 ext.c(无功能变更)。
  11. 11 reject DSQ 改为无条件存在,reason 直接写入 p->scx.flags,新 scx_finish_reenqueue() 仅在未被再次 reject 时清 reason。
  12. 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. 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. 14 引入 SCX_OPS_ENQ_BLOCKED(要求 ops.enqueue())、SCX_ENQ_BLOCKEDSCX_TASK_ENQ_WAKEUPscx_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. 15 新增 enq_blocked selftest(构造 nice +19 owner、nice -20 donor、每 CPU 一个 nice 0 contender 的优先级反转,同/跨 CPU × 关/开 × 统计 enqueue 与 hold/wait 时间差)。
  16. 16 scx_qmap 加 -X 启用 SCX_OPS_ENQ_BLOCKEDSCX_OPS_ALWAYS_ENQ_IMMED 下遇 SCX_ENQ_BLOCKED|REENQ 落回 SHARED_DSQ;新增 nr_enq_blocked
  17. 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:风险与注意点

  1. wake_cpu vs task_cpu 双轨:跨 CPU 拓扑下 donor 调度上下文搬到 owner rq,但执行上下文仍在 wake_cpu;BPF 若按 wake_cpu 选 placement 会反复抖动,14 patch 在 task_can_run_on_remote_rq() 拦截。
  2. ops.running/stopping 配对:donor 在代理执行期间始终 RUN_TRACKED,但 BPF 若假定 "current == running" 会读错;13 patch 已把 dispatch_core_pick 切到 rq->donor
  3. RT/DL PI 跨类切换被保守 reset:08 patch 一律 sched_proxy_block_task(),放弃 RT 提升 donor 继续代理 owner,注释明示"留给未来"。
  4. 跨 rq 锁切换 REENQ_PROXY 队列:12 patch 把 race 任务暂存 reject_dsq,下轮 drain仍可能再 race,需 BPF 端 SCX_OPS_ALWAYS_ENQ_IMMED + 共享 DSQ 回退(16 patch scx_qmap 实现)。
  5. selftest 覆盖有限:仅在 weighted-vtime BPF 与 scx_qmap(-X) 跑过;scx_bpfland、scx_rusty 等真实 BPF 调度器需各自适配 SCX_OPS_ENQ_BLOCKED
  6. DONOR vs CURR 在 NOHZ tick:03 patch 用 cfs.h_nr_queued 替代 nr_running,但必须保证 rq->donor 确实是受限 FAIR donor,FAIR↔RT/DL 跨类切换 reset proxy 后需重新评估。
  7. tejun 反复追问:08/09/12/14多个 patch 被 review 中反复挑出 race/RT PI/reject reason 问题,02 与 09 各自推出独立 v2。

版本变化(v11 → v12)

  • 02 patch:采纳 tejun 建议,把 reset_donor 的 if 提到外层,让 dequeuedreset_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_EXECSCHED_CLASS_EXT 的构建期互斥。