sched-ext discussion
[PATCHSET sched_ext/for-7.2-fixes] sched_ext: Assorted sub-scheduler and cgroup fixes
LLM 分析
sched_ext: 子调度器与 cgroup 修复系列分析
系列基线信息
| 项目 | 内容 |
|---|---|
| 标题 | sched_ext: Assorted sub-scheduler and cgroup fixes |
| 作者 | Tejun Heo |
| 版本 | 未标注版本号,单次提交 |
| 规模 | 4 patches,2 files,+43 / −14 |
| Message-ID | 20260716213058.1739522-1-tj@kernel.org |
| 目标分支 | sched_ext/for-7.2-fixes |
| 来源 | sched-ext 频道 |
| 状态 | 已合入(Tejun 确认 applied 1-4) |
明确目的
本系列修复 sched_ext 子调度器(sub-scheduler)和 cgroup 交互中的四个 bug:
- 0001:
ops.init_task()在非 enable 路径设置disallow会走一条仅适用于 root enable 的"静默回退策略"逻辑,导致 live task 的策略被悄悄改写。修复方式:直接 fail 整个 scheduler,与 fork 和 non-root 分支一致。 - 0002:
scx_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.h 中 disallow 字段文档:原文将 !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 窑出问题
-
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,需要持续跟进。 -
Patch 2 的锁顺序反转风险:虽然
set_*路径在 rwsem 读侧内不拿 cgroup lock,但如果未来有人在此读侧内新加 cgroup_lock() 调用,三方死锁会立即复现。代码注释已警告这一点,但需在代码审查中持续关注。 -
Patch 4 的
SCX_OPS_SWITCH_PARTIAL语义复杂性:partial 模式下 fair class 任务与 ext class 任务共存于同一子调度器 cgroup 中,状态机交互更复杂,后续改动需格外注意"只 enable ext class 任务"这一约束。 -
所有四个 Fixes tag 均指向同一 commit
337ec00b1d9c("sched_ext: Implement cgroup sub-sched enabling and disabling"),说明子调度器的初始实现存在系统性缺陷,本次是集中修补。
版本演进
本系列为首次提交,无版本演进记录。
与其他相关 patch 系列的关联
- Tejun 在回复中明确提到正在准备cgroup 迁移时 re-home tasks 的修复系列,以解决 Andrea 发现的
p->scx.scheddangling 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。