0/6 已展开

LLM 分析

sched_ext: Sub-scheduler follow-ups — 技术分析

系列基线信息

字段内容
标题[PATCHSET v2 sched_ext/for-7.3] sched_ext: Sub-scheduler follow-ups
作者Tejun Heo
版本v2(v1 → v2 修复了 circular include 问题)
规模4 patches, 8 files, +240 −142
Message-ID20260715235133.810434-1-tj@kernel.org
来源sched-ext
基线分支sched_ext/for-7.3 (35f9cbbacb67)

明确目的

本系列是 sub-scheduler 能力系列的后续补丁,回应 Andrea Righi 在 v5 review 中提出的两个问题:

  1. 调度器卸载时 ecaps 同步的无界等待scx_discard_ecaps_to_sync()resched_cpu() + msleep() 循环等待 balance_one() 消费 dying sched 的 ecaps 同步节点,但当 CPU 被 fair/RT 高优先级类占满时,ext idle CPU 无法被 pick,等待可能永远不结束——这是一个潜在的 stall/lockup。

  2. CONFIG_EXT_SUB_SCHED=y 但无子调度器时的热路径开销:sub-scheduler 的书簿逻辑(ecaps 同步、reject DSQ、caps 检查等)无条件地嵌入热路径,即使系统只有 root scheduler 也要付出代价。

Patch 1 解决问题 1(将无界等待改为直接从 llist 删除节点);Patches 2–3 是前置重构;Patch 4 用 static key 解决问题 2。


遍历代码

Patch 1/4 — 直接移除 ecaps 同步节点

核心改动scx_offline_ecaps() 不再 poll 等待 balance_one() 消费节点,而是直接从 rq->scx.ecaps_to_sync llist 中摘除 dying sched 的 ecaps_to_sync_node

步骤:

  1. 检查节点是否仍在链表上(llist_on_list())。
  2. 若在,取下整条 llist(llist_del_all()),遍历找到目标节点,init_llist_node() 初始化它(使其脱离链表),其余节点重新拼接回去(llist_add_batch())。
  3. 全程在 rq_lock_irqsave 下进行,保证与 queue_sync_ecaps() 生产端的 dedup 逻辑一致——被摘除的节点读作"on-list",所以生产端不会重复排队。
  4. 摘除后仍需 cpu_relax() 等待,但只等正在执行的 scx_process_sync_ecaps() batch 完成(bounded by 当前 batch,而非未来的 balance 调用)。

Patch 2/4 — 拆分 inlines.h

scx_dispatch_sched() 原来定义在 sub.h 尾部,但 sub.h 需要 cid.h,而 cid.h 又引用 internal.h,把 scx_dispatch_sched() 放在 internal.h 末尾会形成 circular include。解决方案:新建 inlines.h,include internal.hcid.h,将 scx_dispatch_sched() 迁移至此。sub.h 删掉 cid.h include 和整个函数体。

Patch 3/4 — 用 CONFIG_EXT_SUB_SCHED 保护 sub_dispatch_prev

rq->scx.sub_dispatch_prev 是 sub-scheduler dispatch 嵌套时保存 prev 的字段,原来无条件存在于 struct scx_rq。此 patch 将其移入 #ifdef CONFIG_EXT_SUB_SCHED 块,与 ecaps_to_sync 并列,并在 scx_dispatch_sched() 中用同一宏保护赋值/清零操作。

Patch 4/4 — scx_has_subs static key

引入 DEFINE_STATIC_KEY_FALSE(__scx_has_subs)

  • scx_sub_enable_workfn() 中子调度器启用前 static_branch_inc()scx_sched_free_rcu_work() 中子调度器释放后 static_branch_dec()
  • 热路径中多处新增 if (!scx_has_subs()) return/skip 快捷退出:
    • scx_process_sync_ecaps():无子调度器时跳过整个 ecaps 同步。
    • scx_reenq_reject():无子调度器时跳过 reject DSQ 处理。
    • scx_missing_caps() / scx_caps_implied():直接返回 0 / true。
    • scx_local_or_reject_dsq():直接返回 local DSQ。
    • SCX_CALL_OP_TASK 断言:仅在子调度器存在时才检查 sch == scx_task_sched_rcu(task)
  • scx_idle_notify() 特殊处理:无子调度器时走快速路径直接通知 root scheduler,跳过层级遍历。

