sched discussion
[PATCH] sched/rt,dl: Skip migrate-disabled tasks when picking a push candidate
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:
- 完整性:patch 完整;含
Fixes:tag 与Signed-off-by;tip commitdae5c0292080,Committter Peter Zijlstra,提交日期 2026-09-02,已合入sched/urgent
补丁目的
修复 RT/DL 调度器在 migrate_disable() 任务存在时的无效 push 循环。一个被 migrate_disable() 钉在当前 CPU 上的任务:
- 仍留在
rq->rt.pushable_tasks/rq->dl.pushable_dl_tasks_root链表里; - 仍把对应 runqueue 标记为
overloaded; - 让其它 CPU 的 push balancer 不断发 push-IPI 试图迁移它;
- 迁移必然失败,于是 fallback 去 push
rq->curr(通过 stopper 线程); find_lock_lowest_rq()的 re-check 又因 pushable head 不是curr永远判负;- 循环重复,每次都不移动任务,但持续吞噬 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 = taskA(migrate_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_task、push_dl_task、find_lock_lowest_rqre-check、stopper 唤醒)的无效分支一并关掉。
作者同时点出三条相关历史 commit:
a7c81556ec4d(sched: Fix migrate_disable() vs rt/dl balancing)—— 引入「push pinned 时改 push curr」的 stopper 兜底;本补丁关闭该兜底被触发的路径。94894c9c477e(sched/rt: Skip currently executing CPU in rto_next_cpu())—— 关闭 self-IPI 路径;但因为overloaded仍被置位,外部 CPU 的 push-IPI 仍会进入同一循环,所以需要本次补丁。e0ca8991b2de(sched: 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。