0/26 已展开

LLM 分析

sched:让 proxy execution 与 sched_ext 兼容(v13)

系列概况

  • 标题: sched: Make proxy execution compatible with sched_ext
  • 作者: Andrea Righi arighi@nvidia.com
  • 版本: PATCHSET v13,目标分支 sched_ext/for-7.4
  • 规模: 18 个补丁
  • 修改文件: kernel/sched/core.ckernel/sched/fair.ckernel/sched/sched.hkernel/sched/syscalls.ckernel/sched/ext/{ext.c,ext.h,sub.c,sub.h,internal.h}include/linux/sched/ext.hinit/KconfigDocumentation/scheduler/sched-ext.rsttools/sched_ext/scx_qmap.{bpf.c,c,h}tools/testing/selftests/sched_ext/{Makefile,config,.gitignore,enq_blocked.bpf.c,enq_blocked.c,test_modules/*}
  • 代码统计: 核心代码 +260/-130 左右;新增 ~917 行 kselftest C 代码 + 116 行 BPF 程序 + ~50 行 qmap 改动
  • Message-ID: 20260831134338.1531664-1-arighi@nvidia.com
  • 完整性: 完整(18/18 patch + cover letter + 7 条回复)

补丁目的

解开 CONFIG_SCHED_PROXY_EXEC=yCONFIG_SCHED_CLASS_EXT=y 的 Kconfig 互斥,并新增一个 per-scheduler 开关 SCX_OPS_ENQ_BLOCKED,让 BPF 调度器能够显式接管被 mutex 阻塞、但作为 donor 仍留在 runqueue 上的任务。一次性解决"发行版/运行时特性只能二选一"以及"BPF helper/kfunc 看到的 current 与内核实际执行上下文不一致"两个老问题。

旧流程的问题

  • Kconfig SCHED_PROXY_EXEC depends on !SCHED_CLASS_EXT:两个特性无法同时编译。
  • __schedule()!task_is_blocked(prev) 把所有 blocked task 摘出 rq,proxy-exec 路径走不到。
  • rq->curr 在 proxy 下指 lock owner,而 scx_bpf_task_running/cpu_curr/cid_curr 都把 rq->curr 当成"BPF 选中的任务",kfunc 视角与实际不一致。
  • set_task_cpu() 对 migration-disabled 任务无条件 WARN_ON_ONCE,proxy 把 blocked donor 调度上下文搬到 owner CPU 时会假阳性。
  • sched_can_stop_tick() 仅在 nr_running==1 时检查 CFS bandwidth,被 retained 的 FAIR donor 会漏掉带宽管控。
  • dequeue_task_scxtask_current(rq, p) 判断是否发 stopping,proxy 下"调度上下文切换 ≠ 物理切换",导致 running/stopping 配对错乱。

新流程

  1. core 在 sched_change_begin() 中传入 next_class,新增 sched_proxy_block_task(),所有"调度类/BPF 所有权变更"先全量 dequeue 残留 donor;
  2. __schedule() 改为只在 task_is_blocked(prev) && scx_allow_proxy_exec(prev) 时保留 prev 为 donor;新增 WF_ON_RQ 区分 ttwu_runnable 与全新 wakeup;
  3. rq->donor 成为 BPF 视角的"current",rq->curr 是实际执行上下文;SCX_TASK_RUN_TRACKED 显式跟踪 running session;
  4. 跨 rq DSQ 转移遇到 proxy 状态时,把任务 park 到 source rq 的 reject_dsq,并标记 SCX_TASK_REENQ_PROXY,等 scx_proxy_resolved() 后再让 BPF 重决策;
  5. BPF 调度器可设置 SCX_OPS_ENQ_BLOCKED 接管 donor admission,core 透传 SCX_ENQ_BLOCKEDops.enqueue()
  6. 取消 Kconfig 互斥。

Patch 概览

#类型主题
P01cleanupfind_proxy_task() 用统一 label 在 mutex 解锁后再 resched
P02refactorblock_task()dequeue_block_task(),让 donor 先 dequeue 再 reset
P03bugfixsched_can_stop_tick() 改用 rq->donor 检查带宽,Fixes: af0c8b2bf67b
P04bugfixset_task_cpu() 对 blocked proxy donor 关闭 WARN_ON_ONCE
P05refactorsched_change_begin() 增加 next_class 参数
P06feature新增 sched_proxy_block_task() 辅助函数
P07feature新增 3 个空 hook:scx_allow_proxy_exec/proxy_donor_start/proxy_resolved
P08feature新增 WF_ON_RQ wake flag
P09featurescx_allow_proxy_exec() 默认拒绝 EXT donor;ownership change 时强制 deactivate
P10bugfix引入 SCX_TASK_RUN_TRACKED,修正 running/stopping 配对
P11refactorreject DSQ 从 sub.c 移到 ext.c 主代码
P12refactor通用化 reject DSQ,新增 reenq reason 通道
P13bugfix远端 DSQ 转移与 proxy 竞态:把任务 park 在 source rq
P14refactor把 ext.c 内 rq->curr 引用统一替换为 rq->donor
P15feature新增 SCX_OPS_ENQ_BLOCKEDSCX_ENQ_BLOCKED;BPF 接管 donor admission
P16test新增 kselftest enq_blocked(同 CPU/跨 CPU × 开关)
P17featurescx_qmap 新增 -X 选项启用 proxy-exec
P18config移除 SCHED_PROXY_EXEC depends on !SCHED_CLASS_EXT

关键实现

1. donor / owner 解耦

/* rq 维护两套指针 */
rq->donor  /* BPF 视角的 scheduling context */
rq->curr   /* 实际执行 context (lock owner) */

/* set_next / put_prev 改走 donor */
donor->sched_class->put_prev_task(rq, donor, donor);
donor->sched_class->set_next_task(rq, donor, true);

2. 仅在 BPF 同意时保留 donor

/* __schedule() */
!task_is_blocked(prev) || !scx_allow_proxy_exec(prev))

