0/9 已展开

LLM 分析

sched_ext: 子调度器与 cgroup 修复系列分析

系列基线信息

项目内容
标题sched_ext: Assorted sub-scheduler and cgroup fixes
作者Tejun Heo
版本未标注版本号,单次提交
规模4 patches,2 files,+43 / −14
Message-ID20260716213058.1739522-1-tj@kernel.org
目标分支sched_ext/for-7.2-fixes
来源sched-ext 频道
状态已合入(Tejun 确认 applied 1-4)

明确目的

本系列修复 sched_ext 子调度器(sub-scheduler)和 cgroup 交互中的四个 bug:

  • 0001ops.init_task() 在非 enable 路径设置 disallow 会走一条仅适用于 root enable 的"静默回退策略"逻辑,导致 live task 的策略被悄悄改写。修复方式:直接 fail 整个 scheduler,与 fork 和 non-root 分支一致。
  • 0002scx_cgroup_lock() 先拿 rwsem 再拿 cgroup_lock() 会与 cgroup teardown 及 cpu.weight 写入形成三方死锁。此 bug 自 v6.18 起存在于已发布内核。修复:调整锁顺序,先拿 cgroup_lock()
  • 0003:enable 失败于 scx_link_sched() 之前的子调度器从未挂入层级树,但其 teardown 仍做完整 task walk,与 root disable 存在竞态,导致 use-after-free。修复:检测 ->sibling 为空则跳过 cgroup/task 操作。
  • 0004:子调度器的 enable/disable 循环无条件调用 scx_enable_task(),使得 SCX_OPS_SWITCH_PARTIAL 下的非 ext class 任务变成 ENABLED 但仍在 fair class,破坏状态机。修复:加 p->sched_class == &ext_sched_class 门控。

遍历代码

Patch 1 — 拒绝非 enable 路径的 disallow 设置

__scx_init_task() 中原有逻辑:

if (unlikely(fork)) {
    scx_error(..., "during fork");
} else {
    /* load-path revert: silently rewrite policy */
}

问题在于 else 分支假设这是 root enable 的加载路径(回退策略后由切换循环立即读到),但子调度器 disable 路径也会走到此处,此时 task 会被 enable 到 root 并以 ext class 运行,策略却被静默篡改。

修复在 else 分支前插入:

} else if (unlikely(scx_enable_state() != SCX_ENABLING)) {
    scx_error(sch, "ops.init_task() set task->scx.disallow ... outside the enable path",
              p->comm, p->pid);
}

同时更新了 include/linux/sched/ext.hdisallow 字段文档:原文将 !fork 等同于 load path,并引用了过时的 debugfs 路径;现修正为指出任何非 enable 路径的设置都会 fail scheduler。

Patch 2 — 先拿 cgroup_lock 再拿 rwsem

scx_cgroup_lock() 顺序:

percpu_down_write(&scx_cgroup_ops_rwsem);  // 先拿 rwsem
cgroup_lock();                               // 再拿 cgroup_mutex

修复后:

cgroup_lock();                               // 先拿 cgroup_mutex
percpu_down_write(&scx_cgroup_ops_rwsem);  // 再拿 rwsem

解锁顺序同步反转。关键约束:set_* 路径在 rwsem 读侧内不拿任何 cgroup lock,因此 pending writer 只等读段完成(读段总是跑到完),rwsem → cgroup_mutex 的反向依赖消失。

Patch 3 — 跳过未链接子调度器的 teardown

scx_sub_disable() 开头新增:

if (list_empty(&sch->sibling))
    goto dump;

未链接的子调度器(enable 在 scx_link_sched() 前失败)从未拥有 cgroup/task,drain_descendants() 也看不到它。若仍做 task walk,root disable 可在 drain 与 walk 之间跑完 all-task teardown,将 task exit 到无 scheduler;walk 再触发 membership WARN 并把已 exit task re-home 到即将释放的层级 → use-after-free。

跳过 walk,直接到 dump: 标签做 dump 输出即可。

注释同步修正:"all descendant schedulers" → "all linked descendant schedulers"。

Patch 4 — 门控非 ext class 任务的 enable

子调度器 enable-commit 循环和 disable re-home 循环中,原代码无条件调用 scx_enable_task(sch, p)

修复在两处加门控:

if (p->sched_class == &ext_sched_class)
    scx_enable_task(sch, p);

对非 ext class 任务(SCX_OPS_SWITCH_PARTIAL 下可能存在),保持 READY 状态,等其通过 switching_to_scx() 自然切换时再 enable。这与 root enable 和 scx_post_fork() 的行为一致。

ASCII 流程图

三方死锁与锁顺序修复(Patch 2)

BEFORE FIX — circular dependency:

  scx enable/disable        cgroup rmdir          cpu.weight write
  ===================       ============          ================
  cgroup_lock() ──────────> waits for
       │                    cgroup_mutex
       │                         │
       v                         v
  percpu_down_write(rwsem)   kernfs_drain() ────> waits for
       │                         │                active ref
       │                         │                     │
       v                         v                     v
  (holds both)              waits in drain        percpu_down_read(rwsem)
       │                    for write ref ───────> blocked by writer
       │                                         (pending write-lock)
  ──────── CIRCULAR DEADLOCK ──────────────────────────────────

AFTER FIX — cgroup_lock first, dependency broken:

  scx enable/disable        cgroup rmdir          cpu.weight write
  ===================       ============          ================
  cgroup_lock()             cgroup_lock()         cgroup_lock()
       │                         │                     │
       v                         v                     v
  percpu_down_write(rwsem)  kernfs_drain()         percpu_down_read(rwsem)
       │                    (no rwsem dep)              │
       v                         │                     v
  (rwsem waits only         rmdir completes        read runs to completion
   for read sections        normally)              normally)

