0/7 已展开

LLM 分析

sched_ext:子调度器后续修正与热路径优化

系列基线信息

  • 标题[PATCHSET sched_ext/for-7.3] sched_ext: Sub-scheduler follow-ups
  • 作者:Tejun Heo
  • 目标分支sched_ext/for-7.3
  • 基线提交35f9cbbacb67
  • 版本背景:未标注独立版本号;这是对子调度器能力系列 v5 评审意见的后续系列
  • 规模:4 个 patch,涉及 6 个文件,新增 223 行、删除 141 行
  • 主 Message-ID20260714230917.84158-1-tj@kernel.org
  • 来源频道sched-ext
  • 代码分支git://git.kernel.org/pub/scm/linux/kernel/git/tj/sched_ext.git scx-sub-followups
  • 讨论状态:收到一条自动化审查意见;Andrea Righi 的回复正文在输入中被截断,无法确认其完整结论

明确目的

这个系列处理 Andrea Righi 在评审原子调度器能力系列 v5 时指出的两类问题。

第一类是 scheduler 销毁可能无限等待。旧代码通过 resched_cpu()msleep(1),等待未来一次 balance_one() 消费排队的 effective capabilities(ecaps)同步节点。如果 CPU 没有 ext 任务,并长期被 fair 或 RT 调度类占用,这次 balance 可能一直不发生,导致 teardown 无限停滞。

第二类是 根调度器独占时仍承担子调度器开销。即使内核启用了 CONFIG_EXT_SUB_SCHED=y,但实际没有任何 sub-scheduler,调度热路径仍执行层级遍历、能力判断、同步检查和任务所属调度器验证。本系列通过 static key 在无子调度器时把这些分支近乎完全跳过。

系列还包含两个准备性清理:

  • 将公共 dispatch 实现从 sub.h 移到 internal.h
  • 只在启用子调度器配置时保存 rq->scx.sub_dispatch_prev

遍历代码

Patch 1/4:直接移除 teardown 中排队的 ecaps 同步

旧流程依赖调度器未来再次 balance:

while (true) {
        if (!llist_on_list(&pcpu->ecaps_to_sync_node))
                return;

        resched_cpu(cpu);
        msleep(1);
}

问题在于,这不是由 teardown 自身保证完成的同步,而是在等待另一个调度事件碰巧发生。

新实现利用 rq lock 直接处理 rq->scx.ecaps_to_sync

  1. 先检查目标节点是否仍在链表语义上的 on-list 状态。
  2. 获取目标 CPU 的 rq lock,并关闭相应中断。
  3. llist_del_all() 一次取走全部待同步节点。
  4. 遍历该批节点,丢弃 dying scheduler 对应的节点。
  5. 对目标节点调用 init_llist_node(),明确标记为 off-list。
  6. 将其他节点重新拼接回原来的 llist
  7. 如果节点正被 scx_process_sync_ecaps() 私有持有,则在 rq lock 下通过 cpu_relax() 等待当前批处理结束。

关键并发依据是:被批量取走、但仍在处理中的节点继续表现为 on-list,因此 producer 侧的去重逻辑不会误判并重复入队。真正观察到目标节点在 rq lock 保护下变为 off-list 后,才能确定不会再访问即将释放的 pcpu

这把等待范围从“等未来某次 balance”缩短为“最多等当前已经开始的 batch 完成”。

Patch 2/4:移动公共 dispatch 实现

scx_dispatch_sched() 并非仅供 sub-scheduler 使用。它负责:

  • 消费全局 DSQ;
  • 处理 scheduler bypass;
  • 调用 BPF ops.dispatch()
  • 刷新 dispatch buffer;
  • 检查本地 DSQ;
  • 限制 dispatch 重试次数并触发 watchdog 相关 kick;
  • 在子调度器嵌套 dispatch 时保存和传递 prev

因此,patch 将整个函数从 kernel/sched/ext/sub.h 移到 kernel/sched/ext/internal.h,保持逻辑不变。

函数继续使用 __always_inline,原因是 sub-scheduler dispatch 可以递归嵌套,省去调用帧有助于控制内核栈占用。

不过,这次移动引入了自动化审查指出的头文件依赖问题:internal.h 尾部包含 cid.h,而 cid.h 本身又包含 internal.h。如果某个 C 文件先包含 cid.h,include guard 可能使 internal.h 中的 scx_dispatch_sched()scx_cpu_arg() 声明出现前被解析,从而产生隐式声明编译错误。

Patch 3/4:按配置裁剪 sub_dispatch_prev

rq->scx.sub_dispatch_prev 只服务于嵌套的 sub-scheduler dispatch,却原本无条件存在于 struct scx_rq

本 patch 将字段移入:

#ifdef CONFIG_EXT_SUB_SCHED
        struct llist_head ecaps_to_sync;
        struct task_struct *sub_dispatch_prev;
#endif

scx_dispatch_sched() 中,保存和清除该字段的操作也通过 CONFIG_EXT_SUB_SCHED 条件编译:

#ifdef CONFIG_EXT_SUB_SCHED
        if (!nested)
                rq->scx.sub_dispatch_prev = prev;
#endif

        SCX_CALL_OP(...);

#ifdef CONFIG_EXT_SUB_SCHED
        if (!nested)
                rq->scx.sub_dispatch_prev = NULL;
#endif

这样,在完全未编译子调度器支持的内核中,不再为无用字段和赋值付费。

Patch 4/4:用 static key 关闭无子调度器时的热路径

新增 static key:

DEFINE_STATIC_KEY_FALSE(__scx_has_subs);

static inline bool scx_has_subs(void)
{
        return static_branch_unlikely(&__scx_has_subs);
}

