0/4 已展开

LLM 分析

sched_ext:修复 nohz_full 上有限时间片丢失 tick

系列概况

  • 标题:[PATCHSET sched_ext/for-7.2-fixes] sched_ext: Fix finite-slice ticks on nohz_full
  • 作者:Andrea Righi <arighi@nvidia.com>
  • 版本:v1(后续演进至 v3 并合入主线)
  • 规模:2 个实际 patch
  • 修改文件:4 个(kernel/sched/ext/ext.c 与 3 个 selftest 文件)
  • 代码统计:+386/-3
  • Message-ID:20260706162819.650155-1-arighi@nvidia.com
  • 完整性:v1 的 cover 与 2/2 patch 完整,b4 共解析 7 封邮件,无缺失或 tip-bot2 污染;v3 也完整取得 2/2

一句话总结:有限 slice 的 EXT 任务必须主动恢复调度 tick;最终 v3 还修复了 finite → idle → finite 和最后一个 EXT 任务离开时的反向状态残留,并已进入主线。

补丁目的

nohz_full CPU 会尽量停止周期 tick,但 sched_ext 的有限时间片仍依赖 tick 扣减、到期和触发重新调度。问题源自 set_next_task_scx()__schedule() 更新 rq->curr 之前运行:它若通过通用函数检查 rq->curr,看到的是 outgoing task(常为 idle),而非刚选中的有限-slice EXT 任务,因而可能错误地允许 tick 保持关闭。

回归由 22a920209ab6 ("sched_ext: Implement tickless support") 引入。v1 试图在有限 slice 分支直接设置 TICK_DEP_BIT_SCHED,并增加 kselftest;评审随后发现 v1 只在 slice 类型转换时执行,覆盖不完整。

旧流程的问题

旧代码只在 SCX_SLICE_INF 与有限 slice 的类型状态变化时更新 SCX_RQ_CAN_STOP_TICK,然后调用 sched_update_tick_dependency(rq)

  • infinite/idle → finiterq->curr 仍是 outgoing task,通用判断可能不启动 tick,任务跑过 slice。
  • finite → idle → finite:idle enqueue/dequeue 路径可能清掉依赖,但 SCX_RQ_CAN_STOP_TICK 仍记录“有限”;下一有限任务因类型未变化而跳过更新。
  • finite → idle:最后一个 EXT 任务出队时,sub_nr_running()rq->curr 切换前评估依赖,旧有限 slice 又可能让 tick 在真正 idle 后仍保持开启。

这三个问题本质相同:控制流处在 context-switch 中间态,却把 rq->curr 当成最终状态来源。

问题流程

无限-slice EXT 运行,允许停止 tick
        │
        ▼
任务阻塞,CPU 进入 idle,周期 tick 已关闭
        │ finite EXT 被唤醒,wakeup/resched IPI 唤醒 CPU
        ▼
进入 __schedule(),持有 rq->lock
        │
        ▼
pick_next_task() 选出 next = finite EXT
        │
        ▼
set_next_task_scx(rq, next)
        ├─ next 是有限 slice
        └─ rq->curr 仍是 idle(尚未更新)
        │
        ▼
旧代码调用 sched_update_tick_dependency(rq)
        └─ 根据 rq->curr = idle 判断“可以停 tick”
        │
        ▼
__schedule() 才执行 rq->curr = next
        │
        ▼
有限-slice EXT 运行,但 tick 仍关闭
        ├─ task_tick_scx() 不执行
        ├─ scx.slice 不扣减
        └─ slice 到期不触发 resched_curr()

新流程

选中下一个 EXT 任务
        |
        +-- slice == INF -------------------------------+
        |    仅在状态转换时设置 CAN_STOP_TICK           |
        |    调用通用依赖评估,并刷新 load average       |
        |                                               v
        +-- slice != INF --> 清 CAN_STOP_TICK --> nohz_full CPU?
                                      |                 |
                                      | yes             | no
                                      v                 v
                         无条件 set TICK_DEP_BIT_SCHED   正常周期 tick

最后一个 EXT 任务出队:rq->scx.nr_running == 0
        -> scx_can_stop_tick() 忽略仍指向 outgoing EXT 的旧 slice
        -> 由通用调度器按即将进入的 idle/其他调度类重新评估并清依赖

Patch 概览

Patch核心改动
1/2修改 set_next_task_scx();最终 v3 对每个有限-slice 任务直接置位 tick 依赖,并在 scx_can_stop_tick() 处理 nr_running == 0
2/2新增 nohz_tick BPF/用户态 selftest,构造跨 idle 的 infinite→finite 与 finite→finite 序列

关键实现

1. 有限 slice 无条件声明 tick 依赖