任务状态机与子调度器交互(Patch 3 & 4)

Sub-scheduler enable/disable task lifecycle:

  Task in cgroup A under sub-sched @sch
  ┌──────────────────────────────────────────┐
  │ INIT → READY → ENABLED (on ext class)   │
  │     or READY (on fair class, PARTIAL)    │
  └──────────────────────────────────────────┘

  Sub-disable re-home loop (BEFORE fix 4):
    ┌─ walk all tasks in subtree ──────────────┐
    │ scx_enable_task(parent, p)  ← UNCONDITIONAL│
    │ → fair-class task becomes ENABLED         │
    │   but stays on fair_sched_class            │
    │ → later SCHED_EXT switch:                 │
    │   WARN: ENABLED w/ prev != READY          │
    │   ops.enable() called TWICE                │
    └──────────────────────────────────────────┘

  Sub-disable re-home loop (AFTER fix 4):
    ┌─ walk all tasks in subtree ──────────────┐
    │ if (p->sched_class == ext_sched_class)   │
    │     scx_enable_task(parent, p)            │
    │ else: stay READY, switch naturally        │
    └──────────────────────────────────────────┘

  Never-linked sub-sched teardown (BEFORE fix 3):
    ┌─ scx_sub_disable(@sch) ─────────────────┐
    │ drain_descendants() can't see @sch        │
    │ root disable exits tasks → no scheduler   │
    │ task walk → WARN + re-home to dying tree  │
    │ → USE-AFTER-FREE                          │
    └──────────────────────────────────────────┘

  Never-linked sub-sched teardown (AFTER fix 3):
    ┌─ scx_sub_disable(@sch) ─────────────────┐
    │ if (list_empty(&sch->sibling))            │
    │     goto dump; // skip walk, just dump    │
    └──────────────────────────────────────────┘

概念类比

把 sched_ext 子调度器想象成一家连锁餐厅的管理制度

  • Patch 1(disallow):餐厅开业时(enable 路径),厨师可以贴"拒绝服务"标签,顾客会被引导到隔壁总店。但如果在非开业场景(比如分店关闭时的善后流程)贴这个标签,顾客的策略会被悄悄篡改,没人重新读这个标签。修复:非开业场景贴标签 = 立即关门报警,不允许静默篡改。

  • Patch 2(锁顺序死锁):三个部门——总部审批(cgroup_mutex)、区域授权(rwsem)、柜台服务(kernfs active ref)——形成环形等待。总部等区域释放授权,区域等柜台关账,柜台等总部审批完成。解决:总部审批先做完再等区域授权,环形链条断开。

  • Patch 3(未链接子调度器):一家还没挂招牌(未 link)的分店开始善后清算,总部看不到这家分店,总部的全店关闭流程可能先跑完,把员工遣散到"无归属"状态。分店的善后还在走路上的员工名单,把已遣散的人重新挂到即将拆除的组织架构 → 空指针。修复:没挂招牌的分店直接做报表输出,不走路上的员工。

  • Patch 4(非 ext 任务 enable):兼职员工(fair class)在总店允许兼职的制度(SWITCH_PARTIAL)下,只在兼职岗位上保持"待命"(READY)。如果善后流程强行把他们标记为"全职已就绪"(ENABLED),他们转全职时会触发矛盾状态(已是 ENABLED 却没有经过 READY → 全职步骤),且入职培训(ops.enable())被重复调用。修复:只有真正在全职岗位的员工才走 enable 流程。

Highlight 窑出问题

  1. Andrea 测试时发现了另一个 pre-existing bug:cgroup 迁移不更新 p->scx.sched,导致移出子调度器 cgroup 的 task 仍持有指向该子调度器的指针,子调度器释放后变成 dangling pointer → watchdog 读到垃圾 timeout → stall 报告 → exit 传播追踪垃圾 ->children 指针 → scx_claim_exit() 中的 use-after-free。Tejun 确认已有修复系列在准备中。这是本系列未覆盖的第五个 bug,需要持续跟进。

  2. Patch 2 的锁顺序反转风险:虽然 set_* 路径在 rwsem 读侧内不拿 cgroup lock,但如果未来有人在此读侧内新加 cgroup_lock() 调用,三方死锁会立即复现。代码注释已警告这一点,但需在代码审查中持续关注。

  3. Patch 4 的 SCX_OPS_SWITCH_PARTIAL 语义复杂性:partial 模式下 fair class 任务与 ext class 任务共存于同一子调度器 cgroup 中,状态机交互更复杂,后续改动需格外注意"只 enable ext class 任务"这一约束。

  4. 所有四个 Fixes tag 均指向同一 commit 337ec00b1d9c("sched_ext: Implement cgroup sub-sched enabling and disabling"),说明子调度器的初始实现存在系统性缺陷,本次是集中修补。

版本演进

本系列为首次提交,无版本演进记录。

与其他相关 patch 系列的关联

  • Tejun 在回复中明确提到正在准备cgroup 迁移时 re-home tasks 的修复系列,以解决 Andrea 发现的 p->scx.sched dangling pointer 问题。该系列将是本系列的直接后续。
  • 所有 Fixes tag 指向的 337ec00b1d9c 是子调度器功能的初始实现 commit,本系列是其首个集中 bugfix 补丁集。

一句话总结

四个针对 sched_ext 子调度器与 cgroup 交互的 bugfix:禁止非 enable 路径设 disallow、修复三方 cgroup 锁死锁、跳过未链接子调度器的 teardown task walk、以及门控非 ext class 任务的 enable——均为子调度器初始实现(337ec00b1d9c)的系统性修补,已获 Reviewed-by 并合入 for-7.2-fixes。