0/20 已展开

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.cfair.crt.cdeadline.cidle.cstop_task.csched.h
  • 代码统计:净删除约 60 行;移除 sched_class::balance 回调,新增 task_on_core() 帮助函数
  • Message-ID20260624121327.190063948@infradead.org
  • 完整性:cover letter + 2 patches;后续 17 封回复围绕 NULL deref 复现、core-sched 竞争、SCX/ext 兼容性

补丁目的

  1. 让 core-sched 下 newidle balancing 真正在每个 SMT sibling 上各跑一次;
  2. 消除 balance()pick_task() 的重复逻辑——它们都 drop rq->lock 并做拉活;
  3. 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_TASKpick_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/dummysched_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:风险与注意点

  1. NULL deref 已复现:Aaron 用带宽测试脚本复现 core_pick == NULL 路径,崩溃点在 cookie_equals(p, cookie)。原因是 newidle 锁释放期间另一兄弟把 core_pick 清空,重锁后没人校验 seq。
  2. RETRY_TASK 没有 forward progress 保证:无限 lock-break 会让任务在两个 SMT 间 ping-pong;Peter 准备用 core_task_seq 给 lock-break 计数做上限。
  3. sched_setaffinity 与 newidle 竞争:K Prateek 提议把 task_on_core 也走 stopper 路径,否则 affine_move_task 可能误判可迁移性。
  4. sched_ext 影响:Tejun 强调 ext 的 lock-drop 只在需要把任务搬到本地 DSQ 时发生,下一轮 pick 时任务已在 local DSQ,因此 RETRY 不会真的反复触发——这同时意味着 ext 可以被进一步简化。
  5. 测试覆盖薄弱:作者承认 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 机制收尾。