/* 默认拒绝 EXT */
bool scx_allow_proxy_exec(const struct task_struct *p) {
    if (p->sched_class != &ext_sched_class)
        return true;
    sch = scx_task_sched(p);
    return !sch || (sch->ops.flags & SCX_OPS_ENQ_BLOCKED);
}

3. running session 跟踪

/* 仅当真的换了 donor 才发 stopping */
if (next != p && (p->scx.flags & SCX_TASK_QUEUED) &&
    (p->scx.flags & SCX_TASK_RUN_TRACKED)) {
    SCX_CALL_OP_TASK(sch, stopping, rq, p, true);
    p->scx.flags &= ~SCX_TASK_RUN_TRACKED;
}

/* proxy 迁移后, 在新 CPU 等 scx_proxy_donor_start() 才开 session */
if (donor->sched_class == &ext_sched_class &&
    (donor->scx.flags & SCX_TASK_QUEUED))
    scx_start_task_running(rq, donor);

4. 远端 DSQ 转移遇到 proxy

if (unlikely(task_proxy_unsafe_to_move(p))) {
    p->scx.dsq = NULL;
    scx_proxy_reject_task(sch, src_rq, p, enq_flags | SCX_ENQ_CLEAR_OPSS);
    switch_rq_lock(src_rq, this_rq);
    return false;
}

5. BPF 接管 donor admission

/* core 透传 */
if ((sch->ops.flags & SCX_OPS_ENQ_BLOCKED) && p->is_blocked &&
    !(enq_flags & SCX_ENQ_WAKEUP))
    enq_flags |= SCX_ENQ_BLOCKED;

类比

