sched discussion
[PATCH v3] sched/core: Skip rq->avg_idle update without a valid idle_stamp
LLM 分析
sched/core:跳过无效 idle_stamp 的 avg_idle 更新
系列概况
- 标题:[PATCH v3] sched/core: Skip rq->avg_idle update without a valid idle_stamp
- 作者:Shubhang Kaushik (Ampere) sh@gentwo.org
- 版本:v3(关联 v1 2026-07-28、v2 2026-08-06,本封 v3 2026-08-07)
- 规模:单文件改动,
kernel/sched/core.c,净增 6 行(8 insertions / 2 deletions) - 修改文件:
kernel/sched/core.c(函数update_rq_avg_idle()) - Message-ID:20260807-master-v3-1-c328354efed3@gentwo.org
- 完整性:6封邮件齐全(1 封 patch 主送 + 2 封作者 ping + 1 封 Vincent Guittot 引用 + 1 封 Zhan Xusheng 技术分析 + 1 封 tip-bot2 入树通告);最终 commit c6dcd97c8be75f052a1ca52cf79b03e7292962f1 于 2026-09-02 由 Peter Zijlstra 经 tip:sched/urgent 合入。
补丁目的
修复 commit 4b603f1551a73("sched: Update rq->avg_idle when a task is moved to an idle CPU")引入的回归。该提交把 rq->avg_idle 的统计点从 wakeup 路径搬到了 put_prev_task_idle(),但搬的过程中漏掉了 idle_stamp == 0 的有效性检查:
u64 delta = rq_clock(rq) - rq->idle_stamp; // 没保护,直接算
当 idle_stamp 为 0 时,delta 实际上等于 rq_clock(rq)(一个非常大的值),第一次进 update_avg() 就把 avg_idle 推到 clamp,后续两个使用点(fair.c:14504 与 14584)再也压不住,就会倾向于一直发起 newidle balancing,造成额外调度开销。
旧流程的问题
旧代码(被 4b603f1551a73 替换前):
if (rq->idle_stamp) {
u64 delta = rq_clock(rq) - rq->idle_stamp;
update_rq_avg_idle(...);
}
校验和计算绑在一起,stamp 为 0 时根本不会进入统计。
新代码(被本次 patch 修复的目标):
void update_rq_avg_idle(struct rq *rq)
{
u64 delta = rq_clock(rq) - rq->idle_stamp; // 无校验
update_avg(&rq->avg_idle, delta);
if (rq->avg_idle > max)
rq->avg_idle = max;
rq->idle_stamp = 0;
}
把统计函数独立出来后,guard 丢了。
新流程
void update_rq_avg_idle(struct rq *rq)
{
u64 idle_stamp = rq->idle_stamp;
u64 delta, max;
if (!idle_stamp)
return; /* stamp不可信,跳过整次统计 */
delta = rq_clock(rq) - idle_stamp;
max = 2 * rq->max_idle_balance_cost;
update_avg(&rq->avg_idle, delta);
if (rq->avg_idle > max)
rq->avg_idle = max;
rq->idle_stamp = 0;
}
行为变化只是 stamp 为 0 时早退——保留原本的 newidle 记账思路,不去 set_next_task_idle() 里强行 stamp,避免触动 force-idle / proxy-exec 记账路径。
Patch 概览
唯一 hunk 在 kernel/sched/core.c 中 update_rq_avg_idle():
- 新增
u64 idle_stamp = rq->idle_stamp;局部缓存; - 新增
if (!idle_stamp) return;早退分支(v3 删掉了unlikely()); - 把
delta/max改成函数顶部先声明、guard 通过后再赋值。
关键实现
-触发路径:当 sched_balance_newidle() 因为 this_rq->ttwu_pending 提前返回时,idle_stamp 没被设置,rq 进入 idle;随后 wakeup 走 put_prev_task_idle() → update_rq_avg_idle(),stamp 还是 0。
- 类似路径:
newidle_balance()经find_proxy_task()或 force-idling 切到 idle,也漏写 stamp。 - 验收:Prateek Nayak (AMD) Reviewed-by;John Stultz Acked-by;hackbench 多轮跑分相对 v7.2-rc5 平均差约 1.5%,与基线抖动同档。
类比
把 idle_stamp 想成「电表读数」:贴封条时记一下底数,拆封条时算差值就是这一段的用电量。4b603f1551a73 把「贴封条」和「算电费」分到了两个不同的柜台,但搬动时把「检查封条有没有贴」这一步忘在了旧柜台前,于是新柜台经常拿「今天0 点」当底数算电费,每一笔都飙到上限。update_rq_avg_idle() 的 stamp guard 就是把「没贴封条就退单,不收钱」写回到新柜台。
+------------------+ +------------------------+
| wakeup path | | put_prev_task_idle() |
| (old accounting)| | (new accounting path) |
+------------------+ +------------------------+
| |
v v
guard idle_stamp? guard idle_stamp?
| |
Y (kept) N <-- BUG: 被搬走时丢了
| |
v v
delta = clock - stamp delta = clock - 0 (~1e12)
| |
v v
update_avg(...) update_avg(...) ---> clamp 立刻命中 ---> newidle balancing永远打开
Highlight:风险与注意点
idle_stamp在整个内核只有一处赋值(fair.c:14563),位置在ttwu_pendingbail-out 之"下";任何绕开这一行进入 idle 的路径都会复现这个 bug。Zhan Xusheng 在评审中指出,作者 changelog 列举的"三种情形"是窄了,实际可达集合更大。- 第一次命中即饱和:
update_avg()加diff/8,几秒钟启动后rq_clock()已达1e12 量级,单次就把avg_idle推到2*max_idle_balance_cost(1e4~1e5范围)的 clamp 上。 - 后果是"只多不少":两个消费点(
fair.c:14504与14584)据此判断是否值得做 newidle balancing,stamp=0 让它们从不抑制,newidle 路径被常态打开。 - 设计选择:本 patch 选择"在统计入口 guard",而不是"在
set_next_task_idle()里强制 stamp"——后者会污染 force-idle / proxy-exec 的现有记账模型,作者主动回避。 - 验证:hackbench 单负载、多轮平均1.5% 抖动,未见回归;建议补
perf stat -e sched:*抓 newidle 触发率确认收敛。
版本变化
- v1(2026-07-28):首版补丁,引入
idle_stamp早退。 - v2(2026-08-06):新增 Prateek Reviewed-by;在描述中补充
find_proxy_task()与 force-idling 作为无 stamp 进入 idle 的示例;抄送 John Stultz。 - v3(2026-08-07):在描述中把
sched_balance_newidle()/ttwu_pending明确为触发示例;去掉unlikely();新增 John Stultz Acked-by。 - 入树(2026-09-02):经 tip:sched/urgent 合入,commit
c6dcd97c8be75f052a1ca52cf79b03e7292962f1,提交者 Peter Zijlstra。
一句话总结
把 idle_stamp==0 这道闸门装回到搬动后的 update_rq_avg_idle() 入口,避免 rq_clock() 被当成空闲时长、一次性把 avg_idle 推到 clamp、让 newidle balancing 一直开着。