sched discussion
[PATCH 0/2] sched: Remove sched_class::balance()
LLM 分析
scheduler:移除 sched_class::balance() 与 core-sched newidle 修复
系列概况
- 标题:[PATCH 0/2] sched: Remove sched_class::balance()
- 作者:Peter Zijlstra (Intel) peterz@infradead.org
- 版本:v1(仅一版公开)
- 规模:2 个 patch,修改 7 个文件,+22/-82 行
- 修改文件:
kernel/sched/core.c、fair.c、rt.c、deadline.c、idle.c、stop_task.c、sched.h - 代码统计:净删除约 60 行;移除
sched_class::balance回调,新增task_on_core()帮助函数 - Message-ID:
20260624121327.190063948@infradead.org - 完整性:cover letter + 2 patches;后续 17 封回复围绕 NULL deref 复现、core-sched 竞争、SCX/ext 兼容性
补丁目的
- 让 core-sched 下
newidlebalancing 真正在每个 SMT sibling 上各跑一次; - 消除
balance()与pick_task()的重复逻辑——它们都 droprq->lock并做拉活; - 为
sched_ext简化铺路,因为 ext 的 DSQ move 必须在 pick 决策同一把锁内看清楚。
旧流程的问题
__pick_next_task() / pick_next_task() 入口先调 prev_balance(rq, rf):
prev_balance(rq)
|- for_active_class_range(class, prev->class, &idle_sched_class)
| |- class->balance(rq, rf) # rt/dl pulls
|- for_each_active_class(class)
|- class->pick_task(rq, rf) # fair/ext drops lock and balances again
两个问题:
prev_balance()在 core-sched 下只对 prev 那个 sibling 跑,其他 SMT 兄弟没机会拉活;balance()与pick_task()都收@rf、drop 锁、迁移任务,逻辑重叠;为了撤销 ext 的 DSQ move 又不得不把这两套叠在一起。
新流程
去掉 prev_balance(),把 balance_rt/dl 直接放进 pick_task_rt/dl,并用 rq_modified_begin/end 信号化"我改了更高优先级 class":
__pick_next_task()
|- for_each_active_class(class)
|- class->pick_task(rq, rf)
|- pick_task_dl(): balance_dl() -> rq_modified_above -> RETRY_TASK
|- pick_task_rt(): balance_rt() -> rq_modified_above -> RETRY_TASK
|- pick_task_fair(): sched_balance_newidle() -> RETRY_TASK if needed
RETRY_TASK 让 pick_next_task() 走 restart_multi 重选,把"锁释放后状态变了"兜在一个 core-wide 原子化重选里。
Patch 概览
PATCH 1/2 sched/core: Allow newidle for core-sched
- 删除
pick_task_fair()在 idle 分支里if (sched_core_enabled(rq)) return NULL;,让 core-sched 也能跑 newidle; - 新增
task_on_core(rq, p)(CONFIG_SCHED_CORE下检查rq->core_pick == p); can_migrate_task()、task_is_pushable()增加task_on_core()判断,防止把已被兄弟 SMT 选中的任务再迁过来。
PATCH 2/2 sched: Remove sched_class::balance()
- 删
prev_balance()、balance_idle/dummy、sched_class::balance字段; balance_rt/dl返回值改void,被pick_task_rt/dl开头调用;sched_balance_newidle()不再用 int 三态返回,改用rq_modified_above()触发RETRY_TASK;- 所有 idle/dl/rt/stop sched_class 结构体的
.balance = ...一并清除。
关键实现
static struct task_struct *__pick_task_dl(struct rq *rq, struct rq_flags *rf)
{
struct dl_rq *dl_rq = &rq->dl;
struct task_struct *p;
rq_modified_begin(rq, &dl_sched_class);
balance_dl(rq, rf);
if (rq_modified_above(rq, &dl_sched_class))
return RETRY_TASK;
again:
if (!sched_dl_runnable(rq))
return NULL;
...
}
static inline bool task_on_core(struct rq *rq, struct task_struct *p)
{
if (sched_core_disabled())
return false;
return rq->core_pick == p;
}
旧:prev_balance 先跑,再 pick_task 又跑一次 balance
+------------------------+
| prev_balance(rq,rf) | <- only on prev SMT sibling
| balance_rt/dl() |
+------------------------+
|
v
+------------------------+
| class->pick_task() | <- drops lock, balances again
+------------------------+
新:pick_task 自带 balance,RETRY_TASK 兜底
+------------------------------------+
| class->pick_task(rq,rf) |
| balance_{rt,dl,fair} embedded |
| rq_modified_above() -> RETRY |
+------------------------------------+
| RETRY
v
restart_multi (core-wide repick)
Aaron 报告的 NULL deref 竞争:
cpuX cpuY
------- -------
pick_next_task()
goto restart_multi
rqX->core_pick = pick_task(rqX)
pick_task(rqY)
pick_task_fair(rqY)
sched_balance_newidle(rqY)
raw_spin_rq_unlock(rqY) // drop core lock
pick_next_task()
goto restart_multi
rqY->core_pick = pick_task(rqY)
rqX->core_pick = pick_task(rqX)
if (rqX->curr == rqX->core_pick)
rqX->core_pick = NULL
UNLOCK rq_lockp(rqY)
raw_spin_rq_lock(rqY)
rqY->core_pick = pick_task(rqY)
p = rqX->core_pick // NULL
cookie_equals(p, cookie) // NULL deref -> crash
类比
把 sched_class::balance() 想成公司里每个部门(sched_class)雇的"跨部门借调员":他们站在门口,看到隔壁忙就拉人过来。但调度中心(pick_task)本来就要做同样的事,等于一份活两套人马。
更关键的是"分店经理"场景:以前 core-sched 只派一位分店经理(prev_balance)巡视,他只盯自己那家分店,隔壁根本没人帮忙调度——明明隔壁闲得发慌,这边还在排队。改成"每家分店自己看调度板(pick_task 内嵌 balance)",大家都能看到全局。
RETRY_TASK 则像菜点错了服务员退回去重做:下单后厨房状态变了(lock-break),服务员宁可把单子撕掉重写,也不端半成品上桌。
Highlight:风险与注意点
- NULL deref 已复现:Aaron 用带宽测试脚本复现
core_pick == NULL路径,崩溃点在cookie_equals(p, cookie)。原因是 newidle 锁释放期间另一兄弟把core_pick清空,重锁后没人校验 seq。 RETRY_TASK没有 forward progress 保证:无限 lock-break 会让任务在两个 SMT 间 ping-pong;Peter 准备用core_task_seq给 lock-break 计数做上限。sched_setaffinity与 newidle 竞争:K Prateek 提议把task_on_core也走 stopper 路径,否则affine_move_task可能误判可迁移性。sched_ext影响:Tejun 强调 ext 的 lock-drop 只在需要把任务搬到本地 DSQ 时发生,下一轮 pick 时任务已在 local DSQ,因此 RETRY 不会真的反复触发——这同时意味着 ext 可以被进一步简化。- 测试覆盖薄弱:作者承认
rt-migration-test没抓到任何问题;后续依赖util-linux/coresched+perf bench sched messaging -g 8跑压力才比较稳。
版本变化
仅 v1 一版公开。后续修复方向(core_task_seq + lock-break 计数 + 可能把 idle pick 改为 pick_task(rq, NULL))尚未形成正式 patch。
一句话总结
通过把 balance() 卷进 pick_task() 并引入 rq_modified_above() + RETRY_TASK 解决 core-sched newidle "SMT 兄弟拉不到活" 的问题,但补丁同时暴露了 core-sched 在 lock-break 后 core_pick 被清空的 NULL deref 竞争,需要 core_task_seq 机制收尾。