sched discussion
[RFC PATCH 0/1] sched/eevdf: Curb wakeup preemption further
LLM 分析
EEVDF 唤醒抢占收敛:RFC 与后续组调度缺陷跟进
系列概况
- 标题: [RFC PATCH 0/1] sched/eevdf: Curb wakeup preemption further
- 作者: K Prateek Nayak (AMD)
- 版本: RFC v1(1 个 patch),另有 2026-07 跟进报告
- 规模: 1 patch,2 files changed,21 insertions(+),4 deletions(-)
- 修改文件: kernel/sched/fair.c、kernel/sched/features.h
- 代码统计: +21 / -4
- Message-ID: 20240325060226.1540-1-kprateek.nayak@amd.com(cover),20240325060226.1540-2-kprateek.nayak@amd.com(patch)
- 完整性: 完整 cover + patch + 多轮维护者/工程师回复;2026 年 Chen Jinghuang 在 backport 时报告 RUN_TO_PARITY_WAKEUP 在 cgroup 拓扑下产生严重饥饿
补丁目的
自 EEVDF 合入(commit 147f3efaa241)以来,DeathStarBench 在单/多 CCX 上的吞吐量出现两位数下降。pick_eevdf() 对 curr 做的 eligibility 检查,会在新唤醒的、带正 lag 的任务把 rq->avg_vruntime 往后拉时立刻判当前任务 ineligible,导致 NGINX 这种延迟敏感服务被频繁非自愿抢占、丢掉连接,最终拖慢整条服务链。目标:在唤醒抢占路径上为 curr 设立「slice 未耗尽即免检 eligibility」的窗口,恢复 v6.5 之前 CFS 时代的吞吐水平。
旧流程的问题
pick_eevdf()直接对curr跑entity_eligible();- 唤醒到来时正 lag 推低
avg_vruntime,curr立即 ineligible; check_preempt_wakeup_fair()据此立即抢占 NGINX;- NGINX 丢连接 → DeathStarBench 延迟平均上升约 14.6%;
- CFS 时代由
wakeup_granularity_ns提供「刚开跑不久别抢占」的缓冲,EEVDF 中该保护被绕过。
新流程
新增 sched_feat RUN_TO_PARITY_WAKEUP,仅在 wakeup_preempt == true、且 curr->vlag == curr->deadline(即自选中起尚未消耗 slice)时跳过对 curr 的 eligibility 检查,从而把旧版 CFS 的「唤醒粒度」语义在 EEVDF 上重现。
if (curr && !curr->on_rq)
curr = NULL;
if (curr &&
!(sched_feat(RUN_TO_PARITY_WAKEUP) && wakeup_preempt &&
curr->vlag == curr->deadline) &&
!entity_eligible(cfs_rq, curr))
curr = NULL;
Patch 概览
pick_eevdf(cfs_rq)改为pick_eevdf(cfs_rq, bool wakeup_preempt)pick_next_entity()调用处传falsecheck_preempt_wakeup_fair()调用处传truefeatures.h新增SCHED_FEAT(RUN_TO_PARITY_WAKEUP, true)- cover letter 中还附带一段无关的 lag clamp 实验性改动作为前置探索
关键实现
核心集中在 pick_eevdf() 对 curr 的替换判断上:
static struct sched_entity *pick_eevdf(struct cfs_rq *cfs_rq, bool wakeup_preempt)
{
struct sched_entity *se = __pick_first_entity(cfs_rq);
struct sched_entity *curr = cfs_rq->curr;
/* on_rq 失效立即让位 */
if (curr && !curr->on_rq)
curr = NULL;
/* 唤醒抢占路径:slice 还没耗尽就免检 eligibility */
if (curr &&
!(sched_feat(RUN_TO_PARITY_WAKEUP) && wakeup_preempt &&
curr->vlag == curr->deadline) &&
!entity_eligible(cfs_rq, curr))
curr = NULL;
/* 后续优先 next / curr / se 的常规选择 */
...
}
curr->vlag == curr->deadline 表示该任务被选中那一刻的「承诺 slice 起点」,尚未因运行而推进。
类比
把 rq 看作公交站台:旧流程下每个候车乘客只要觉得自己比司机认可的平均「更靠前」就让司机立刻下台;新流程给司机划了一块「刚刚被指派还不到一站地」的保护区,这段时间内即便新乘客上车,也不赶司机下车,从而让车上乘客(NGINX)能持续应答请求。代价是新候车的乘客可能多等一站,但对延迟敏感服务的整体吞吐是合算的。
Highlight:风险与注意点
- 延迟换吞吐:被唤醒任务等待时间可能变长,但 hackbench / tbench / stream 等标准 benchmark 在 2S EPYC 上未见持续回归。
- 图示解读争议:Peter Zijlstra 指出 cover letter 的 vruntime 时间轴图上下两条线未对齐,难以直观看出
avg_vruntime后移的效果,应把 NGINX 的 vruntime 对齐后再画 Service 与 avg_vruntime 的相对位移。 base_slice_ns防护不足:Madadi Vineeth Reddy 质疑base_slice_ns是否已足够,Prateek 澄清它影响的是 lag 钳位幅度,而 eligibility 判定仍由vruntime与avg_vruntime之差决定,因此仍会出现频繁抢占。- 另一种思路:Youssef Esmat 分享 Google 内部在所有路径禁用 eligibility 也能拿到类似增益,备选讨论在 OSPM 进行中。
- 2026 跟进缺陷(关键):Chen Jinghuang 在 v7.2-rc2 backport 时发现 cgroup 拓扑下 A1 长期独占 CPU:CPU 20 上兄弟任务 A2/A3 频繁 sleep/wake → gse(A) 反复
update_cfs_cgroup/reweight_entity→entity_lag()触发 vlag 钳位 → A1->vruntime 单向漂移(vsavg_vruntime)→protect_slice()永远 true →pick_eevdf()一直返回 curr → 抢占彻底失效。 - 数学不对称:
rescale_entity()中deadline/vprot按权重重新缩放,但vruntime单向漂移导致vruntime < vprot恒成立,破坏 EEVDF 的不变量,引发死锁式的饥饿。 - 跟进方向:在不丢 RUN_TO_PARITY_WAKEUP 吞吐收益的前提下,需要避免 cgroup reweight 反复触发 vlag 钳位导致的
protect_slice()假阳性,可能需要在reweight路径上重置vprot或调整 vlag 钳位的窗口。
版本变化
仅一版 RFC,无 v2/v3;2026-07 的 [Question] PATCH 是同一特性的 bug 报告而非新版本,但展示了 vprot/vruntime 在 vlag clamp + reweight 下的不对称副作用。
一句话总结
在 EEVDF pick_eevdf() 的唤醒抢占路径上为 curr 加「slice 未耗尽即免检 eligibility」窗口,避免带正 lag 的唤醒任务频繁抢断 NGINX;2026 年 backport 时被发现该窗口在 cgroup reweight 与 vlag 钳位叠加下会让 protect_slice() 恒真、引发严重饥饿,仍待社区进一步修复。
+----------------------------------------------------------+
| pick_eevdf(cfs_rq, wakeup_preempt) |
+----------------------------------------------------------+
|
+---------------+---------------+---------------+
| | | |
v v v v
+-------------+ +----------------+ +------------------+
| curr on_rq? | | wakeup_preempt | | entity_eligible? |
| no -> NULL | | && R2P | | no -> NULL |
+-------------+ | && vlag==deadl| +------------------+
| ? yes -> keep |
| curr |
+----------------+
no yes
| |
v v
curr = NULL keep curr
(skip eligibility)
|
v
return se or curr
CPU 0 (after Chen Jinghuang's bug report)
+----------------------------------------+
| gse(A) -> vlag hits clamp limit |
| -> vruntime drifts one-way |
| -> protect_slice() = true |
| -> pick_eevdf() -> curr (A1) |
| -> A1 hogs CPU 0 forever |
+----------------------------------------+
^
| triggered by
|
CPU 20
+----------------------------------------+
| A2/A3 frequent sleep/wake (10us/50us) |
| -> update_cfs_cgroup() repeatedly |
| -> reweight_entity(gse(A)) each time |
+----------------------------------------+