sched-ext discussion
[PATCHSET v2 sched_ext/for-7.3] sched_ext: Sub-scheduler follow-ups
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-ID | 20260715235133.810434-1-tj@kernel.org |
| 来源 | sched-ext |
| 基线分支 | sched_ext/for-7.3 (35f9cbbacb67) |
明确目的
本系列是 sub-scheduler 能力系列的后续补丁,回应 Andrea Righi 在 v5 review 中提出的两个问题:
-
调度器卸载时 ecaps 同步的无界等待:
scx_discard_ecaps_to_sync()用resched_cpu() + msleep()循环等待balance_one()消费 dying sched 的 ecaps 同步节点,但当 CPU 被 fair/RT 高优先级类占满时,ext idle CPU 无法被 pick,等待可能永远不结束——这是一个潜在的 stall/lockup。 -
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。
步骤:
- 检查节点是否仍在链表上(
llist_on_list())。 - 若在,取下整条 llist(
llist_del_all()),遍历找到目标节点,init_llist_node()初始化它(使其脱离链表),其余节点重新拼接回去(llist_add_batch())。 - 全程在
rq_lock_irqsave下进行,保证与queue_sync_ecaps()生产端的 dedup 逻辑一致——被摘除的节点读作"on-list",所以生产端不会重复排队。 - 摘除后仍需
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.h 和 cid.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 突出问题
-
llist 摘除的原子性假设:新逻辑假设
llist_del_all()+ resplice 在rq_lock_irqsave下与queue_sync_ecaps()的生产端无竞争。需确认queue_sync_ecaps()也在此锁下运行,否则 resplice 后生产端可能插入到被init_llist_node()清零的节点之后。 -
cpu_relax() 等待仍然存在:虽然 bounded,但在极端情况下 in-flight batch 持有 rq lock 时间长(dispatch 嵌套深),
cpu_relax()等待可能持续数毫秒。是否应考虑更精细的唤醒机制? -
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 计数是否能保持正确? -
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 门控 |
| v2 | 将 scx_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 让无子调度器的常见场景跳过热路径开销。