它不是简单布尔值,而是对子调度器生命周期计数:

  • sub-scheduler 启用、且其程序可能开始影响 CPU 前,执行 static_branch_inc()
  • scheduler 完全失活并进入最终 RCU free 阶段后,执行 static_branch_dec()
  • root scheduler 不计入该计数。
  • 未启用 CONFIG_EXT_SUB_SCHED 时,scx_has_subs() 恒为 false

static key 覆盖的主要热路径包括:

  • 无子调度器时直接选择本地 DSQ,而不做能力和 reject DSQ 处理。
  • scx_reenq_reject() 直接返回。
  • scx_process_sync_ecaps() 不检查空的 ecaps 同步链表。
  • scx_missing_caps() 直接返回 0。
  • scx_task_can_stay_on_cpu() 直接允许保留。
  • task callback 中只在确有子调度器时验证任务所属 scheduler。
  • scx_idle_notify() 不再遍历 scheduler 层级,而是直接通知 root。

scx_idle_notify() 需要单独 fast path,因为它原本是一次树形层级遍历。无 sub-scheduler 时,只有 root 可能等待 idle 通知,因此直接复用原遍历中的通知条件即可。

ASCII 流程图

Old teardown:

dying scheduler
       |
       v
ecaps node still queued
       |
       v
resched_cpu() + msleep(1)
       |
       v
wait for future balance_one()
       |
       +---- fair/RT keeps CPU busy ----> unbounded stall


New teardown:

dying scheduler
       |
       v
lock target rq
       |
       v
llist_del_all()
       |
       +---- target node ----> init_llist_node() ----> discard
       |
       +---- other nodes ----> llist_add_batch() ----> queue restored
       |
       v
wait only if an active batch still owns the target node
Root-only hot path:

CONFIG_EXT_SUB_SCHED=y
          |
          v
    scx_has_subs()?
       /       \
     no         yes
     |           |
     v           v
direct root   hierarchy walk
fast path     caps/ecaps/reject bookkeeping

概念类比

Patch 1 类似撤销机场候机队列中的一张登机牌。旧做法是不断广播“请重新检查队列”,然后每隔一毫秒睡一下,期待工作人员未来某次轮询时拿走这张票;如果柜台一直在处理更高优先级航班,撤销操作就可能永远等下去。新做法是在锁住柜台后暂时取出整叠票,直接移除目标票,再把其余票放回去。若工作人员已经拿着这一批票,只需等当前处理结束,不再依赖未来的新一轮巡检。

Patch 4 的 static key 则像商场的扶梯控制开关。没有任何楼上店铺营业时,顾客不必每走一步都检查楼层导向、跨层权限和楼上队列;只有第一家楼上店铺开门时才启用整套分层导流逻辑,最后一家关门并完成清场后再关闭。

Highlight 突出问题

  1. Patch 2 的循环头文件依赖需要解决。 internal.h -> cid.h -> internal.h 的顺序可能导致 scx_cpu_arg() 在使用点尚未声明。应通过调整声明位置、拆出独立头文件或重新安排依赖消除环路,并进行不同 include 顺序的构建验证。

  2. Patch 1 的正确性依赖精细的 llist 状态语义。 从全局链表批量摘下的节点仍必须保持 on-list 表象,否则 producer 可能重复排队同一节点。

  3. resplice 会改变节点顺序。 当前代码通过头插方式重建链表。需要确认 ecaps 同步处理不依赖原始 FIFO 或批内顺序;从讨论给出的设计看,它依赖的是去重与最终同步,而不是顺序,但这是评审时应明确验证的假设。

  4. static key 的生命周期必须严格配对。 static_branch_inc() 位于 sub-scheduler 程序可能影响 CPU 之前,而 dec() 推迟到 scheduler 完全失活后的 RCU free。任何启用失败、回滚或异常释放路径都必须保证计数不会泄漏或提前归零。

  5. root-only idle fast path 需验证通知等价性。 特别要测试 do_notifyroot_renotify、bypass 和 SCX_RQ_SUB_IDLE_RENOTIFY 的组合,确保跳过层级遍历不会漏掉 root 的 update_idle()

  6. Andrea Righi 的回复内容不完整。 输入仅包含其引用的 cover letter,未包含截断后的实际评论,因此不能将该回复解释为 Reviewed-by、Acked-by 或确认问题已解决。

版本演进

本系列本身没有标注 v2、v3 等版本号。它是原 sub-scheduler capability series v5 的后续:

  • 原系列 v5:引入或完善 sub-scheduler capability 支持。
  • 本 follow-up Patch 1:修复 teardown 中依赖 msleep() 轮询、可能无限等待的问题。
  • 本 follow-up Patch 2-3:整理 dispatch 代码归属,并裁剪仅供 sub-scheduler 使用的字段。
  • 本 follow-up Patch 4:根据 Andrea 的性能意见,以 static key 消除 root-only 常见场景的子调度器热路径开销。

目前可见的评审演进是:Sashiko AI 对 Patch 2 提出了低严重度但可能直接影响编译的循环 include 问题。输入没有后续修订版或作者回应。

与相关 patch 系列的关联

直接前置系列是:

  • https://lore.kernel.org/r/20260709225041.1695495-1-tj@kernel.org
  • 内容为 sub-scheduler capability series v5。
  • 本系列不是独立功能,而是针对该系列评审中暴露的 teardown 正确性与 root-only 性能问题进行补强。
  • 所有 patch 基于 sched_ext/for-7.335f9cbbacb67

一句话总结

该系列一方面把 sub-scheduler 销毁从可能无限等待的轮询改为受 rq lock 保护的直接摘链,另一方面用 static key 让没有子调度器的常见系统避开层级调度开销,但 Patch 2 的循环头文件依赖仍需修正或验证。