0/4 已展开

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() 像人事部门发起"全员按指定楼层重新登记"的活动,正常员工会去新楼层打卡,但通知发到保安大叔头上就会出现两种怪事:

  1. 离岗流程要求他下班登记,可他不下班(idle sched class 没有真正的 dequeue_task),人事只能打印一张"保安居然下班了"的提示;
  2. 重新上岗流程让他去新楼层,他根本没有这个动作的剧本(enqueue_task 是 NULL),结果在登记表上签字时,笔尖是断的——这就是 RIP: 0010:0x0

补丁相当于在通知名单上加一条:"保安大叔请忽略此通知",既避免他在怪流程里被卡住,也避免人事改写他的"所属楼层"档案造成后续 ___migrate_enable() 永远走不通。

Highlight:风险与注意点

  • 守卫语义破坏dequeue_task_idle 在守卫临界区里 drop + re-take rq->lock,即使打了补丁前的旧代码也已经是隐患;补丁虽然绕开了入口,但守卫本身的健壮性需要后续 review。
  • 真正的根因不在调度器:作者后续的 vmcore 显示 swapper/3->migration_disabled == 0x8,而 migrate_disable() API 不会产生 (counter=8, nr_pinned=0) 这种组合,这是一个单比特位翻,怀疑 DDR5 非 ECC 内存硬件问题(同一台机器还出现过 css_rstat_flush GPF、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 的单比特位翻。