0/17 已展开

LLM 分析

sched_ext + proxy-exec:把代理执行"打开"给 BPF 调度器

系列概况

  • 标题: [PATCHSET v5 sched_ext/for-7.3] sched: Make proxy execution compatible with sched_ext
  • 作者: Andrea Righi arighi@nvidia.com
  • 版本: v5(基于 John Stultz 早期工作)
  • 规模: 10 个 patch + cover letter,外加 6 封 sashiko-bot 自动化审查回复
  • 修改文件(按 patch 累计去重): kernel/sched/core.c、kernel/sched/sched.h、kernel/sched/syscalls.c、kernel/sched/ext/{ext.c,ext.h,internal.h,sub.c}、include/linux/sched/ext.h、init/Kconfig、tools/sched_ext/{scx_qmap.bpf.c,scx_qmap.c,include/scx/compat.h,include/scx/enum_defs.autogen.h}、tools/testing/selftests/sched_ext/{Makefile,config,.gitignore,enq_blocked.{c,bpf.c},test_modules/*}
  • 代码统计: 约 +260 行内核 + 1100+ 行 selftest + ~30 行 qmap 示例
  • Message-ID: 20260713162112.26785-1-arighi@nvidia.com
  • 完整性: cover letter、10 封 patch 邮件、6 封 AI 审查回复全部覆盖

补丁目的

proxy-exec 让等待 mutex 的"donor"把它的调度上下文借给锁 owner,让 owner 跑临界区而 donor 仍留在 runqueue 上。
当前 CONFIG_SCHED_PROXY_EXEC=y 与 CONFIG_SCHED_CLASS_EXT=y 在 Kconfig 里硬互斥,发行版想"一个内核、运行时挑功能"做不到。
本系列把互斥拆掉,并引入显式 opt-in:BPF 调度器必须设置新标志 SCX_OPS_ENQ_BLOCKED,sched_ext 才把 blocked donor 交给它处理;否则默认走常规 block 路径,行为与 proxy-exec 不兼容时一致。

旧流程的问题

  1. Kconfig SCHED_PROXY_EXEC depends on !SCHED_CLASS_EXT 直接拒绝两者共存。
  2. 即使强行打开,被 mutex 阻塞的 EXT donor 仍以"调度选中任务"的身份出现在 rq->curr 上,但 CPU 实际执行的是 owner。BPF 调度器看到的"current"和 core 真正执行的 task 不一致,DSQ、vtime、callback bookkeeping 全部错位。
  3. ops.running()/ops.stopping() 会因为 proxy donor 被当成"被选中任务"而错误触发,产生不成对的 stopping 事件。
  4. consume_remote_task() 在切换 rq 锁之间存在 TOCTOU 窗口,donor 可能被并发迁移到不允许的 rq,触发 sleeping-while-atomic 与 lockdep 损坏。
  5. rq->curr 在多个 EXT 路径里被当成"被调度选中者"使用,proxy-exec 启用后这条假设不再成立。

新流程

  1. 默认拒绝:新增 scx_allow_proxy_exec(),EXT 任务默认返回 false,__schedule() 走常规 block 路径。
  2. 调度器切换前清理:sched_proxy_block_task() 强制把 retained donor 从 runqueue 上完整出队(含 sched_delayed)。
  3. 回调配对:新增 SCX_TASK_RUN_TRACKED,仅在真正进入 running/stopping 会话时打标,proxy donor 不会再触发 spurious running。
  4. 概念切分:把所有 EXT 路径里的 rq->curr 替换为 rq->donor,把"调度上下文"和"执行上下文"彻底分开。
  5. TOCTOU 修复:抢源 rq 锁后再做一次 task_can_run_on_remote_rq() 检查,失败回退到全局 DSQ,不报 scheduler error。
  6. BPF opt-in:新增 SCX_OPS_ENQ_BLOCKEDSCX_ENQ_BLOCKED,由 BPF 在 enqueue() 里决定怎么排 blocked donor;wakeup_preempt_scx()is_blocked 情况下显式 resched。
  7. Kconfig 解锁:删除 depends on !SCHED_CLASS_EXT,允许两者同时打开。

Patch 概览

#文件作用
01/10core.c, sched.hsched_proxy_block_task() 工具函数
02/10core.c, ext.{c,h}, syscalls.c默认拒绝 + 调度类切换时清理
03/10ext.c, sched/ext.hSCX_TASK_RUN_TRACKED + 回调配对
04/10ext.crq->currrq->donor 替换
05/10ext.cconsume_remote_task() TOCTOU 修复
06/10ext.cblocked/active donor 迁移策略
07/10ext.c, internal.h, sub.cSCX_OPS_ENQ_BLOCKED 委托 BPF
08/10selftests新增 enq_blocked 测试
09/10scx_qmap.bpf.c, scx_qmap.c-B 选项 + 抢占式入队
10/10init/Kconfig解除 SCHED_PROXY_EXEC 互斥依赖

关键实现

(D, O, T) 三元组与 rq->donor

D -----------------> M -------------> O ----------------> T
[donor] blocked on  [mutex] owned by  [owner] preempted by  [task]
 \_________________________________^
   donates scheduling context

rq->donor = BPF 选中、贡献调度上下文与时间片的任务(永远是 D)。
rq->curr = CPU 真正在执行的指令所属任务(可能是 O)。
所有调度/记账操作必须看 rq->donor,否则 BPF 会把 O 误当成自己 dispatch 的任务。

默认拒绝 + 调度切换清理

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 scx_prepare_setscheduler(struct task_struct *p,
                              const struct sched_class *next_class) {
    if (p->sched_class == next_class || next_class != &ext_sched_class)
        return;
    sched_proxy_block_task(task_rq(p), p);
}

回调配对标志

SCX_TASK_RUN_TRACKED = 1 << 6,  /* task entered ops.running()/stopping() session */

/* set_next_task_scx() */
if ((p->scx.flags & SCX_TASK_QUEUED) && !p->is_blocked) {
    if (SCX_HAS_OP(sch, running))
        SCX_CALL_OP_TASK(sch, running, rq, p);
    p->scx.flags |= SCX_TASK_RUN_TRACKED;
}

BPF 委托与 -B 选项

if (enq_blocked)                        /* SCX_OPS_ENQ_BLOCKED && p->is_blocked */
    enq_flags |= SCX_ENQ_BLOCKED;
if (!enq_blocked && !(sch->ops.flags & SCX_OPS_ENQ_EXITING) && ...)
    ...                                   /* 走原有 fallback 链 */

/* scx_qmap_enqueue() */
if (enq_flags & SCX_ENQ_BLOCKED) {
    scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL_ON | scx_bpf_task_cid(p),
                       slice_ns, enq_flags | SCX_ENQ_PREEMPT);
    return;
}

