0/6 已展开

LLM 分析

sched_ext:core-sched 任务排序对 scheduler 层级结构的对齐修正

系列概况

  • 标题:[PATCHSET sched_ext/for-7.3] sched_ext: Update core-sched task ordering for scheduler hierarchies
  • 作者:Tejun Heo tj@kernel.org
  • 版本:基于 sched_ext/for-7.3(commit 3167bd3e0c22),未声明 vN,单 PATCHSET 直接投递
  • 规模:4 个 patch,覆盖 4 个文件,+81 / -97 行
  • 修改文件:include/linux/sched/ext.hkernel/sched/ext/ext.ckernel/sched/ext/internal.htools/sched_ext/scx_qmap.bpf.c
  • Message-ID:20260816000527.988170-1..5-tj@kernel.org;落盘前由作者本人在 329d3d9a... 的回复中 Applied 1-4
  • 完整性:完整 4 封 patch + 1 封 maintainer applied 公告,共 6 封

补丁目的

sub-schedulers 引入之后,core-sched 在做 SMT 兄弟核任务排序时仍沿用 sub-scheduler 出现之前的规则。这套规则存在两个互不兼容的缺陷:

  1. ops.core_sched_before()scx_prio_less() 反向调用,导致返回值语义颠倒;
  2. 默认 fallback 用 p->scx.core_sched_at,但这个时间戳在「放入等待」和「dispatch」两种路径下用两套规则维护,比较结果不构成有意义的序。

本系列一次性修掉三处错误并清理一段死代码,目标是在 merge window 打开前把 sub-scheduler 路径的核心坑补齐。

旧流程的问题

  • scx_prio_less() 返回 a 排在 b 之后,但 ops.core_sched_before() 的文档约定是 a 排在 b 之前,scx_prio_less() 把 op 结果原样返回等于在运行时把语义反转;scx_qmap 反向跟着调偏,把两次反转相抵消才恰好工作。
  • core_sched_at 在「等待开始」与「dispatch」两种路径下各用一套刷新规则:同一对比较对象可能一个时间戳来自 dispatch、另一个来自等待路径,根本不在同一时基上秤重。
  • scx_prio_less() 只在 sch_a == sch_b 且 op 存在时调用 op,跨 scheduler 都退化到默认序,根 scheduler 无法对子 scheduler 的任务表达顺序。
  • dequeue_task_scx() 里有 deq_flags & SCX_DEQ_CORE_SCHED_EXEC 测试,但 SCX_DEQ_CORE_SCHED_EXEC 是 bit 32,而 deq_flags 是 int(高 32 位被截掉),且 core-sched execute 路径根本不走 class dequeue,分支永远为假。

新流程

  • 调用 op 时交换参数 (a, b) 改为 (b, a),让两边语义一致;scx_qmap 同步翻转比较方向。
  • 删掉 core_sched_attouch_core_sched*(),fallback 直接复用 p->scx.runnable_at(stall watchdog 已经在维护的等待时间)。在 scx_prio_less() 中显式把 on_cpu 的任务排在所有等待任务之后,把「刚被服务过」这个事实表达出来。
  • 跨 scheduler 的配对,沿着 ancestors[] 找最近的公共祖先;只有这个祖先实现了 core_sched_before() 才调用它的 op,且仅在该祖先对两个 CPU 都不 bypass 时调用。
  • 删除 dequeue_task_scx() 中那段永远为假的 SCX_DEQ_CORE_SCHED_EXEC mask。

Patch 概览

#主题性质
1/4Fix inverted ops.core_sched_before() invocationbugfix
2/4Use runnable_at for the default core-sched task orderingcleanup+bugfix
3/4Make core-sched task ordering hierarchy-awarefeature/enable
4/4Drop the dead SCX_DEQ_CORE_SCHED_EXEC testcleanup

关键实现

/* patch 1: swap op arguments, remove runtime polarity inversion */
return SCX_CALL_OP_2TASKS_RET(sch_a, core_sched_before,
                              task_rq(a),
                              (struct task_struct *)b,
                              (struct task_struct *)a);

/* scx_qmap flips its comparator to match the documentation */
return task_qdist(a) < task_qdist(b);
/* patch 2: use runnable_at instead of core_sched_at;
 * running task is ordered after every waiting task */
bool a_running = a->on_cpu;
bool b_running = b->on_cpu;
if (a_running != b_running)
        return a_running;          /* a on_cpu => a runs after b */
