sched-ext discussion
[PATCHSET v2 sched_ext/for-7.3] sched_ext: Bandwidth-limited rescue execution for stranded tasks
LLM 分析
sched_ext 带宽受限的 rescue 执行机制 (PATCHSET v2)
系列基线信息
- 标题: [PATCHSET v2 sched_ext/for-7.3] sched_ext: Bandwidth-limited rescue execution for stranded tasks
- 作者: Tejun Heo tj@kernel.org
- 版本: v2(基于 sched_ext/for-7.3 c5b9316cf3d5)
- 规模: 12 个 patch,共 1187 行新增 / 285 行删除
- Message-ID: 20260802215447.3134509-1-tj@kernel.org
- 来源: https://lore.kernel.org/sched-ext/20260802215447.3134509-1-tj@kernel.org/
- Git tree: git://git.kernel.org/pub/scm/linux/kernel/git/tj/sched_ext.git scx-rescue-v2
明确目的
子调度器(sub-sched)只持有父级授予的 cids,没有机制保证这些 cids 覆盖任务 affinity。当任务可运行 cpu 不落在任何授权 cid 上时,目前没有好结局:调度器插入请求被 cap-reject,循环 reenqueue 直到重试上限驱逐调度器,或者任务卡住触发 stall watchdog。exiting 任务更糟,会跳过 ops.enqueue() 进入自重排死循环。
本系列在 kernel 侧加入 rescue 执行路径:调度器在可能 cap-reject 的 local DSQ insert 上打 SCX_ENQ_RESCUE 标记,kernel 改用一受限带宽直接驱动任务运行;当救援队列长时间过载时,按救援消耗驱逐真正的肇事 sub-sched 而不是误罚等待者 owner。
遍历代码(按 patch)
按邮件顺序:
0001 — 重命名 scx_local_or_reject_dsq → scx_resolve_local_dsq
仅为改名(cleanup)。后续 patch 让该函数多一种返回目的(rescue DSQ),原名"local or reject"列举了所有结局不够中性。
0002 — 让 ext.c 几个 helper 暴露到 ext.c 之外
把 set_task_slice、task_unlink_from_dsq、move_local_task_to_local_dsq、init_dsq、dump_line、scx_sched_all 等从 static 改成全局 scx_xxx,供 sub.c 在救援路径里调用。
0003 — 抽出 __scx_bpf_now(struct rq *rq)
原 scx_bpf_now() 把 rq 锁定在 this_rq()。抽出 __scx_bpf_now(rq) 后,sub.c 救援路径可以针对被持有 rq lock 的目标 rq 读取时钟。
0004 — 在 dsq move kfunc 中拒绝内部 enq_flags
dsq insert kfunc 之前在 scx_dsq_insert_preamble() 拒 __SCX_ENQ_INTERNAL_MASK,但 dsq move kfunc 走的是另一条 vet 路径;BPF 调度器可借此塞入 SCX_ENQ_CLEAR_OPSS 等内部位。把检查挪到 scx_vet_enq_flags(),让所有进入路径都过同一关。vtime move 之前在 vet 前 OR 入了 SCX_ENQ_DSQ_PRIQ,现在改为 vet 之后再置位。
0005 — SCX_ENQ_IGNORE_CAPS 同时豁免 preemption cap
该 flag 标记 kernel 强制 placement,但 preemption 路径仍通过 scx_caps_for_preempt() 检查导致不一致。给 scx_caps_for_preempt() 加 enq_flags 参数,IGNORE_CAPS 时返回 0(不需要任何 cap)。
0006 — 同步 slice 与 dsq_vtime 写入
核心改动:dsq insert kfunc 不再直接写 slice/dsq_vtime,而由 verdict 携带数值,在插入时通过 apply_slice_vtime() 一次性 commit。
static void apply_slice_vtime(struct task_struct *p, u64 slice, u64 vtime, u64 enq_flags)
{
if (slice) {
p->scx.slice = slice;
if (!(enq_flags & SCX_ENQ_SLICE_DFL))
clear_task_slice_oob(p);
} else if (!p->scx.slice) {
p->scx.slice = 1;
}
if (enq_flags & SCX_ENQ_DSQ_PRIQ)
p->scx.dsq_vtime = vtime;
}
并新增 SCX_ENQ_APPLY_SLICE 携带位,让 direct_dispatch/dispatch_to_local_dsq 等场景把预存于 ddsp_slice/ddsp_vtime 的 verdict 数值带到提交点。
0007 — 引入 SCX_TASK_PROTECTED
slice/DSQ 头部位置保护标志。kernel 授予的 slice 在 set_task_slice_keep_oob() 拒绝 BPF 进一步改写;dsq_insert_head() 对 rq-owned DSQ 把 protected 任务留在队首,新任务插到它们后面。新 scx_task_slice_ended() 在 slice 耗尽/dequeue/yield/bypass 时清理 PROTECTED 位。
0008 — 带宽受限的 rescue 执行(核心特性)
- 新增内置 DSQ
SCX_DSQ_RESCUE与SCX_ENQ_RESCUEflag。 - 在
scx_resolve_local_dsq()里:当 cap 检查失败且*enq_flags & SCX_ENQ_RESCUE,把任务放进 rescue DSQ,记录rescue_at = jiffies。 - 每 CPU 一个 token bucket,预算为
rescue_bandwidth_ppt(默认 20,即 2%),每个救援获得rescue_quantum_us(默认 5ms) 的 slice;队列满则按等待者数均分(最低 1ms)。 scx_rescue_charge()在update_curr_scx()中按实际执行时间扣 bucket。- 救援期任务挂在 local DSQ 尾部,不抢优先级,调度器可正常 preempt/reslice。
- 服务超时则升级为 protected execution(
SCX_TASK_PROTECTED)抢占当前任务。 - 暴露
sched_ext_ops两个 root-only 旋钮:rescue_bandwidth_ppt、rescue_quantum_us,取值受SCX_RESCUE_MIN/MAX_*范围约束。 SCX_RESCUE_DISABLE关闭救援:带 RESCUE 的 insert 按普通 cap-reject 走。- 新事件
SCX_EV_SUB_RESCUE统计救援触发次数;SCX_OPS_OPEN()加 compat 修复让旧 kernel 不识别新字段时报错。
0009 — 过载时驱逐救援消耗最多的 sub-sched
- 新增
SCX_EXIT_ERROR_RESCUE退出原因。 scx_sched_pcpu加rescue_avg与rescue_avg_at(jiffies_64),每次救援 charge 时decay_avg() + delta_exec。scx_rescue_check_overload()由 rescue timer 触发:当队首等待者rescue_at + overload_threshold仍未获得服务,按rescue_avg选最大者驱逐;设有kill_atgrace 期防止反复踢人。scx_rescue_set_knobs()根据预算周期算overload_after(clamp 到 1s–15s)与decay_halflife,并在 watchdog timeout ≤ 过载阈值时pr_warn,避免 scheduler 先于救援机制被 stall-kill。
0010 — 同步 tools 自动生成的 enum 头文件
机械更新 enum_defs.autogen.h、enums.autogen.bpf.h、enums.autogen.h,新增 HAVE_SCX_* 宏与 SCX_ENQ_RESCUE。
0011 — scx_qmap 对 pinned 任务做 idle check
pick_direct_dispatch_cid() 之前对 nr_cpus_allowed == 1 的 pinned 任务直接走 prev_cid 直派,共享 cid 上的直派被强制 IMMED 可能落到忙 cpu 而被 kernel 回弹,进入 reenqueue 循环。删除该旁路,让 pinned 任务也走 idle 检测。always_enq_immed rodata 镜像随之删除(已无读者);-I 仍设 SCX_OPS_ALWAYS_ENQ_IMMED。
0012 — scx_qmap 接入 rescue 支持
qmap_enqueue检测任务可运行 cid 与 self cids 无交集时,强制 IMMED 派到首个允许 cid 的 local DSQ 并附SCX_ENQ_RESCUE。- 新函数
scan_shared_dsq()替代dispatch_highpri():扫描 SHARED_DSQ 时,把 stranded 任务救援出去;HIGHPRI_DSQ cull 后若变 stranded 也走 rescue。 - 命令行加
-B PPT(救援带宽,0映射SCX_RESCUE_DISABLE)和-q US(quantum)。 - 新增
nr_rescue_dsp统计并在 hier 状态行打印rescue=+N。
ASCII 流程图
task 唤醒 / reenqueue
│
▼
scx_resolve_local_dsq(sch, rq, p, *enq_flags)
│
cap 检查 scx_missing_caps
┌──────┴──────┐
通过 失败 + RESCUE
│ │
▼ ▼
SCX_DSQ_LOCAL SCX_DSQ_RESCUE (记录 rescue_at)
│ │
▼ ▼
正常调度路径 rescue 桶/计时器
│
每 tick 检查
│
┌───────────────┼──────────────────┐
▼ ▼ ▼
slice=0 仍有等待者 超载过载阈值
│ │ │
▼ ▼ ▼
rescuee 结束 token 允许 → 选 rescue_avg
或提升 put_prev requeue 最大的 sub-sched
运行 quantum 并 __scx_exit
(ERROR_RESCUE)
排队/调度器视角 ─────────────── 调度器控制面 ─────────────── rescuee 视角
┌────────────────┐
dsq_insert_head() ──► │ 保护队首任务 │ ◄── dsq_insert_head()
│ SCX_TASK_PROT. │
BPF 试图改写其 slice ──► │ 拒绝 + 计 EV │
└────────────────┘
┌─────────────┐ 到达 rescue 队列 ┌─────────────┐
│ RESCUE 队列 │ ────────────────────► │ rescuee │
│ 尾插/无优先 │ │ 普通调度器 │
└─────────────┘ │ 可 preempt │
└─────────────┘
反馈回路: 救援消耗 → 平均值 → 过载判定
rescue_charge() ─► pcpu->rescue_avg = decay_avg() + delta
│
▼
scx_rescue_check_overload()
│
头等待 rescue_at 早于 overload_after 且
rq->scx.rescue.kill_at 已过 grace
│
▼
遍历 scx_sched_all, 选 avg 最大且非 EXITING
│
▼
__scx_exit(victim, ERROR_RESCUE)
概念类比
把 sched_ext 的 sub-sched 与 cap 机制想成 一栋楼的会议室预订系统:每个 sub-sched 是某个部门,只能用公司分给本部门的会议室(cids)。如果某员工(task)只接受某些楼层的会议室,而本部门没拿到那几层,他就没地方开会。旧版系统只会让员工原地等待、再尝试(reenqueue 循环),直到看门大爷(watchdog)把整个部门赶走。
新引入的"rescue"相当于 物业值班室:当部门确实订不到合适的会议室时,员工可以去物业的共享小会议室(rescue DSQ)开短会,物业只给一小块时间(默认 2% CPU 时间),但保证员工有进展。如果某部门老是把员工推到物业会议室,物业会按"过去一周向物业借了多少时间"算账,把最爱借的部门请走(驱逐肇事 sub-sched),而不是冤枉借不到会议室的员工所在部门。
SCX_TASK_PROTECTED 类似 "请勿打扰"门牌:kernel 给救援任务开门后,BPF 调度器不能擅自压缩其会议时间,也不能把它从队首挤掉;会议结束(slice 用尽)门牌自动摘下。
Highlight 突出问题
- 救援只对 root 默认开启:子调度器若要享受 kernel 兜底,必须在 root 层配
rescue_bandwidth_ppt;root 默认 2% 是保守值,对 latency-critical 负载可能仍嫌激进。 SCX_RESCUE_DISABLE与 cap-reject 的语义等价:关闭后所有 RESCUE insert 退回普通 cap-reject,可能进入 reenqueue 循环直到退出调度器;qmap-B 0已演示该路径。overload_after与 watchdog timeout 不匹配时会先被 stall-kill:kernel 主动pr_warn,但不会强制更改配置;sub-sched 作者仍需自己调高 watchdog 或降低救援预算周期。- 救援升级为 protected 时仍可被调度器 rebalance:升级仅限 slice 与 DSQ 头部保护;调度器仍可移动任务到其他 cpu,需要结合 sub-sched 的 cap 设计。
- 新增
SCX_ENQ_APPLY_SLICE路径改动语义:BPF 调度器如果旧逻辑依赖 dsq insert kfunc 直接修改p->scx.slice,升级后行为改变;scx_qmap已经适配,其它示例调度器需要相应更新。 - 救援消耗按实际 CPU 时间计费:slice 被调度器主动缩短不会延长救援;可能放大 sub-sched 之间的资源争议。
版本演进 (v1 → v2)
- v2 引用 sashiko AI 评审意见:
scx_sched_all移出CONFIG_EXT_SUB_SCHED块,因定义是无条件的(0002)。rescue_at/rescue_avg_at用jiffies_64,避免 32 位算术回绕(0009)。- 删除无人读的
always_enq_immedrodata 镜像(0011)。
- 第 8 个 patch(rescue 主体)单独发了 v2:补
SCX_OPS_OPEN()对新 ops 字段的兼容处理,让 cpu-form 调度器能在旧 kernel 加载。
与其他 patch 系列的关联
- 基于
sched_ext/for-7.3之上,与EXT_SUB_SCHED子调度器、cap 体系、SCX_DSQ_*内置 DSQ 紧密耦合。 - 上游依赖 cap / SCX_TASK_QUEUED / SCX_TASK_IN_CUSTODY 等语义;本系列未改 cap 检查本身,仅在 cap 拒绝后提供兜底。
- 与
sched_ext: Add SCX_TASK_PROTECTED(0007)共同形成"kernel 保护 + BPF 不可改写"的对称面。 scx_qmap既是功能示范,也是救援路径的压力测试:-B 0让 qmap 走纯 cap-reject 路径,-B 20(默认)走 rescue 路径,便于对比。
一句话总结
本系列为 sched_ext 子调度器在"任务无授权 cid 可跑"这种绝境提供 kernel 兜底的带宽受限救援执行、protected 升级与肇事驱逐三件套,同时修缮 slice/dsq_vtime 写入并发与内部 flag 校验,整组改动让 stranded 任务不再以调度器被 watchdog 驱逐告终。