sched-ext discussion
[PATCHSET v3 sched_ext/for-7.2-fixes] sched_ext: Fix finite-slice ticks on nohz_full
LLM 分析
sched_ext: Fix finite-slice ticks on nohz_full — 系列分析
系列基线信息
- 标题: [PATCHSET v3 sched_ext/for-7.2-fixes] sched_ext: Fix finite-slice ticks on nohz_full
- 作者: Andrea Righi arighi@nvidia.com
- 目标分支:
sched_ext/for-7.2-fixes(指向 7.2 修复窗口) - 版本: v3(共经历 v1 → v2 → v3)
- 规模: 2 个 patch + cover letter
- PATCH 1/2:
kernel/sched/ext/ext.c(核心修复,+35 / -12) - PATCH 2/2: 新增
nohz_tick.bpf.c、nohz_tick.c、Makefile 注册(+413 行 selftest)
- PATCH 1/2:
- 消息列表:
- Cover letter — Message-ID:
20260708074812.1041434-1-arighi@nvidia.com - PATCH 1/2 — Message-ID:
2026070807484-2-arighi@nvidia.com - PATCH 2/2 — Message-ID:
20260708074812.1041434-3-arighi@nvidia.com - Tejun Heo ACK — Message-ID:
3e37aecc051e1e272f4a52ca37f58aa3@kernel.org
- Cover letter — Message-ID:
- 来源频道: sched-ext(lore.kernel.org)
- 结果: Tejun Heo 已
Applied 1-2 to sched_ext/for-7.2-fixes
明确目的
本系列修复了 sched_ext 在 nohz_full CPU 上的 tick 依赖在任务切换之间会"残留"导致 fine-grained accounting 失准 的 bug。
具体而言:
- infinite → finite 跨 idle 切换:当目标 CPU 上一个无限时片 EXT 任务离开并 idle 后,新到达的有限时片 EXT 任务本应让 tick 重新启动,但
set_next_task_scx()检查的是"出队任务"的 slice 状态,而不是"新入队任务"的状态,于是 tick 仍保持关闭,slice 不会被 scheduler tick 抢占。 - finite → idle:当最后一个有限时片 EXT 任务离开、
nr_running归零但rq->curr还指向旧的 EXT 任务时,SCX_RQ_CAN_STOP_TICK标志可能让 tick 误以为还需要保持,导致 CPU 真正 idle 后 tick 不能停掉。 - finite → finite 跨 idle 切换:当上一次也是有限时片 EXT 任务,本次与上次 slice 类型相同,
SCX_RQ_CAN_STOP_TICK不会重新评估,导致 tick 依赖丢失。
修复目标是让 tick 依赖与"当前即将运行的 EXT 任务"的 slice 类型严格对齐,并在 runqueue 上已无 EXT 任务时正确清理残留状态。
遍历代码
PATCH 1/2 — kernel/sched/ext/ext.c
改写 set_next_task_scx()(tick dep 路径拆分)
原代码用 if ((p->scx.slice == SCX_SLICE_INF) != (bool)(rq->scx.flags & SCX_RQ_CAN_STOP_TICK)) 把"无限"和"有限"两条路径合并在一个三元条件里,意味着只有 slice 类型发生翻转时才会触发更新,同类型连续切换会跳过。
v3 改成两条互不交叉的分支:
if (p->scx.slice == SCX_SLICE_INF) {
// 无限时片分支:依赖 !SCX_RQ_CAN_STOP_TICK 才能进入
if (!(rq->scx.flags & SCX_RQ_CAN_STOP_TICK)) {
// bypass 路径下永远不会有 INF slice(注释里说明了)
// 这里把出队任务喂给 sched_update_tick_dependency()
sched_update_tick_dependency(rq);
update_other_load_avgs(rq);
}
} else {
// 有限时片分支:依赖 SCX_RQ_CAN_STOP_TICK 才能进入
if (rq->scx.flags & SCX_RQ_CAN_STOP_TICK) {
rq->scx.flags &= ~SCX_RQ_CAN_STOP_TICK;
update_other_load_avgs(rq);
}
// 关键修复:只要选定的是 finite EXT 任务,
// 且在 nohz_full 上,就无条件声明 tick 依赖
if (tick_nohz_full_cpu(cpu_of(rq)))
tick_nohz_dep_set_cpu(cpu_of(rq), TICK_DEP_BIT_SCHED);
}
这样:
- 无限 → 无限:不会做任何操作(已经在 INF 路径里了)。
- 无限 → 有限:清除 `SCX_RQ_CAN_STOP_TICK`,并在 nohz_full 上**强制**声明 tick 依赖。
- 有限 → 有限:原本会被"同类型"判断跳过,现在因为是独立分支且不再读 `SCX_RQ_CAN_STOP_TICK` 作为门控,所以会重新评估;并且由于 `SCX_RQ_CAN_STOP_TICK` 被清掉了,本就会进入"重新声明 tick"路径。
- 有限 → 无限:进入 INF 分支后看到 `!SCX_RQ_CAN_STOP_TICK` 成立,于是调用通用 `sched_update_tick_dependency()`。
注释里还明确写了一点重要不变量:**bypass 模式下永远分配有限时片**,所以这里用 `sched_update_tick_dependency()` 处理 INF 路径时,评估的是已不在 CPU 上的"出队任务",逻辑是安全的。
#### 修改 `scx_can_stop_tick()`
新增一段:
/*
- @rq->curr may still reference an outgoing EXT task after it has been
- dequeued. If no EXT tasks are accounted on @rq, ignore its stale
- slice state. If another task is dispatched from a DSQ,
- set_next_task_scx() will update the dependency for the incoming task.
*/
if (!rq->scx.nr_running)
return true;
含义:当 rq 上已经没有 EXT 任务在跑(`nr_running == 0`),即便 `rq->curr` 还指向刚 dequeue 的 EXT 任务,也告诉 nohz 代码"可以停 tick",因为这是一个 `idle → idle`(或 `outgoing EXT → next task`)的窗口,残留状态不可信。下一次新任务进来时,`set_next_task_scx()` 会重新评估。
### PATCH 2/2 — selftest
`nohz_tick.bpf.c` 实现 `nohz_tick` scheduler:
- `enqueue` 根据 `finite_phase` 全局变量切换分配 `SCX_SLICE_INF` 或 1ms 有限时片。
- `running` 累计 `nr_inf_running` / `nr_finite_running`。
- `tick` 在 finite 阶段下累计 tick 次数 `nr_finite_ticks`。
- 使用 `SCX_OPS_ENQ_LAST` + `SCX_KICK_IDLE`,让目标 CPU 在 idle 时立刻被 kick。
`nohz_tick.c` 用户态测试:
1. 在 `/sys/devices/system/cpu/nohz_full` 里挑一个允许使用的 nohz_full CPU 作为 `test_cpu`;如果没有 allowed nohz_full CPU 或找不到 housekeeping CPU,输出 `SKIP`。
2. 把自己绑到非 `test_cpu` 的 housekeeping CPU 上,避免测试本身干扰目标 CPU。
3. 阶段 1:fork 一个 `SCHED_EXT` worker → 把它绑到 `test_cpu` → 等待 `nr_inf_running >= 1`(代表 INF slice 已被选定)。
4. 阶段 2:`SIGSTOP` 暂停该 worker,让 rq 保留 INF slice 状态,但 CPU 进入 idle;sleep 100ms 等 tick 真正停下。
5. 阶段 3:唤醒 worker → BPF 在下一轮 enqueue 时切换 `finite_phase = true` 并分配 1ms 有限时片;测试等待 `nr_finite_ticks >= 3`,否则 FAIL。
6. 测试线程异常退出时,子 worker 由 `PR_SET_PDEATHSIG = SIGKILL` 兜底回收。
测试结果对比:
- **修复前**:CPU 1 在 finite 阶段只能收到 1 次 tick → FAIL。
- **修复后**:CPU 1 收到 6 次 tick → PASS。
### Tejun 回复(Message 4)
Tejun Heo 直接回复 `Applied 1-2 to sched_ext/for-7.2-fixes.`,表示整组补丁被打入 fix 分支,没有再次要求改动。
---
## ASCII 流程图
### tick 依赖状态机(修复后)
+----------------------------+
| CPU idle (tick stopped) |
| SCX_RQ_CAN_STOP_TICK=N/A |
+-------------+--------------+
|
wakeup / dispatch a finite-slice EXT
|
v
+--------------------------------------------+
| set_next_task_scx(p=finite-EXT) |
| clear SCX_RQ_CAN_STOP_TICK |
| if tick_nohz_full_cpu(): |
| tick_nohz_dep_set_cpu(SCHED) |
+--------------------+-----------------------+
|
v
+--------------------------------------------+
| tick ON: scheduler ticks delivered to p |
| update_other_load_avgs() refreshes PELT |
+--------------------+-----------------------+
|
task blocked / sleeps
|
v
+--------------------------------------------+
| dequeue, nr_running-- |
| sub_nr_running -> sched_update_tick_dep() |
+--------------------+-----------------------+
|
nr_running == 0 ?
/ \
yes no
| |
v v
+-----------------------+ +-------------------------+
| scx_can_stop_tick(): | | keep SCX_RQ_CAN_STOP_ |
| if !nr_running | | TICK as-is; let generic |
| return true | | scheduler re-evaluate |
| -> tick may stop | +-------------------------+
+-----------+-----------+
|
v
(back to CPU idle)
### 测试路径
fork inf worker (INF slice)
|
v
wait nr_inf_running >= 1 ---> skip / fail otherwise
|
SIGSTOP inf worker
|
sleep 100 ms (CPU idle, tick stopped)
|
v
SIGCONT + BPF enqueue with finite_phase=1, slice=1ms
|
v
wait nr_finite_ticks >= 3
|
PASS / FAIL
### 评审/版本时间线
v1 -- initial fix
|
v2 -- reassert tick on every finite selection;
| ignore stale slice when !nr_running;
| kill orphan workers; clarify bypass unreachable
|
v3 -- split infinite/finite branches (Tejun);
| tidy comments
|
v3 applied by Tejun to sched_ext/for-7.2-fixes
---
## 概念类比
把 `set_next_task_scx()` 想象成**工厂换班的交接仪式**:
- **车间主管(scheduler tick)**:平时车间没活时主管在办公室打盹(tick stopped),靠 `SCX_RQ_CAN_STOP_TICK` 这块牌子决定要不要"留在办公室"。
- **无限时片工人(INF slice 任务)**:他们签的是"开放式合同",不需要主管盯进度,主管可以安心打盹。
- **有限时片工人(1ms slice 任务)**:他们签的是"小时工合同",必须由主管每过一段时间来巡检一次(否则没人催他们下班、也没人记账)。
bug 出现的过程:
1. 上一班"小时工"下班前把"主管需要巡检"的牌子擦掉了,但下一班来的也是"小时工",换班的人看到牌子还是打盹状态,**就没人叫醒主管**——小时工超时却没人在意。
2. 上一班小时工下台后,runqueue 的"现行工人"标签还指着前任(一张没更新的旧工牌),而真正的当前状态是"车间空无一人"。调度器盯着旧工牌以为"还有人要管",于是**车间空着主管也不打盹**——浪费电。
修复后的协议:
- **新工人一上任**,是 INF 还是 finite 一目了然;只要是 finite,**主管必须立刻被叫醒**(`tick_nohz_dep_set_cpu()`),不管上一班是什么状态。
- **车间空了**(`!nr_running`),即便旧工牌还在,也**默认让主管去休息**(`return true`),等下次有人来再叫醒。
- 换班仪式把"INF 与 finite 走两条独立通道",这样不会再因为"两次都是 finite 而跳过检查"。
---
## Highlight 突出问题
1. **隐式假设 bypass 路径不分配 INF slice**:`set_next_task_scx()` 注释里明确写了"Bypass mode always assigns finite slices, so @p can't have an infinite slice while bypassing",但这条不变量没在代码里强制,未来如果有人改动 bypass 的 slice 分配策略,这里对出队任务的 `sched_update_tick_dependency()` 调用就需要重新审视。
2. **测试强依赖硬件拓扑**:必须同时存在允许进程使用的 nohz_full CPU 和另一个 housekeeping CPU,否则 `SKIP`。在 CI 节点或单核环境跑不出来结果,merge 阶段需要确认各 CI 农场都满足条件,或者后续补一个 `sched_ext` 自己的 nohz 模拟机制。
3. **race window 在 `set_next_task_scx` 与 `__schedule` 之间**:commit message 提到 `set_next_task_scx() updates the tick dependency before __schedule() updates rq->curr`,这正是 bug 根源。修复通过**无条件**在 finite 路径上调用 `tick_nohz_dep_set_cpu()` 来绕开这个窗口,但读者需要意识到这条不变量是"selected task 即将运行",而不是 `rq->curr`。
4. **`scx_can_stop_tick()` 的早返回位置**:新加的 `if (!rq->scx.nr_running) return true;` 放在前面,意味着 nr_running 为 0 时不再检查 `p->sched_class == &ext_sched_class` 等后续条件。理论上没问题(rq 上没人了 → 应该可以停 tick),但与下游 CFS/Idle 的 `can_stop_tick()` 链是否有副作用耦合,需要长期观察。
5. **virtme-ng 使用门槛**:README 中的复现命令依赖 virtme-ng + `CONFIG_NO_HZ_FULL=y` + `nohz_full=all`,普通开发者不易重现。如果以后扩展场景,要考虑提供一个 standalone 的 qemu 启动脚本。
6. **Tejun 在 v3 没有再提反馈**,说明前一轮 "split infinite/finite branches" 的建议已被吸收,v3 之后的反馈通道相对开放——后续如果上游跑出回归,可能直接在本分支上追加 fix。
---
## 版本演进
| 版本 | 关键改动 | 来源 |
| --- | --- | --- |
| v1 | 初始版本,提交修复 patch + selftest | lore 链接 |
| v2 | (1) 对每个被选中的 finite-slice EXT 任务重新声明 tick 依赖,覆盖 finite→idle→finite;(2) 在 rq 上无 EXT 任务时忽略残留 outgoing slice state;(3) 注释澄清 infinite-slice bypass 路径不可达;(4) selftest 子进程在父进程异常退出时自动 SIGKILL | Tejun / sashiko AI 反馈 |
| v3 | (1) 把 infinite/finite 路径拆成独立分支,slice 类型只比较一次;(2) 整理/重写代码注释 | Tejun 反馈 |
| v3 (应用) | Tejun Heo 直接 `Applied 1-2 to sched_ext/for-7.2-fixes`,无进一步 review 改动 | Message 4 |
---
## 与其他相关 patch 系列的关联
- **Fixes: 22a920209ab6 ("sched_ext: Implement tickless support")**:这是原始引入 SCX tickless 的 commit,本次修复的目标就是它。
- **`sched_ext/for-7.2-fixes` 分支**:sched_ext 维护者 Tejun Heo 维护的 7.2 修复分支,本系列已落入其中。后续如果有 7.2 的 RC 周期,预期会通过 `sched_ext/for-7.2-fixes` → `sched_ext` 主分支 → upstream。
- **`sashiko AI`**:cover letter 中多次出现对 sashiko AI 的致谢,说明作者工作流里已集成 AI 协助 review;这与同作者其它 v2/v3 系列的 ACK 模式一致。
- **同类 issue 的扩展方向**:未来如果 EXT 在 `SCX_OPS_SWITCH_PARTIAL` 之外的行为差异(例如 `SCX_OPS_ENQ_LAST` 在 idle wakeup 路径下的副作用)被进一步厘清,本 selftest 的 `PHASE_TIMEOUT_MS` 与 `MIN_FINITE_TICKS` 常量可能需要重新调参。
---
## 一句话总结
Andrea Righi 修复了 sched_ext 在 nohz_full CPU 上因 `set_next_task_scx()` 与 `scx_can_stop_tick()` 之间状态残留而漏启 scheduler tick 的问题,通过拆分 infinite/finite 两条分支、强制 finite 路径声明 tick 依赖、以及在 `nr_running == 0` 时忽略过期状态,并配套可复现的 kselftest,已被 Tejun Heo 合入 `sched_ext/for-7.2-fixes`。
---