0/2 已展开

LLM 分析

sched/rt,dl:跳过被 migrate_disable 的任务作为 push 候选

系列概况

  • 标题[PATCH] sched/rt,dl: Skip migrate-disabled tasks when picking a push candidate
  • 作者:Seiji Nishikawa <snishika@redhat.com>
  • 版本:单封 v1(无 v2 标记),随后由 tip-bot2 公告合并到 tip: sched/urgent
  • 规模:2 文件修改,+4 / -4 行(每个文件仅改一行条件)
  • 修改文件
    • kernel/sched/rt.c —— pick_next_pushable_task()
    • kernel/sched/deadline.c —— pick_next_pushable_dl_task()
  • 代码统计:仅两个 if 条件改写,无新增函数或数据结构
  • Message-ID
    • 主 patch:20260830073746.2189355-1-snishika@redhat.com
    • tip-bot 公告:178833372384.3717435.9283330313829938009.tip-bot2@tip-bot2
  • 完整性:patch 完整;含 Fixes: tag 与 Signed-off-by;tip commit dae5c0292080,Committter Peter Zijlstra,提交日期 2026-09-02,已合入 sched/urgent

补丁目的

修复 RT/DL 调度器在 migrate_disable() 任务存在时的无效 push 循环。一个被 migrate_disable() 钉在当前 CPU 上的任务:

  1. 仍留在 rq->rt.pushable_tasks / rq->dl.pushable_dl_tasks_root 链表里;
  2. 仍把对应 runqueue 标记为 overloaded
  3. 让其它 CPU 的 push balancer 不断发 push-IPI 试图迁移它;
  4. 迁移必然失败,于是 fallback 去 push rq->curr(通过 stopper 线程);
  5. find_lock_lowest_rq() 的 re-check 又因 pushable head 不是 curr 永远判负;
  6. 循环重复,每次都不移动任务,但持续吞噬 CPU 时间。

补丁让 pick_next_pushable_task() / pick_next_pushable_dl_task() 在选择候选时直接跳过 is_migration_disabled(i) 的任务,从源头关掉这个循环。

旧流程的问题

旧条件只过滤 task_on_cpu,没有处理 migrate_disable

/* make sure task isn't on_cpu (possible with proxy-exec) */
if (!task_on_cpu(rq, i)) {
    p = i;
    break;
}

后果链条(作者实测 89 ms 窗口):

  • pushable head = taskAmigrate_disable + queued)
  • rq->rt.overloaded = 1 一直成立
  • push-IPI → pick_next_pushable_task() 返回 taskA → push_rt_task() 试图 push 失败
  • fallback push rq->curr(taskB)→ wake stopper 抢占 taskB
  • find_lock_lowest_rq() re-check:taskB != taskA → 放弃
  • 下一轮 push-IPI 完全重复上述过程
                +---------------------------+
                |  push-IPI arrives         |
                +---------------------------+
                              |
                              v
       +------------------------------------------+
       | pick_next_pushable_task() -> taskA      |
       | (head of pushable list, pinned)         |
       +------------------------------------------+
                              |
                              v
       +------------------------------------------+
       | push_rt_task() cannot move taskA        |
       | => fall back: push rq->curr (taskB)     |
       | => wake stopper (migration/N)           |
       +------------------------------------------+
                              |
                              v
       +------------------------------------------+
       | find_lock_lowest_rq() re-check:         |
       |   taskB  vs  pushable head taskA        |
       |   => mismatch, give up                  |
       +------------------------------------------+
                              |
                              v
       +------------------------------------------+
       | rq unchanged: taskA still head,         |
       | overloaded still set                    |
       | => ~17 us later, next push-IPI repeats  |
       |    (5204 times in 89 ms)                |
       +------------------------------------------+

实测数据:89 ms 内 taskB 仅得 ~52 ms CPU,stopper 抢走 ~37 ms,taskA 全程 runnable+queued 但 0 ms;re-check 失败 5204 次,成功 push 1 次,taskA 迁移 0 次。

新流程

补丁把 skip 条件扩展为「既不在 CPU 上,也不能迁移」:

/* skip tasks that cannot be migrated */
if (!task_on_cpu(rq, i) && !is_migration_disabled(i)) {
    p = i;
    break;
}

行为变化:

  • 若 pinned 任务是 pushable 列表上唯一的可考虑项,pick_next_pushable_*() 返回 NULL
  • push_rt_task() / push_dl_task() 早放弃,不再 wake stopper,不会触发 re-check 失败循环。
  • pinned 任务就地等待 curr 让出 CPU,本地自然被调度。
  • 若 pinned 任务后面还有真正可迁移的任务,新逻辑会跳过 pinned 头、选中后者并真正完成 push。
  • rq->rt.overloaded 的含义回归「这个 rq 上确实有可迁移的多余任务」,不再被「幽灵 pushable」污染。

