sched discussion
Re: [PATCH] sched/core: Don't pin the idle task in migrate_disable_switch()
LLM 分析
sched/core:在 migrate_disable_switch() 中跳过 idle 任务
系列概况
- 标题:[PATCH] sched/core: Don't pin the idle task in migrate_disable_switch()
- 作者:Krystian Slowik me@krystianslowik.com
- 版本:v1(单 patch,无版本号标注)
- 规模:1 个 patch,1 个文件,+4 行
- 修改文件:
kernel/sched/core.c - 代码统计:
1 file changed, 4 insertions(+),新增一段 4 行的早返回保护 - Message-ID(首封):
20260806071740.83931-1-me@krystianslowik.com - 完整性:4 封邮件齐全 — patch + 3 封回复(Peter Zijlstra 提问 + 作者根因调查 + 维护者追问)
补丁目的
修复一个生产环境反复触发的内核 NULL 指针解引用:当 idle 任务被调度出去时,migrate_disable_switch() 会把 idle 任务当成普通任务去 pin / migrate,但 idle sched class 的 enqueue_task 是 NULL,sched_change_end() 通过函数指针调用就跳进 RIP 0010:0x0。
思路非常直接:idle 任务是 per-CPU 常驻任务,永远不会迁移,pin 它没有意义。在函数入口检查 p == rq->idle 并提前返回,顺带避免 idle 的 cpus_ptr 被改写而造成 ___migrate_enable() 不可达。
旧流程的问题
static void migrate_disable_switch(struct rq *rq, struct task_struct *p)
{
// p可能是 idle 任务,但仍然走完整路径
if (p->cpus_ptr != &p->cpus_mask)
return;
scoped_guard(task_rq_lock, p)
do_set_cpus_allowed(p, &ac);
}
调用链 __schedule() -> schedule_idle() -> migrate_disable_switch(),自 commit 942b8db96500 把 migrate_disable_switch() 提到 __schedule() 顶部之后,每次出 idle loop 都会执行。一旦 p->migration_disabled 非零(哪怕是异常置位),就会进入 do_set_cpus_allowed(),进而触发 sched_change_begin / sched_change_end 守卫:
sched_change_begin()调用dequeue_task(rq, p, DEQUEUE_SAVE),命中 idle 的dequeue_task_idle,里面只是触发"bad: scheduling from the idle thread!"调试桩,并在守卫临界区内 drop + re-take rq lock,破坏守卫语义;sched_change_end()走enqueue_task()函数指针,idle 类enqueue_task = NULL,调用(*enqueue_task)()时跳进 NULL。
新流程
static void migrate_disable_switch(struct rq *rq, struct task_struct *p)
{
/* The per-CPU idle task never migrates, there is nothing to pin. */
if (p == rq->idle)
return;
if (p->cpus_ptr != &p->cpus_mask)
return;
scoped_guard(task_rq_lock, p)
do_set_cpus_allowed(p, &ac);
}
注意:用 p == rq->idle 而不是 is_idle_task(p),因为后者也会匹配 PF_IDLE 的 idle-injection 线程(普通可队列化任务),不能用同一招跳过它们。
关键实现
__schedule()
|
v migrate_disable_switch(rq, p)
|
+---------+---------+
| p == rq->idle ? |
+---------+---------+
yes | | no v v return p->cpus_ptr != &p->cpus_mask ?
+---------+---------+
yes | | no
v v return scoped_guard(task_rq_lock, p)
do_set_cpus_allowed(p, &ac)
|
v
sched_change_begin()
|
v
dequeue_task_idle(p)
(only prints warning stub,
drops + re-takes rq->lock)
|
v
sched_change_end()
|
v
enqueue_task(p)
-> idle class fn ptr = NULL
-> RIP 0010:0x0 (oops)
调用栈(崩溃现场):
schedule_idle
-> __schedule
-> migrate_disable_switch -> sched_change_begin -> dequeue_task_idle ("bad: scheduling from idle thread!")
-> sched_change_end
-> enqueue_task (NULL fn ptr) -> RIP: 0010:0x0
类比
把 idle 任务想象成每个楼层的保安大叔,他永远站在自己那栋楼(per-CPU)的电梯口,从来不会换楼层。migrate_disable_switch() 像人事部门发起"全员按指定楼层重新登记"的活动,正常员工会去新楼层打卡,但通知发到保安大叔头上就会出现两种怪事:
- 离岗流程要求他下班登记,可他不下班(idle sched class 没有真正的
dequeue_task),人事只能打印一张"保安居然下班了"的提示; - 重新上岗流程让他去新楼层,他根本没有这个动作的剧本(
enqueue_task是 NULL),结果在登记表上签字时,笔尖是断的——这就是RIP: 0010:0x0。
补丁相当于在通知名单上加一条:"保安大叔请忽略此通知",既避免他在怪流程里被卡住,也避免人事改写他的"所属楼层"档案造成后续 ___migrate_enable() 永远走不通。
Highlight:风险与注意点
- 守卫语义破坏:
dequeue_task_idle在守卫临界区里 drop + re-takerq->lock,即使打了补丁前的旧代码也已经是隐患;补丁虽然绕开了入口,但守卫本身的健壮性需要后续 review。 - 真正的根因不在调度器:作者后续的 vmcore 显示
swapper/3->migration_disabled == 0x8,而migrate_disable()API 不会产生(counter=8, nr_pinned=0)这种组合,这是一个单比特位翻,怀疑 DDR5 非 ECC 内存硬件问题(同一台机器还出现过css_rstat_flushGPF、slab freelist、RCU cblist 损坏)。补丁屏蔽了症状,但 scheduler 之外的硬件可靠性调查才是治本。 - Stable 回溯范围:作者打了
Cc: stable # v7.0+,涉及650952d3fb38引入的sched_change模式之后的所有版本,backport 范围较大。 - API 选取理由:用
p == rq->idle而不是is_idle_task(),避免误伤 idle-injection 线程;这是补丁里很容易被 reviewer 漏掉的判断点。
一句话总结
在 migrate_disable_switch() 入口加 if (p == rq->idle) return;,跳过永远不会被迁移的 idle 任务,修复 sched_change_end 通过 NULL enqueue_task 跳进 RIP: 0 的崩溃,同时给上游指明根因更像 DDR5 非 ECC 的单比特位翻。