0/8 已展开

LLM 分析

sched/fair:在带宽路径中使用 cfs_rq->h_curr

系列概况

  • 标题:[PATCH 0/2] sched/fair: Use cfs_rq->h_curr in the bandwidth paths
  • 作者:Wanwu Li liwanwu@kylinos.cn
  • 版本:v1(只有一版,提交即被合入)
  • 规模:2 个 patch,共 6 行改动(3 增 / 3 删)
  • 修改文件:kernel/sched/fair.c
  • 代码统计:1 file changed, 3 insertions(+), 3 deletions(-)
  • Message-ID:<20260831101141.391382-1-liwanwu@kylinos.cn>
  • 完整性:完整 thread。包含 0/2 封面说明、1/2 与 2/2 两个 patch、Aaron Lu 的 Reviewed-by 与 Tested-by 回复、作者致谢回信,以及 tip-bot2 在 9 月 2 日发布的两个 sched/urgent 入树通知。

补丁目的

EEVDF 重构为单一 runqueue(85570f10a4c6 sched/eevdf: Move to a single runqueue)之后:

  • cfs_rq->curr 只在根 cfs_rq 上维护,由 set_next_task_fair() / put_prev_task_fair() 设置和清除。
  • cfs_rq->h_curr 才是每一层级 cfs_rq 上"当前正在运行的实体",由 set_next_entity() 在每一层级设置。

带宽控制(throttle / distribute)路径仍然按层级遍历 cgroup。本 series 修正两个在迁移时漏改的、把 cfs_rq->curr 当成"本层级是否有任务在跑"来使用的旧代码点:

  1. throttle_cfs_rq():本层级有任务在跑时,应当请求一整片 sched_cfs_bandwidth_slice() 的运行时间,并通过 task_throttle_setup_work() 注册延迟 throttle 的 task_work。原先的 cfs_rq->curr 在 cgroup 层级永远是 NULL,于是 cgroup 永远只拿到 1ns 运行时间,延迟 throttle 也不会被安装,导致运行中的任务可以跑超配额,直到下一次 pick 才补上 throttle。
  2. distribute_cfs_runtime():用来决定是否在分发带宽前刷新 rq clock 并调用 update_curr() 做运行时间记账。原本的判断条件 cfs_rq->curr 在 cgroup cfs_rq 上恒为假,整段刷新逻辑成了死代码。

旧流程的问题

[cgroup cfs_rq]              [root cfs_rq]
 curr == NULL (always)        curr != NULL
        |                          |
        v                          v
throttle_cfs_rq() check    throttle_cfs_rq() check
target_runtime = 1 (BUG)    target_runtime = slice (OK)
no task_throttle_setup_work install task_throttle_setup_work
        |                          |
        v                          v
task overruns quota until   proper throttling
next pick arms the work

distribute_cfs_runtime():
    if (cfs_rq->curr) -> never fires for cgroup
    -> update_rq_clock() + update_curr() become dead code

distribute_cfs_runtime() 一侧同理:cfs_rq->curr 在 cgroup 层级永远为假,update_rq_clock() + update_curr() 永远不执行。

新流程

[cgroup cfs_rq]              [root cfs_rq]
 h_curr == group entity      h_curr == task / group entity
        |                          |
        v                          v
throttle_cfs_rq() check    throttle_cfs_rq() check
target_runtime = slice (OK) target_runtime = slice (OK)
install task_throttle_setup_work install task_throttle_setup_work

distribute_cfs_runtime()    distribute_cfs_runtime()
if (h_curr) -> update_rq_clock()  if (h_curr) -> update_rq_clock()
            -> update_curr()                  -> update_curr()

简单说:把带宽路径里"本层级是否有实体在跑"的判断,从根专属的 cfs_rq->curr 切到逐层级维护的 cfs_rq->h_curr

关键实现

Patch 1/2 — throttle_cfs_rq()

static bool throttle_cfs_rq(struct cfs_rq *cfs_rq)
{
    struct sched_entity *curr = cfs_rq->h_curr;   /* was cfs_rq->curr */

    /* If cfs_rq->h_curr is still runnable, we are here from an ... */
    ...
    if (curr) {
        /* request sysctl_sched_cfs_bandwidth_slice worth of bandwidth */
        target_runtime = cfs_b->period;
        ...
        task_throttle_setup_work(curr, cfs_rq, false);
    } else {
        target_runtime = 1;
    }
}

要点:当本层级确实有在跑实体(h_curr 非空)时,把目标运行时间设成一个完整 slice 并安排延迟 throttle;否则只给 1ns 占位。

