0/11 已展开

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 时代的吞吐水平。

旧流程的问题

  1. pick_eevdf() 直接对 currentity_eligible()
  2. 唤醒到来时正 lag 推低 avg_vruntimecurr 立即 ineligible;
  3. check_preempt_wakeup_fair() 据此立即抢占 NGINX;
  4. NGINX 丢连接 → DeathStarBench 延迟平均上升约 14.6%;
  5. 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() 调用处传 false
  • check_preempt_wakeup_fair() 调用处传 true
  • features.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:风险与注意点

  1. 延迟换吞吐:被唤醒任务等待时间可能变长,但 hackbench / tbench / stream 等标准 benchmark 在 2S EPYC 上未见持续回归。
  2. 图示解读争议:Peter Zijlstra 指出 cover letter 的 vruntime 时间轴图上下两条线未对齐,难以直观看出 avg_vruntime 后移的效果,应把 NGINX 的 vruntime 对齐后再画 Service 与 avg_vruntime 的相对位移。
  3. base_slice_ns 防护不足:Madadi Vineeth Reddy 质疑 base_slice_ns 是否已足够,Prateek 澄清它影响的是 lag 钳位幅度,而 eligibility 判定仍由 vruntimeavg_vruntime 之差决定,因此仍会出现频繁抢占。
  4. 另一种思路:Youssef Esmat 分享 Google 内部在所有路径禁用 eligibility 也能拿到类似增益,备选讨论在 OSPM 进行中。
  5. 2026 跟进缺陷(关键):Chen Jinghuang 在 v7.2-rc2 backport 时发现 cgroup 拓扑下 A1 长期独占 CPU:CPU 20 上兄弟任务 A2/A3 频繁 sleep/wake → gse(A) 反复 update_cfs_cgroup/reweight_entityentity_lag() 触发 vlag 钳位 → A1->vruntime 单向漂移(vs avg_vruntime)→ protect_slice() 永远 true → pick_eevdf() 一直返回 curr → 抢占彻底失效。
  6. 数学不对称rescale_entity()deadline/vprot 按权重重新缩放,但 vruntime 单向漂移导致 vruntime < vprot 恒成立,破坏 EEVDF 的不变量,引发死锁式的饥饿。
  7. 跟进方向:在不丢 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   |
+----------------------------------------+