ASCII 流程图

  Patch 1: ecaps 同步节点卸载(旧 vs 新)
  ┌─────────────────── 旧流程 ───────────────────┐
  │ offline_ecaps()                                 │
  │   ├─ if !scx_enabled() || !cpu_active()         │
  │   │     → discard_queued_syncs()  一次性丢弃    │
  │   ├─ else:                                       │
  │   │     loop: resched_cpu() + msleep(1)          │
  │   │     └─ 等待 balance_one() 自然消费           │
  │   │        ↑ 可能永远不发生 (stall)               │
  └─────────────────────────────────────────────────┘

  ┌─────────────────── 新流程 ───────────────────┐
  │ offline_ecaps()                                │
  │   ├─ llist_on_list(&node)?                     │
  │   │     → llist_del_all()                      │
  │   │     → 遍历: init 目标节点, resplice 其余    │
  │   │     → 全程 rq_lock_irqsave 保护            │
  │   ├─ cpu_relax()                               │
  │   │     → 仅等 in-flight batch 完成 (bounded)  │
  └─────────────────────────────────────────────────┘

  Patch 4: scx_has_subs static key 门槛
  ┌──────────── root-only (常见) ────────────┐
  │ scx_has_subs() == false                  │
  │  ├─ process_sync_ecaps → skip            │
  │  ├─ reenq_reject      → skip            │
  │  ├─ missing_caps      → return 0         │
  │  ├─ idle_notify       → direct to root   │
  │  └─ local_or_reject   → local_dsq        │
  └───────────────────────────────────────────┘
  ┌──────────── 有子调度器 ──────────────────┐
  │ scx_has_subs() == true                   │
  │  → 正常走 sub-sched 层级逻辑             │
  └───────────────────────────────────────────┘

概念类比

想象一个公司(root scheduler)可以开设子公司(sub-scheduler)。

Patch 1:子公司注销时,旧流程是"等子公司所有待审批文件被流程自然消化"——但如果审批部门(balance_one)被其他更紧急的事占满了,这些文件可能永远排不上,子公司就一直卡在注销状态。新流程是:直接从待办筐里抽出属于该子公司的文件扔掉,其他公司的文件放回去。虽然仍需等正在被翻阅的那一沓(in-flight batch)看完,但等待是有上限的。

Patch 4:公司没开子公司时,每次审批都要走过"检查子公司权限""核对子公司拒单"等流程——虽然这些步骤全是空跑,但每天都在浪费。static key 就像一盏灯:子公司存在时灯亮,审批流程走完整版;子公司不存在时灯灭,审批直接走简化版,跳过所有子公司相关的检查。


Highlight 突出问题

  1. llist 摘除的原子性假设:新逻辑假设 llist_del_all() + resplice 在 rq_lock_irqsave 下与 queue_sync_ecaps() 的生产端无竞争。需确认 queue_sync_ecaps() 也在此锁下运行,否则 resplice 后生产端可能插入到被 init_llist_node() 清零的节点之后。

  2. cpu_relax() 等待仍然存在:虽然 bounded,但在极端情况下 in-flight batch 持有 rq lock 时间长(dispatch 嵌套深),cpu_relax() 等待可能持续数毫秒。是否应考虑更精细的唤醒机制?

  3. static key inc/dec 时序static_branch_inc()scx_sub_enable_workfn() 中、ops->priv 发布前执行;static_branch_dec()scx_sched_free_rcu_work() 末尾执行。需确认两个路径之间的 race 条件——如果 enable 和 free 并发,static key 计数是否能保持正确?

  4. CONFIG_EXT_SUB_SCHED=n 时 stub 行为:Patch 4 在 #else 分支中 scx_has_subs() 返回 false,scx_dec_has_subs() 为空函数。需验证所有调用方在 false 分支下的行为与 CONFIG_EXT_SUB_SCHED=y && no sub 完全等价。


版本演进

版本关键改动
v1初始版本:直接移除 ecaps 节点 + static key 门控
v2scx_dispatch_sched()internal.h 末尾迁至新建 inlines.h,修复 internal.h ↔ cid.h circular include(由 sashiko AI 发现);sub.h 删除不再需要的 cid.h include

与其他相关 patch 系列的关联

本系列是 sub-scheduler capability 系列 v5 的后续补丁,专门解决 Andrea Righi review v5 时提出的两个问题。sub-scheduler capability 系列本身引入了子调度器层级、ecaps(effective capabilities)、cid(CPU ID)等核心机制。


一句话总结

修复 sub-scheduler 卸载时的无界 stall,并用 static key 让无子调度器的常见场景跳过热路径开销。