把 proxy execution 想成餐厅里"等候者把点菜单让给已经被服务的桌":donor 是还在等候区排队、已经决定要点什么菜的顾客,owner 是已经坐下但被厨房卡住的桌。当 owner 卡住时,donor 的"调度上下文(菜单决策权)"临时借给 owner,让 owner 那桌先上菜;但服务员(BPF)始终只认 donor 是下单人,账也算在 donor 头上。只有当 donor 同意(BPF 设置 SCX_OPS_ENQ_BLOCKED)时,这种"借菜单"才被允许;如果 donor 没同意(默认),就老老实实回去排队,owner 也回到正常阻塞。

Highlight:风险与注意点

  • Kfunc 视角错乱风险:所有依赖 rq->curr 的 BPF helper 必须改为 rq->donor,P14 一次性改完,但 BPF 程序侧(用户态)如果自己缓存 current 指针就需要重读;
  • running/stopping 嵌套:忘记 SCX_TASK_RUN_TRACKED 会让 BPF 收到重复的 running/stopping,影响 vtime/slice 统计;
  • tick 带宽检查漏检:P03 之前 proxy-exec 下 nr_running>1 直接跳过带宽检查,长时间运行可能让受限 FAIR donor 抢不到 CPU;
  • 远端 DSQ 转移竞态:必须在拿到 source rq 锁后重新检查 task_proxy_running_or_donating(),否则可能把还在执行的 task 拉走;
  • 保守策略遗留:RT/DL PI 链路上保留 proxy session 的能力被推迟,仍可能存在不必要的 wakeup/queue;
  • scx_qmap 政策激进:默认行为对争用 mutex 的任务非常偏袒,仅适合 demo,生产调度器应基于此自行设计策略。

版本变化

  • v1–v3(2026-05–2026-06):基础拆分,把 proxy 与 sched_ext 改动集中放在 sched/core 与 ext.c 早期版本;
  • v4–v7(2026-07 上旬):开始拆分 sched_proxy_block_task(),并把"调度类切换前 deactivate"显式化;
  • v8–v10(2026-07 中下旬):引入 SCX_TASK_RUN_TRACKED,把 ops.running/stopping 从 task_current 改为 donor-aware;
  • v11–v12(2026-08 中旬):reject DSQ 从 sub-sched 专用改为通用,新增 SCX_TASK_REENQ_PROXY
  • v13(2026-08-31):scx_qmap 从默认开启 proxy 改为 -X opt-in;kselftest 加入同 CPU / 跨 CPU × 开关矩阵。

一句话总结

v13 通过 SCX_OPS_ENQ_BLOCKED 让 BPF 调度器显式接管被 mutex 阻塞的 donor,并把 sched_ext 的"current"视角从物理 rq->curr 改成逻辑 rq->donor,最终在 Kconfig 上解除了 proxy-exec 与 sched_ext 的互斥。

                +----------------------+
                |   BPF scheduler      |
                |  ops.enqueue()       |
                |  SCX_ENQ_BLOCKED?    |
                +----------+-----------+
                           | admit donor
                           v
       +-----------------------------------+
       | sched core: __schedule()         |
       |   task_is_blocked(prev) &&       |
       |   scx_allow_proxy_exec(prev)     |
       +-----------------+-----------------+
                         | yes
                         v
       +-----------------------------------+
       | rq->donor = blocked donor         |
       | rq->curr  = mutex owner           |
       +-----------------+-----------------+
                         |
                         v
       +-----------------------------------+
       | find_proxy_task(): migrate donor  |
       | scheduling context -> owner CPU   |
       +-----------------+-----------------+
                         |
                         v
       +-----------------------------------+
       | SCX_TASK_RUN_TRACKED session     |
       |  - open  : scx_proxy_donor_start  |
       |  - close : donor really changes   |
       +-----------------------------------+

  remote DSQ xfer races with proxy ?
        |
        v
  +----------------------------+
  | task_proxy_unsafe_to_move |
  |   - task_on_cpu(src_rq,p) |
  |   - task_current_donor()  |
  +-------------+--------------+
                | unsafe
                v
  +----------------------------+
  | park on src_rq.reject_dsq |
  | SCX_TASK_REENQ_PROXY      |
  | wait scx_proxy_resolved() |
  +----------------------------+