TOCTOU 兜底

if (unlikely(!task_can_run_on_remote_rq(sch, p, this_rq, false))) {
    p->scx.dsq = NULL;
    p->scx.holding_cpu = -1;
    scx_dispatch_enqueue(sch, src_rq,
        find_global_dsq(sch, task_cpu(p)), p,
        enq_flags | SCX_ENQ_CLEAR_OPSS | SCX_ENQ_GDSQ_FALLBACK);
    if (sched_class_above(p->sched_class, src_rq->donor->sched_class))
        resched_curr(src_rq);
    switch_rq_lock(src_rq, this_rq);
    return false;
}

整体状态流

+----------------+   sched_setscheduler/EXT  +---------------------+
| EXT 任务 p     | -----------------------> | scx_prepare_setscheduler|
| (可能是 donor)  |                          +----------+------------+
+----------------+                                     |
                                                         v
                                         +----------------------------+
                                         | sched_proxy_block_task(p) |
                                         | 完整出队 + 处理 sched_delayed|
                                         +----------------------------+
                                                         |
                                       +-----------------+-----------------+
                                       v                                     v
                          +-------------------------+        +----------------------------+
                          | sch->flags & ENQ_BLOCKED?|        | sch->flags & !ENQ_BLOCKED?  |
                          +-------------------------+        +----------------------------+
                                       | yes                                 | no
                                       v                                     v
                       +------------------------------+         donor 走常规 mutex 等待路径
                       | enqueue() 收 SCX_ENQ_BLOCKED |
                       | BPF 自己决定入队哪个 DSQ    |
                       +------------------------------+
                                       |
                                       v
                       +------------------------------+
                       | donor 在 local DSQ 头 + PREEMPT|
                       | 等待 pick_next_task 选中      |
                       +------------------------------+
                                       |
                                       v
                       +------------------------------+
                       | proxy-exec 沿 blocked_on 走  |
                       | 找到 owner,把 O 跑在 D 上下文|
                       +------------------------------+