最终 v3 将 infinite/finite 两条路径拆开。有限路径不再调用会观察 outgoing rq->curr 的通用判断,而是执行:

if (tick_nohz_full_cpu(cpu_of(rq)))
	    tick_nohz_dep_set_cpu(cpu_of(rq), TICK_DEP_BIT_SCHED);

这段代码位于类型转换判断之外,所以每次选中有限-slice EXT 任务都会重新声明依赖,覆盖 finite → idle → finite

2. SCX_RQ_CAN_STOP_TICK 与 load average 分工

  • infinite 路径仅在标志从“不可停”变为“可停”时调用 sched_update_tick_dependency()update_other_load_avgs()
  • finite 路径只在标志发生变化时刷新 load average,但每次都重新设置 tick 依赖。
  • bypass 模式总会分配有限 slice,因此 infinite 路径调用通用判断时不存在“正在 bypass 的无限 slice”组合。

3. 修正最后一个 EXT 任务离开的反向窗口

最终 scx_can_stop_tick() 在确认当前类为 EXT 后增加:

if (!rq->scx.nr_running)
	    return true;

它不是宣称 CPU 必然停 tick,而是让 SCX 不再以 outgoing task 的陈旧 slice 阻止通用调度器清理依赖;若随后又从 DSQ 选中任务,set_next_task_scx() 会按 incoming task 重新建立正确状态。

4. selftest 如何击中竞态

测试选择一个允许使用的 nohz_full CPU,并把 controller 移到独立 housekeeping CPU:先运行 infinite worker、暂停并等待目标 CPU idle,再运行有限 worker并要求至少收到 3 次 ops.tick();随后终止它、再次 idle,再启动第二个有限 worker,专门覆盖 finite → idle → finite。没有合适 CPU 拓扑时测试为 SKIP。

worker 设置 PR_SET_PDEATHSIG=SIGKILL 并复核父 PID,避免测试进程异常退出后留下永久自旋的孤儿进程。

类比

把 tick 当作厨房定时器:无限 slice 是慢炖,可以关闹钟;有限 slice 是煎牛排,每次下锅都必须重新按下计时键。旧实现根据“上一口锅”和“菜品类型是否变化”决定是否按键,空锅期间若有人关闭定时器,下一块同样的牛排便会漏计时。v3 改成只要新菜需要限时就无条件按键;灶台彻底空了,则不再让上一道菜的旧标签阻止关闭定时器。

Highlight:风险与注意点

  • v1 的修复不完整;Tejun Heo 与 Sashiko 指出的 finite → idle → finite 漏洞在 v2 修复,不能把原 URL 的 diff 直接视作最终方案。
  • tick_nohz_dep_set_cpu() 位于选中有限任务的热路径;它只对 nohz_full CPU执行,但每次 finite selection 都会断言依赖,这是正确性换取的少量固定开销。
  • nr_running == 0 判断依赖 sched_ext 的 runqueue 记账与实际可运行 EXT 任务保持一致;以后修改 enqueue/dequeue 或 DSQ 转移路径时应保留这一不变量。
  • selftest 需要一个 allowed nohz_full CPU 和另一个 housekeeping CPU;普通配置会 SKIP,因此 CI 必须包含专门的 CONFIG_NO_HZ_FULL=ynohz_full=all 环境才真正覆盖回归。
  • v1 的 b4 DKIM 校验显示 BADSIG: DKIM/nvidia.com,这是邮件认证告警,不是 patch 缺失;两封 patch 本身均完整。
  • review 状态明确:v3 中 Tejun 回复 Applied 1-2 to sched_ext/for-7.2-fixes;最终代码为 4ec10f38ff90,并由 merge f7574d3f906a 于 2026-07-13 进入 torvalds/linux。

版本变化

版本重要变化
v1初始修复,仅在 slice 类型转换时处理 tick;测试只覆盖 infinite→idle→finite;存在孤儿 worker 风险
v2每次选择 finite 都重设依赖;nr_running == 0 时忽略陈旧 outgoing 状态;补 finite→idle→finite 测试及 PDEATHSIG 清理
v3按 Tejun 建议拆开 infinite/finite 分支,使 slice 类型只判断一次并重写注释;功能逻辑延续 v2
合入Tejun 应用 v3 的 1–2/2;主线 commit 4ec10f38ff90 与 v3 核心 patch 一致,随后进入 v7.2-rc3 fixes merge

一句话总结

该系列把 sched_ext 的 tick 决策从“不可靠的 outgoing 状态”改为“incoming 有限 slice 的明确需求”,并对 EXT runqueue 清空的反向路径消除陈旧状态;应以已合入的 v3 而非原链接中的 v1 作为正确实现基线。