Patch 概览

两个 pick_next_pushable_*() 共享完全相同的语义改写,是 1:1 镜像:

  • kernel/sched/rt.c:1872 —— pick_next_pushable_task()
  • kernel/sched/deadline.c:3028 —— pick_next_pushable_dl_task()

关键实现

  • is_migration_disabled(i) 检查 i->migration_disabled 计数器,返回真即任务被 migrate_disable() 钉住、无法跨 CPU 迁移。
  • 这个 skip 与既有的 !task_on_cpu(rq, i) 是「与」关系,覆盖两个不同维度的「不可被 push」:正在本 CPU 跑 + 被 migrate pin 住。
  • 循环结构与退出语义不变,差异只来自首过条件;进入 p = i; break; 的逻辑保持不变。
  • 这两个函数都是 push 路径的入口;修这一个入口,就把整个 push 流程(IPI、push_rt_taskpush_dl_taskfind_lock_lowest_rq re-check、stopper 唤醒)的无效分支一并关掉。

作者同时点出三条相关历史 commit:

  • a7c81556ec4dsched: Fix migrate_disable() vs rt/dl balancing)—— 引入「push pinned 时改 push curr」的 stopper 兜底;本补丁关闭该兜底被触发的路径。
  • 94894c9c477esched/rt: Skip currently executing CPU in rto_next_cpu())—— 关闭 self-IPI 路径;但因为 overloaded 仍被置位,外部 CPU 的 push-IPI 仍会进入同一循环,所以需要本次补丁。
  • e0ca8991b2desched: Make class_schedulers avoid pushing current, and get rid of proxy_tag_curr())—— 给 !task_on_cpu 提供了先例;本补丁把 migration_disabled 任务视为同一类「绝不应该被当作 push 候选」的对象。

类比

把 CPU 想象成一个小区门口的快递柜。pushable_tasks 是「今天还没被收件人取走的包裹清单」,overloaded 是快递员看到这份清单就会按门铃通知隔壁小区快递员来拉走。

migrate_disable() 的任务相当于贴了「请勿转投,只许本小区收件人本人取」的包裹:它被锁在本小区收件人手上,但仍然挂在「未被取走」的清单上。结果隔壁快递员一直按门铃、按完发现不能转走,就临时把当前正要下楼的主人(rq->curr)叫下来代行取件,主人走到一半又被叫回去(re-check 不过),然后下一位快递员又来按门铃。

补丁做的事情就是给清单加一条规则:只要包裹贴了「不许转投」标签,就不要写进清单。这样快递员看一眼就知道这个柜子没东西可拉,转身走人,把 CPU 时间还给真正在等的收件人。

Highlight:风险与注意点

  • Fixes: 指向 a7c81556ec4d:意味着上游认定那是引入 regression 的提交;stable backport 时应成对评估,避免半截修复。
  • DL 路径同样改动:同时改了 pick_next_pushable_dl_task(),DL 工作负载若长期 migrate_disable 也会有相同循环,需要在 DL 场景独立测试,而不是只看 RT。
  • 隐式语义变化pick_next_pushable_*() 返回 NULL 的概率上升,调用方需要正确处理;commit message 给出「pinned 任务就地等 curr 让出」这一假设,需要确认 RT/DL 的 pick 主循环不会因为 pushable 头变化而误判 rq 状态。
  • overloaded 标志的真实性:补丁没有显式清 overloaded,而是通过让 pushable 头不再包含 pinned 任务,使 pick_next_pushable_*() 自然返回 NULL,进而让 push 路径不再认为该 rq 有可迁任务。是否在 migrate_enable() 时主动重置 overloaded,是值得 follow-up 的方向。
  • 测量基线依赖:commit message 的实测数字(89 ms / 5204 次 / 37 ms stopper)依赖作者额外打的 tracepoint;merge 前需要确认这些 tracepoint 不会随补丁一起进主干。
  • rto_next_cpu() 修复互补:单独打 94894c9c477e 或单独打本补丁都不完整;评审时要看两个 fix 是否都打到目标 stable 分支。

一句话总结

在 RT/DL 的 pick_next_pushable_*() 入口处同时跳过 migrate_disable 的任务,让一个永远推不动的 pinned 任务不再把 runqueue 误标为 overloaded,也不再触发 push → stopper → re-check 失败的死循环,把本该属于实时任务的 CPU 时间还给真正在跑的 curr。