类比

把 donor 想象成"挂账额度持有者",owner 是"拿着挂账额度去买东西的人"。调度器看的是 donor 的优先级和时间片(挂账额度),但真正下单的是 owner。
sched_ext 像"商场收银系统"——它必须明确知道哪些卡允许挂账(SCX_OPS_ENQ_BLOCKED)、挂账上限多少、记账周期怎么算,否则对账会乱成一团。
如果 BPF 没表态(默认情况),收银系统就把这笔挂账直接拒掉,让 donor 自己走常规支付流程,行为安全、可预测。

Highlight:风险与注意点

  1. opt-in 是必须的SCX_OPS_ENQ_BLOCKED 没设置的 BPF 调度器会失去 proxy-exec 收益;发行版若默认开启 sched_ext 而调度器忘记设标志,会出现性能回退。
  2. rq->currrq->donor 长期可分离:所有写 EXT 路径的人都要警惕"我读的是哪个指针";nohz_full tick 路径目前只能保守地拒绝停 tick,等待后续 core scheduler 把"donor 可分离"推到 NOHZ。
  3. PI de-boost 漏点(sashiko High)scx_root_enable_workfn() 走的是另一条 scx_prepare_task_sched_change() 钩子,patch 02 的 __sched_setscheduler 钩子覆盖不到,要看 patch 06/07 是否补齐——若仍有遗漏,donor 会泄漏进 ext_sched_class。
  4. TOCTOU 与 sleeping-while-atomic:patch 05 修复了 race,但前提是 BPF 调度器不要在 unlocked 窗口里再尝试对同一个 donor 做迁移;stress-ng --pipeherd 0 是当前已知能撞到的负载。
  5. -B 策略激进:scx_qmap 把 blocked donor 放 local DSQ 头并强制 PREEMPT,注释明确说"刻意不公平"——切勿把它当成生产策略照搬。
  6. 测试覆盖:enq_blocked 用 priority inversion + nice +19/-20/0 三档测同 CPU / 跨 CPU 拓扑,mutex hold/wait 平均值只是 informational;断言集中在 nr_blocked_enqueues 计数上,sashiko 指出 same-CPU 分支的 strict-affinity 检查条件可能有 bug。
  7. PREEMPT_RT 仍未解禁:Kconfig 里 depends on !PREEMPT_RT 仍然存在,RT 场景下整套 proxy-exec 仍未启用。

版本变化(v5 → 现在)

  • 把"保留 donor 停用"与"sched_ext 默认拒绝"拆成两个 preparatory patch,让 scheduler-core 部分可单独路由。
  • 移除 John Stultz 的 proxy destination query kfunc 与 mutex lock-scope 预备修改。
  • p->is_blocked 替代 task_is_blocked(),修复 scx_pair 测试里的 WARN(John Stultz)。
  • SCX_TASK_IS_RUNNING 重命名为 SCX_TASK_RUN_TRACKED,注释明确它跟踪的是 ops.running()/stopping() 会话而非物理 rq->curr
  • proxy-migrated blocked donor 留在 owner 的 rq 直到 wakeup,不再让 BPF-directed migration 把它拉回 wake_cpu。

一句话总结

CONFIG_SCHED_PROXY_EXECCONFIG_SCHED_CLASS_EXT 的 Kconfig 互斥解开,让 BPF 调度器通过新增的 SCX_OPS_ENQ_BLOCKED 显式接管 mutex-blocked donor,同时修齐 rq->curr/rq->donor 视图分离带来的 callback 配对、TOCTOU 与迁移策略问题。