sched-ext discussion
[PATCHSET sched_ext/for-7.3] sched_ext: Bandwidth-limited rescue execution for stranded tasks
LLM 分析
sched_ext:为搁浅任务提供带宽受限的救援执行
系列概况
- 标题:
[PATCHSET sched_ext/for-7.3] sched_ext: Bandwidth-limited rescue execution for stranded tasks - 作者:Tejun Heo
- 版本:v1(
scx-rescue-v1,基于sched_ext/for-7.3,基线ee7aece60817) - 规模:12 个实际 patch
- 修改文件:13 个
- 代码统计:
+1186/-283 - Message-ID:
20260801085150.2697653-1-tj@kernel.org - 完整性:
b4获取到 18 封消息(cover、12 个 patch、5 个回复),实际 patch 为 12/12,均通过 DKIM;未发现 tip-bot2 污染或缺失警告。
一句话总结:为 sched_ext 子调度器中因 CPU affinity 与其 cid 不相交而无法落地的任务增加内核侧救援路径,并以带宽预算和过载逐出控制代价。
补丁目的
子调度器只拥有父调度器授予的 cid,系统却不保证这些 cid 覆盖其任务的 affinity。任务被插入目标 CPU 的 local DSQ 后,如果缺少所需 cap,旧代码把它放入 reject DSQ,再通过 ops.enqueue() 重新决策;若没有任何合法目标,任务会反复 reenqueue,最终触发 repeat limit 逐出调度器或 watchdog stall。
本系列让 BPF 调度器在可能被 cap 拒绝的 local insert 上设置 SCX_ENQ_RESCUE。内核随后接管该任务,在目标 CPU 的 rescue DSQ 中按小比例运行;正常情况下不打破持有 cap 的调度器控制,等待过久才升级为 protected execution。救援队列持续过载时,逐出近期消耗救援 CPU 时间最多的子调度器。
这里的“接管”不是把任务永久交给另一个 BPF scheduler,而是内核暂时接管“如何让它取得 CPU”这一件事。任务最终仍要进入某个 CPU 的 local DSQ 才能执行;rescue DSQ 主要是内核内部的等待和预算控制队列。
旧流程的问题
task enqueue -> local DSQ insert -> cap 检查失败
|
reject DSQ / REENQ
|
scheduler 能否找到合法 CPU?--否-->
反复返回 enqueue -> repeat limit / watchdog
这不仅会饿死 affinity 与子调度器 cid 不相交的任务;退出中的任务跳过 ops.enqueue(),还可能在内核侧形成自重入式拒绝循环,消耗 CPU 后才被 watchdog 处理。
一次 REJECT 并不等于故障:它只是把任务退回给 BPF scheduler。若 scheduler 在收到 SCX_ENQ_REENQ 后改投 global DSQ 或选择合法 CPU,循环就结束。真正的问题是 scheduler 每次都把任务投回同一个无 cap 的 CPU;这时 REJECT 路径和旧行为本质相同,只能依靠 reenqueue 上限或 watchdog 结束异常状态。
新流程
local insert
|
+-- cap 足够 ----------------------> local DSQ -> 正常调度
|
+-- cap 不足且无 RESCUE ------------> reject DSQ -> reenqueue
|
`-- cap 不足且有 RESCUE、已启用 ------> per-CPU rescue DSQ
|
token bucket 有完整 quantum?
| 否 | 是
v v
等待 timer admitted rescuee
|
正常控制下运行,按 CPU 时间计费
|
等待过久?--否--> slice 完成后结束救援
|
是
v
SCX_TASK_PROTECTED + PREEMPT + IGNORE_CAPS
旧:拒绝意味着让所属调度器再次决策。新:调度器显式请求后,内核提供最后的 forward-progress backstop;默认每 CPU 仅分配 20 ppt(2%)带宽,每次 quantum 默认 5000 μs。
BYPASS、REJECT、RESCUE 的实际生效路径
| 队列 | 谁把任务放入 | 谁决定下一步 | 典型场景 | 是否受 rescue 2% 限制 |
|---|---|---|---|---|
BYPASS | sched_ext disable/exit/abort 路径 | 内核或最近的非-bypass ancestor | scheduler 已失效,先保证系统继续运行 | 否 |
REJECT | local insert 的 cap 检查失败 | BPF scheduler 的 ops.enqueue() | 这次目标不合法,但 scheduler 可能找到其他目标 | 否 |
RESCUE | cap 失败且 insert 带 SCX_ENQ_RESCUE | 内核 token bucket 和 rescue timer | scheduler 没有任何合法 cid,任务会 stranded | 是 |
三者的控制权变化是:
BYPASS: scheduler 退出 -> 内核/ancestor 接管整个 scheduler 的任务
REJECT: 一次 insert 失败 -> 任务退回原 BPF scheduler 重选
RESCUE: 重选很可能永远失败 -> 内核接管该任务的 forward progress
BYPASS 是 scheduler 级别的故障转移,不是 cap 拒绝。每个 scheduler、每个 CPU 都有 bypass DSQ;进入 bypass 后,原 scheduler 的 ops.dispatch() 不再负责这些任务,子 scheduler 的任务会放到最近仍工作的 ancestor 的 bypass DSQ 中。SCX_DSQ_BYPASS 因此是“调度器失效期间的安全通道”。
REJECT 是一次性的退回路径。patch 的 rq_owned_post_enq() 对 reject DSQ 安排 deferred reenqueue,使任务再次进入 ops.enqueue();它不是最终执行队列,也没有“内核强制运行”的含义。
RESCUE 是 per-CPU 的内核内部等待队列。任务不会直接从 rescue DSQ 运行,而是经过预算检查后移动到 local DSQ。若普通调度器不断抢占 rescuee,内核可以恢复剩余 slice;等待过久后设置 SCX_TASK_PROTECTED,使 BPF scheduler 不能再随意缩短它的 slice 或把别的 HEAD 任务插到它前面。
Patch 概览
| Patch | 核心改动 |
|---|---|
| 1/12 | 将 scx_local_or_reject_dsq() 改名为能容纳第三种目的地的 scx_resolve_local_dsq()。 |
| 2/12 | 暴露 ext.c helper 和 scx_sched_all,供 sub.c 救援逻辑使用。 |
| 3/12 | 抽取可针对指定 rq 取时钟的 __scx_bpf_now()。 |
| 4/12 | 统一校验 dsq-move kfunc 的内部 enq_flags,阻止 BPF 夹带内核标志。 |
| 5/12 | 令 SCX_ENQ_IGNORE_CAPS 同时豁免 preemption cap。 |
| 6/12 | 将 slice/vtime 随 insert verdict 携带,在 insertion commit 点统一写入。 |
| 7/12 | 增加 SCX_TASK_PROTECTED,保护 slice 和 rq-owned DSQ 头部位置。 |
| 8/12 | 增加 rescue DSQ、token bucket、timer、计费及延迟升级机制。 |
| 9/12 | 以衰减的 rescue 使用量选出 top consumer,过载时逐出它。 |
| 10/12 | 同步 tools 侧自动生成的枚举头文件。 |
| 11/12 | scx_qmap 对 pinned task 也先做 idle 检查,避免错误的 direct dispatch。 |
| 12/12 | scx_qmap 在 enqueue/SHARED_DSQ 扫描中标记 rescue,并增加 -B、-q 参数。 |
关键实现
-
三个 DSQ 的层次。 CPU 真正执行任务的入口仍然是 local DSQ。global DSQ 和 custom DSQ 中的任务需要被 move 到 local DSQ;reject DSQ 等待重新调用 scheduler;rescue DSQ 等待内核准入。因此新增 rescue 并没有把 CPU 的执行入口从一个变成两个。
-
救援准入与预算。
scx_resolve_local_dsq()只对 local DSQ 处理SCX_ENQ_RESCUE。cap 不足时,scx_rescue_try_admit()检查 per-rq token bucket;无当前 rescuee、无排队者且预算达到完整 quantum 才立即准入,否则按到达顺序进入 rescue DSQ。拥挤时 quantum 按等待深度分摊,最小 slice 为 1 ms。 -
quantum 不是 period。 默认
quantum=5000 μs是一次救援允许消耗的 CPU 时间;bandwidth=20 ppt是预算补充速率。近似计算为:5000 μs / 0.02 = 250 ms,即 CPU 大约经过 250 ms 才积攒出一次 5 ms rescue 预算。它是 token bucket,不是每 250 ms 严格打开的固定窗口;允许有限 burst,但长期 rescue CPU 占比趋近 2%。 -
拥挤时的 quantum。 若 5 个 stranded task 等待,单次 slice 大约是
5000/5=1000 μs,而不是每个任务都获得完整 5 ms。内核按实际获得的 CPU 时间扣预算;被普通调度逻辑抢占不会把未执行的时间错误地算作已服务。 -
救援不是取消 cap。 admitted rescuee 仍受所属调度器通常的抢占/重切片控制,实际执行时间由
update_curr_scx()调用scx_rescue_charge()计费。被调度器抢占后,scx_rescue_keep()恢复未服务的剩余 slice;scx_can_stop_tick()保持 tick,以便计费和超时推进。 -
保护升级。 rescue timer 发现 rescuee 等待过久且预算超过两个 quantum 时,先设置剩余 slice,再置
SCX_TASK_PROTECTED,通过SCX_ENQ_HEAD | SCX_ENQ_PREEMPT | SCX_ENQ_IGNORE_CAPS放回 local DSQ。protected 任务拒绝 BPF 的 slice 修改,HEAD 插入也不能越过它;slice 消耗、dequeue、yield 或 bypass 会清除保护。 -
过载归责。 patch 9 在 rescue DSQ 队首等待超过派生阈值后,遍历各子调度器在该 CPU 的衰减
rescue_avg,选择最大者以SCX_EXIT_ERROR_RESCUE退出。默认配置下 funding period 约为 250 ms,过载阈值约为16*250ms=4s,最终被限制在 1–15 s 范围。 -
qmap 覆盖排队期间变化。 patch 12 不只在
qmap_enqueue()标记没有 self-cid 的任务,也在每次 SHARED_DSQ 扫描时救援 affinity 已变化而搁浅的任务;错误 cid 注入同样走 rescue,便于确定性测试。
无 RESCUE 时是否回到旧流程
是的,核心行为没有改变:
SCX_ENQ_RESCUE 未设置,或 rescue 被 -B 0 关闭
|
v
cap 检查失败 -> SCX_DSQ_REJECT
|
v
SCX_ENQ_REENQ -> ops.enqueue() 再次决策
|
+-- 改投合法 DSQ:循环结束
|
`-- 每次重复错误目标:计数增加,最终逐出 scheduler
因此 REJECT -> reenqueue 不是 rescue 的替代实现,而是旧的恢复机制。patch 12 在 -B 0 时仍会设置 rescue 请求,但内核因为 rescue disabled 而走 reject;Tejun 的回复确认,每次 bounce 会累计 p->scx.reenq_cnt,超过 SCX_REENQ_MAX_REPEAT 后逐出 qmap。若任务长期 runnable 却始终得不到有效执行,也可能由 watchdog stall 路径逐出;正常情况下应先命中 reenqueue repeat limit。
类比
把 CPU 想成办公楼、子调度器想成只租了部分楼层的公司、cid 想成门禁卡。
BYPASS:公司倒闭,物业接管员工,保证大楼不停摆。REJECT:员工走错门,被保安退回公司重新选楼层。RESCUE:员工根本没有任何可用门禁卡,物业提供一个限时临时工位。
rescue 的 5000 μs 是一次临时工位的最长工作额度,2% 是临时工位重新获得额度的长期速率;不是“每 5000 μs 只能用 2%”。
Highlight:风险与注意点
-B 0会关闭 rescue;scx_qmap的 rescue insert 会照常被 cap 拒绝并重新入队。Tejun 回复称每次 bounce 都计入p->scx.reenq_cnt,超过SCX_REENQ_MAX_REPEAT后逐出 qmap,因此循环有界,但预期失败模式是“击落 qmap”而不是继续救援。- REJECT 不是新的执行场所,也不是“无限等待队列”:它只负责触发一次 deferred reenqueue。scheduler 如果重复把任务投到同一个无 cap CPU,结果仍然是旧问题,只是最终会被 repeat limit/watchdog 处理。
- 当前本地
linux-mainlinecheckout 尚未包含SCX_DSQ_RESCUE;本报告解释的是该 v1 patch series 引入的目标实现。BYPASS 在当前代码中已经存在,REJECT/RESCUE 则属于该子调度器 rescue 版本的内核内部路径。 - 自动审查指出 patch 9 在 rq lock 下用
list_for_each_entry_rcu()遍历scx_sched_all,但未显式出现rcu_read_lock();这是需要复核的并发/RCU 风险,不应视为已解决。 - 自动审查还指出 patch 11 删除 pinned task 快速路径后,
always_enq_immed可能成为未使用的 BPF rodata/用户态赋值;属于低风险清理项。 - patch 6 明确接受某些并发写入的“后提交者生效”语义:rq-lock 下的
scx_bpf_task_set_slice()与 DSQ insertion commit 可能竞争,BPF 调度器需自行同步 user-DSQ 场景。 - watchdog timeout 若短于 rescue funding/过载阈值,watchdog 可能先于过载归责逐出调度器;代码会警告,但没有自动调整参数。
- 线程是 v1;5 个回复主要来自自动审查,未见正式人工
Reviewed-by/Acked-by或后续修订版本,不能把该系列视为已获维护者认可。
一句话总结
该系列把 sched_ext 的“cap 不相交导致永远无法运行”从无界拒绝循环改造成有预算、有计费、可升级且可归责的内核救援路径,同时以 scx_qmap 提供了覆盖 enqueue 与排队期间 affinity 变化的示例实现。