sched-ext discussion
[PATCH sched_ext/for-7.3-fixes] sched_ext: Serialize cgroup knob updates
LLM 分析
sched_ext:序列化 cgroup knob 更新
系列概况
- 标题:
[PATCH sched_ext/for-7.3-fixes] sched_ext: Serialize cgroup knob updates - 作者:Andrea Righi
<arighi@nvidia.com> - 版本:单封 patch,目标分支
sched_ext/for-7.3-fixes - 规模:修改 4 个文件,
+38 / -10行 - 修改文件:
include/linux/sched/ext.h、kernel/sched/core.c、kernel/sched/ext/ext.c、kernel/sched/ext/ext.h - Message-ID:
20260825092153.2602809-1-arighi@nvidia.com - 完整性:单封 patch 完整,可独立 review;回信讨论替代方案
补丁目的
并发写 cgroup knob(weight / idle / bandwidth)时,核心调度器在自己的内部锁里更新 task_group,但 释放锁之后 才把这次更新通知给 sched_ext。两个并发的写各自完成核心调度器更新,但它们的 sched_ext 通知可能乱序完成。结果就是:核心调度器里 tg->shares 已经更新到 B 值,BPF scheduler 看到的却是 A 值(或反之),三者状态错位。补丁的目标是把"核心调度器更新"和"sched_ext 通知"绑成一个原子事务,确保同一 task_group 的连续写严格按顺序落地。
旧流程的问题
CPU A: cgroup_lock --> sched_group_set_shares(100) --> cgroup_unlock |
v (delay before BPF call)
scx_group_set_weight(100) <- notify A
CPU B: cgroup_lock --> sched_group_set_shares(200) --> cgroup_unlock
v (delay)
scx_group_set_weight(200) <- notify B (arrives first)
Problem: notify B reaches BPF scheduler before notify A
core scheduler says 200, BPF cache says 100 -> mismatch
sched_ext 已经有一把 scx_cgroup_ops_rwsem(percpu rwsem),但它只保护 BPF ops 调用本身,不覆盖核心调度器那段更新,所以串不起来。
新流程
给每个 task_group 加一把 knob_mutex,把核心调度器更新和 sched_ext 通知都包在同一把锁里:
CPU A: knob_mutex.lock --> sched_group_set_shares(100) --> scx_group_set_weight(100) --> knob_mutex.unlock
CPU B: |
CPU B: v (blocked)
CPU B: knob_mutex.lock --> sched_group_set_shares(200) --> scx_group_set_weight(200) --> knob_mutex.unlock
Result: notifications follow core update order strictly, all three views consistent
Patch 概览
四处改动:
include/linux/sched/ext.h:在struct scx_task_group增加struct mutex knob_mutex;kernel/sched/ext/ext.c:scx_tg_init()中mutex_init(&tg->scx.knob_mutex);kernel/sched/ext/ext.h:定义 lock / unlock 函数,并用DEFINE_GUARD(scx_group_knob, ...)包成作用域锁;当CONFIG_EXT_GROUP_SCHED关闭时退化为空函数。kernel/sched/core.c:在tg_weight、cpu_idle_write_s64、cpu_weight_write_u64、cpu_weight_nice_write_u64、tg_set_bandwidth等写路径加入guard(scx_group_knob)(tg);,并把css_tg(css)提取到局部变量以减少重复求值。
关键实现
锁封装代码(来自 kernel/sched/ext/ext.h):
static inline void scx_group_knob_lock(struct task_group *tg)
{
mutex_lock(&tg->scx.knob_mutex);
}
static inline void scx_group_knob_unlock(struct task_group *tg)
{
mutex_unlock(&tg->scx.knob_mutex);
}
DEFINE_GUARD(scx_group_knob, struct task_group *,
scx_group_knob_lock(_T), scx_group_knob_unlock(_T));
/* CONFIG_EXT_GROUP_SCHED disabled stubs */
static inline void scx_group_knob_lock(struct task_group *tg) {}
static inline void scx_group_knob_unlock(struct task_group *tg) {}
调用点改写(来自 kernel/sched/core.c):
static int cpu_weight_write_u64(struct cgroup_subsys_state *css,
struct cftype *cftype, u64 cgrp_weight)
{
struct task_group *tg = css_tg(css);
unsigned long weight;
int ret;
weight = sched_weight_from_cgroup(cgrp_weight);
guard(scx_group_knob)(tg);
ret = sched_group_set_shares(tg, scale_load(weight));
scx_group_set_weight(tg, cgrp_weight);
return ret;
}
guard() 是内核作用域锁惯用法,函数返回时自动 unlock,避免漏写 unlock 或提前 return 留下死锁。锁作用域关系见下图:
+-----------------------------+
| cgroup write handler entry |
+-------------+---------------+
|
v
+-----------------------------+
| guard(scx_group_knob)(tg) | <- acquires knob_mutex
+-------------+---------------+
|
v
+-----------------------------+
| sched_group_set_shares(tg) | <- core scheduler update
+-------------+---------------+
|
v
+-----------------------------+
| scx_group_set_weight(tg) | <- sched_ext notify (BPF call)
+-------------+---------------+
|
v
+-----------------------------+
| function return / guard exit| <- knob_mutex auto-released
+-----------------------------+
类比
把每个 cgroup 想象成银行的一个 VIP 客户账户,并发写就像两个柜员同时改这个账户的余额:
- 旧流程:柜员 A 在 A 窗口把余额改成 100,柜员 B 在 B 窗口把余额改成 200,但 A 的"短信通知"发得比 B 晚。客户看到的短信顺序是 200 -> 100,跟卡里真实余额对不上,账单对账失败。
- 新流程:给每个 VIP 账户发一把"业务章"(
knob_mutex),任何柜员改这个账户都要先盖章,改完余额立刻发短信,最后才能解章。两个柜员不可能同时改同一个账户,短信和真实余额就一定对齐。
更进一步,Tejun 推荐的 Michal 方案相当于直接把"短信通道"挪进银行核心系统,让改余额和发通知在同一笔业务里完成,连那把额外的章都不用发——更"治本"。
Highlight:风险与注意点
- 维护者倾向 Michal 的方案:Tejun 明确说"I'm inclined to go with that one",Michal 把 fair 的锁提升到 core 写 handler,共用一把锁,省掉
knob_mutex这一层嵌套。Andrea 这版大概率会被弃用或演化成 v2。 - 嵌套锁风险:
knob_mutex只在 cgroup 写路径获取,没有和rq lock/ cgroup 自旋锁嵌套,但tg_set_bandwidth里还有cfs_bandwidth相关锁,新加的 guard 是否落在合理位置需要细看 diff 上下文。 - CONFIG 开关覆盖:
cpu_weight_write走CONFIG_GROUP_SCHED_WEIGHT,cpu_idle_write走CONFIG_CFS_BANDWIDTH,guard 调用要确保对应编译路径下scx_group_knob_lock桩存在;#else分支需要空桩版本(本 patch 已提供)。 - scx_cgroup_ops_rwsem 重叠:已存在的 percpu rwsem 是为 BPF ops 串行化而设;
knob_mutex范围更宽、且非 percpu,要留意未来是否会引入不必要的全局争用。 - 回归验证点:每个写 handler 都加锁后,需跑 cgroup v1/v2 的 weight / cpu.idle / cpu.max 写压测,确认不会出现死锁、优先级反转或 BPF 程序被饿死。
版本变化
单封 patch,没有 v2/v3。
与其他 patch 系列的关联
Tejun 在回信中点名了 Michal Koutný 的同主题 patch:
http://lkml.kernel.org/r/20260824074913.2468177-1-michalblk@google.com
两版是同一问题的两种修法:
- Andrea:scx 侧加 per-tg mutex,core 侧用 guard 加锁。
- Michal:把 fair 写 handler 的锁提升,使 core 更新和 sched_ext 通知在同一把锁下串行。
维护者倾向 Michal 的"提升到 core"做法。
一句话总结
并发写 cgroup knob 时核心调度器更新和 sched_ext 通知会乱序,导致 core / BPF / sched_ext 缓存三者值错位——补丁在每个 task_group 上加一把 knob_mutex 把两次更新绑成原子事务,但维护者更偏好另一种"把 fair 锁提到写 handler 里"的同主题方案。