Patch 2/2 — distribute_cfs_runtime()

static bool distribute_cfs_runtime(struct cfs_bandwidth *cfs_b)
{
    ...
    for_each_throttled_cfs_rq(cfs_b, cfs_rq) {
        ...
        if (cfs_rq->h_curr) {   /* was cfs_rq->curr */
            update_rq_clock(rq);
            update_curr(cfs_rq);
        }
    }
}

要点:只有在 throttled 层级当前还有实体真正在跑(处于延迟 throttle 窗口里)的时候,才刷新 rq clock 并做运行时间记账。

Patch 概览

+------+-------------------------------+--------------------------+--------------------------------+
| Patch | Location                      | Change                    | Effect                         |
+------+-------------------------------+--------------------------+--------------------------------+
| 1/2  | throttle_cfs_rq()             | curr = cfs_rq->curr       | cgroup quota exhausted:        |
|      |   curr = cfs_rq->curr         |   -> cfs_rq->h_curr       |   correctly request a full     |
|      |   comment updated             |                            |   slice and arm deferred       |
|      |                               |                            |   throttle task_work           |
+------+-------------------------------+--------------------------+--------------------------------+
| 2/2  | distribute_cfs_runtime()      | if (cfs_rq->curr)         | runtime consumed in the         |
|      |   if (cfs_rq->curr)           |   -> if (cfs_rq->h_curr)  |   deferred-throttle window is |
|      |                               |                            |   docked before redistribution |
+------+-------------------------------+--------------------------+--------------------------------+

类比

把 cgroup 的层级 runqueue 想成一座办公楼:

  • cfs_rq->curr 是"正门接待台显示的当前来访者",只有一楼(根 cfs_rq)有人值班,其它楼层永远是空的。
  • cfs_rq->h_curr 才是每层楼的楼层接待员,看到的才是"本层此刻正在开会的那个人"。

带宽路径里,旧代码只看正门显示(curr),于是二楼的会议室真在开会,却被误判为"没人",配额没按时扣掉,会议室用着用着就超支了。改成看楼层接待员(h_curr)后,二楼也能正确判断有人,节流和记账都恢复工作。

Highlight:风险与注意点

  1. cfs_rq->h_curr 的 TODO:commit 85570f10a4c6 的注释里留了 TODO,希望以后彻底干掉 cfs_rq->h_curr,统一回到单一 runqueue 模型。本系列是过渡方案:如果上游后续做了 h_curr 的整合重构,需要把这 6 行改动一并重新审视。
  2. 回归测试范围:Aaron Lu 用一个 affined 到单 CPU 的 nop 任务复现了周期性超配额;这是个相对薄的负载模型,建议补充多核、嵌套 cgroup 以及 CPU hotplug / cpufreq 切换场景下的带宽控制回归。
  3. 公平 vs 及时性distribute_cfs_runtime() 的刷新只是在延迟 throttle 窗口里补登运行时间,作者明确指出当前靠 28ad5427682b 在 unthrottle 时无条件 update_curr() 才不至于出现正确性漏洞——属于"被另一个 patch 兜着"的隐含依赖,需要持续跟踪其有效性。
  4. 审计完整性:作者声明已经审计过 kernel/sched/fair.c 中所有 cfs_rq->curr 引用,确认只有这两处仍按层级跑。其余读者要么只在根 cfs_rq,要么已经走 h_curr。审阅者应抽查这条断言。

版本变化

只有一个 v1,无 v2。但 thread 里能直接看到一次"提交 -> 进入 tip/sched/urgent"的快路径:

  • 8/31:作者发 v1;Aaron Lu 当晚给出 Reviewed-by,并补充 Tested-by(nop 单 CPU 亲和复现 + 验证修复后不再超配额)。
  • 9/1:作者回信确认这就是出问题的"延迟 throttle 逃逸"路径。
  • 9/2:Peter Zijlstra 通过 tip 把两个 patch 分别以 commit f8610c57f4078c63d1d4e2f3d7134f3dc1768403(patch 1/2)和 b038383526d8c7883ea0486dd1911102b6dda414(patch 2/2)推入 sched/urgent 分支。

一句话总结

EEVDF 单 runqueue 化后 cfs_rq->curr 只在根 cfs_rq 上有意义,作者把 bandwidth 路径里两处仍按层级跑的 cfs_rq->curr 换成 cfs_rq->h_curr,让 cgroup 配额耗尽能正确请求整片 slice 并安装延迟 throttle,同时恢复 distribute_cfs_runtime() 中本应执行的 clock refresh 与运行时间记账。