sched discussion
[PATCH] sched/fair: Fix flat hierarchy
LLM 分析
sched/fair:修复 flat hierarchy 下 update_curr 漏更新问题
系列概况
- 标题:
[PATCH] sched/fair: Fix flat hierarchy - 作者:Vincent Guittot vincent.guittot@linaro.org
- 版本:单 patch(v1),后续经讨论调整后被拆为
sched/urgent+sched/core两份,最终通过 tip-bot 推入tip: sched/core - 规模:单文件
kernel/sched/fair.c,1 个 patch(5 insertions(+), 1 deletion(-));合并版相比原始 patch 增加了一个内联 helper,最终 diff 也只涉及 fair.c - 修改文件:
kernel/sched/fair.c - 代码统计:v1
+5/-1;合并到 tip 的最终版在requeue_delayed_entity中删除 1 行update_curr,新增内联函数update_curr_eevdf,并在enqueue_task_fair和__dequeue_task中各加一行调用 - Message-ID:首发
20260812125039.1717249-1-vincent.guittot@linaro.org;合并通知178673615753.442315.4575538493973476588.tip-bot2@tip-bot2;Commit-ID68e37487810a3da43c48340fab7a55b3b6efdae3 - 完整性:完整。包含 patch、作者与维护者多轮讨论、拆分
sched/urgent/sched/core的决定,以及最终 tip-bot 推送通知
补丁目的
自 85570f10a4c6 ("sched/eevdf: Move to a single runqueue") 之后,EEVDF 把所有调度决策集中在 rq->cfs 这一条单一 runqueue 上,cgroup 树只是用于统计与记账的"扁平层次(flat hierarchy)"。在这种布局下,update_curr() 走的是 cfs_rq->h_curr(hierarchy curr)这条路径,而 h_curr 是 cgroup 链上的中间层标记,并不会真正指向 rq->cfs.curr,因此对根 cfs_rq 的 avg_vruntime 不会被刷新。
后果是:在 fair task 入队/出队时,新任务的 vruntime 与当前任务(curr)的最新 vruntime 之间的"上一次执行段落"被忽略。Vincent 给出的具体例子:cgroup G0 里有常驻任务 TA(一直 running),cgroup G1 里有 cyclictest 短任务 TB。TB 入队时 TA 的 vruntime 还没有被更新(要等下一次 tick),TB 的 lag 不断累积直至 clamp 上限;等到 TA 终于被 update_curr 刷新时,它最近一段执行才把正向 lag "还" 给 TB。这会让短任务的延迟监控(如 cyclictest)看到虚高的 latency。
旧流程的问题
入队路径在 flat hierarchy 下不再经过 enqueue_hierarchy() 把根实体的 lag 推到叶子,于是 update_curr() 即使被调用,也无法覆盖到 rq->cfs 根层。__dequeue_task 里 update_curr(cfs_rq_of(se)) 同样用的是 se 所在的 cfs_rq,而不是当前正在运行的 cfs_rq->curr。结果:
- 入队:新任务的 vruntime 与 curr 的 vruntime 之间缺少最近执行段。
- 出队:正在离开的任务
se并不一定是cfs_rq->curr,调用update_curr(cfs_rq_of(se))不会更新真正在跑的那个实体。 - cyclictest 误报:短任务观测到的 lag 一直上涨到 clamp 上限。
Old enqueue_task_fair (simplified)
task p arrives in G1
|
v
enqueue_task_fair -> enqueue_entity(G1)
|
v
update_curr(cfs_rq_of(se)) <-- se is in G1, but curr (TA) is in G0
|
v
update_entity_lag(G1) <-- only G1's avg gets refreshed
|
v
root cfs_rq (rq->cfs) avg_vruntime NOT refreshed
|
v
TB is placed with stale avg -> lag grows toward clamp
新流程
入队和出队时显式地把"先刷新 curr 的 vruntime"这一步提到最前面,并只在 cfs_rq->curr 存在时执行。维护者 Peter Zijlstra 在评审中要求把这段重复逻辑抽成内联函数 update_curr_eevdf(cfs_rq),避免在多个调用点散落。
New enqueue_task_fair / __dequeue_task (merged form)
task p arrives in G1
|
v
update_curr_eevdf(cfs_rq)
|
|-- if (!cfs_rq->curr) return
|-- update_curr(cfs_rq_of(cfs_rq->curr)) <-- refreshes true curr
v
enqueue_entity -> update_entity_lag
|
v
root cfs_rq avg_vruntime now reflects last exec phase of TA
|
v
TB is placed with fresh avg -> lag stays bounded
Patch 概览
v1(Vincent 首发):直接在 enqueue_task_fair 顶部分支前和 __dequeue_task 的 clear_buddies 后插入 if (cfs_rq->curr) update_curr(cfs_rq_of(cfs_rq->curr));。
最终合入 tip 的版本(commit 68e37487):抽出 update_curr_eevdf() 内联函数;requeue_delayed_entity 中原本的 update_curr(cfs_rq) 被移除(被上层 helper 覆盖);enqueue_task_fair 增加 update_curr_eevdf(cfs_rq);__dequeue_task 中原本的 update_curr(cfs_rq_of(se)) 替换为 update_curr_eevdf(cfs_rq)。
关键实现
最终合入的关键代码大致如下:
/* Update curr's vruntime before placing entity or updating lag */
static inline void update_curr_eevdf(struct cfs_rq *cfs_rq)
{
if (!cfs_rq->curr)
return;
update_curr(cfs_rq_of(cfs_rq->curr));
}
enqueue_task_fair(...)
{
...
update_curr_eevdf(cfs_rq);
...
}
static bool __dequeue_task(...)
{
...
clear_buddies(cfs_rq, se);
update_curr_eevdf(cfs_rq); /* was update_curr(cfs_rq_of(se)) */
update_entity_lag(cfs_rq, se);
...
}
Peter 在评审中指出,原 v1 patch 在描述里写"非重叠 cgroup 层级",但实际上 update_curr 走的是 h_curr,逻辑上并不直接作用于 root cfs_rq。这种"语义不清的注释"会让读者误以为问题是 cgroup 路径引起的。最终合并版用 update_curr_eevdf 命名清晰地把意图表达成"在入队/出队时,把 EEVDF 真正需要的 curr vruntime 先更新"。
Peter 还观察到:在 requeue_delayed_entity 这种 delayed task 的主路径上,原本也有一处 update_curr(cfs_rq);合并版把这处移除,因为上层 enqueue_task_fair 已经用 update_curr_eevdf 覆盖。这避免了重复刷新同一份统计。
类比
把 CFS 的 runqueue 想成一家公司门口的排队叫号机。update_curr 是"刷新正在被服务那位客户的等待时间"。在引入 flat hierarchy 之前,每个部门(cgroup)都有自己一台叫号机,刷新时各自的队都能同步到最新状态;引入 flat hierarchy 之后,调度器只有"全公司唯一一台"叫号机放在 root,而中间部门墙上贴的"当前正在服务"标签(h_curr)只是装饰,不是真正刷新的来源。
旧流程:每次有新客户入队时,只刷新了"他所在部门"那张装饰标签,全公司的叫号机屏幕没动。
新流程:入队时先抬头看一眼"现在真正被服务的是谁"(cfs_rq->curr),把全公司那台叫号机刷新一次,再决定把新客户排在哪。
update_curr_eevdf 就像前台一句固定话术:"请先更新大屏,再排号",把这句统一在入口处执行一次,避免每个窗口各自重复。
Highlight:风险与注意点
- avg_vruntime 偏差导致 cyclictest 误报:未修复前 short task 的 lag 会冲到 clamp 上限,可能让用户误以为系统调度延迟很高。补丁合入后应回归验证。
h_curr与curr的差异容易混淆:维护者最初评审时就对"非重叠 cgroup 层级"的说法感到困惑。后续 patch 文档应明确写"update_curr走h_curr,不会到达 root"。requeue_delayed_entity中重复调用:v1 没有动requeue_delayed_entity,但那里原本也有update_curr(cfs_rq);最终合并版把它移除,避免重复统计。需要注意任何外部依赖这条路径的代码(例如自定义 scheduler 测试用例)是否依赖那里那次 update。- 拆分
sched/urgent/sched/core:Peter 提议把requeue_delayed_entity的那一处单独放进sched/urgent(作为更紧急的修复),主补丁进sched/core。任何 backport 需要分别挑选这两个分支的对应提交。
版本变化
- v1 → 合并版(commit
68e37487):- 把两处重复的
if (cfs_rq->curr) update_curr(...)抽成内联 helperupdate_curr_eevdf()。 - 移除
requeue_delayed_entity()中原有的update_curr(cfs_rq),由上层enqueue_task_fair的update_curr_eevdf覆盖。 __dequeue_task中update_curr(cfs_rq_of(se))替换为update_curr_eevdf(cfs_rq),使被更新的是真正的 curr 而非被出队的se。Fixes:tag 保留85570f10a4c6 ("sched/eevdf: Move to a single runqueue")。
- 把两处重复的
一句话总结
flat hierarchy 下 update_curr 走 h_curr 而碰不到 root cfs_rq,导致入队/出队时 avg_vruntime 用旧值,short task 的 lag 一直冲到 clamp 上限;补丁通过 update_curr_eevdf() 内联 helper 在入队/出队入口强制刷新真正 curr 的 vruntime,并清理掉 requeue_delayed_entity 中的重复调用。