return time_after(a->scx.runnable_at, b->scx.runnable_at);
/* patch 3: walk ancestors[] to pick the nearest common arbiter */
struct scx_sched *sch = NULL;
if (sch_a == sch_b) {
        if (SCX_HAS_OP(sch_a, core_sched_before))
                sch = sch_a;
} else {
        for (level = min(sch_a->level, sch_b->level); level >= 0; level--) {
                struct scx_sched *anc = sch_a->ancestors[level];
                if (anc == sch_b->ancestors[level] &&
                    SCX_HAS_OP(anc, core_sched_before)) {
                        sch = anc;
                        break;
                }
        }
}
if (sch && !scx_bypassing(sch, task_cpu(a)) && !scx_bypassing(sch, task_cpu(b)))
        return SCX_CALL_OP_2TASKS_RET(sch, core_sched_before, ...);
/* patch 4: dead mask test removed */
if (!(deq_flags & DEQUEUE_SLEEP))

类比

想象一座办公楼里有多部电梯:每部电梯(scheduler)自己排乘客顺序,各自的科长只看得到自己楼层的人。SMT 兄弟核就像同一楼层的两部电梯——必须先确定哪部先走、谁先上。

  • patch 1:之前调度员把「先下后上」规则的真假表贴反了,电梯按钮却又恰好按反方向,两错相消才看起来工作;现在把按钮贴正。
  • patch 2:以前每个乘客领一张「开始排队时间」小票,但大楼有两个发票口(等待窗口、发车窗口),两个口子的钟不一样。改成统一用「开始排队时间」一张票(runnable_at),并且规定:已经在电梯里(on_cpu)的人总是最晚被服务。
  • patch 3:以前每部电梯只懂自己楼层的人,跨楼层协调就乱套。现在会议室主管(最近共同祖先)出来当裁判,只有他下达的指令才能跨楼层生效。
  • patch 4:电梯调度代码里有一段「如果乘客从 32 楼来就不让他进」的死测试,32 楼压根没人坐这部电梯,删掉。
+------------------+      +------------------+      +------------------+
|   scx_prio_less  |----->|  core_sched_before|---->|   real order     |
| "a runs after b" |      |     (b, a)       |      |                  |
+------------------+      +------------------+      +------------------+

                 cross-scheduler pair (patch 3)
        +-----------+              +-----------+
        |   sch_a   |              |   sch_b   |
        |  level=2  |              |  level=1  |
        +-----+-----+              +-----+-----+
              |  \                      /
              |   \  walk ancestors[] /
              v    v                  v
        +-----+----+------------------+-----+
        | nearest common ancestor            |
        | that implements core_sched_before |
        +------------------------------------+
                       |
                       v
              op called on the arbiter
        (old) core_sched_at                (new) runnable_at + on_cpu
        +---------------------+            +-----------------------+
        | touch on wait start |            | refreshed on enqueue  |
        | touch on dispatch   |   ====>   | running task ordered  |
        | two rules mixed     |            | after every waiter    |
        +---------------------+            +-----------------------+

Highlight:风险与注意点

  • core_sched_at 字段被整个删掉,include/linux/sched/ext.h 之外若有 out-of-tree 用户依赖该字段,会编译失败;Cc stable 是 v6.12+,意味着回稳分支也需要带上前两个 patch。
  • scx_qmap 的翻转方向是「与其他可能的 out-of-tree 使用者」配对推断的;任何把 BPF scheduler 写成 return task_qdist(a) > task_qdist(b) 的下游,都需要同步翻为 <,否则回到「看起来对、实际反」的状态。
  • runnable_at 只在 enqueue 时刷新,长跑任务会带陈旧戳;当前用 on_cpu 兜底,但若将来出现 running 但不 on_cpu(如被 rq 临时拔出)状态需要再确认。
  • 跨 scheduler 走「最近共同祖先」意味着祖先实现了 op 才会调用;未实现任何 core_sched_before 的根 scheduler 仍退回默认序,行为是「等待更久者优先」,这是显式边界,不是 bug。
  • 第 4 个 patch 的清理依赖 03f5304aad0f 的修补历史;如果哪个回稳分支没有那个 commit,这段「死代码」其实是活代码,cherry-pick 时要小心。

一句话总结

scx_prio_less() 的 op 反向调用与 core_sched_at 两套时间戳规则修掉,让跨 sub-scheduler 的任务序交给「最近共同祖先」仲裁,并顺手清掉一段死测试,在 7.3 merge window 前把 sub-scheduler 的核心坑补齐。