sched-ext discussion
[PATCH sched_ext/for-7.3] sched_ext: Bound per-task reenqueues and eject the owning scheduler
LLM 分析
sched_ext: 限制每任务重入队次数并弹出所属调度器
系列基线信息
| 字段 | 内容 |
|---|---|
| 标题 | sched_ext: Bound per-task reenqueues and eject the owning scheduler |
| 作者 | Tejun Heo |
| 版本 | v1 → v2(当前线程内),v3 在 Tejun 回复中提及 |
| 规模 | 6 files, +46/-40 |
| 首封 Message-ID | 7f80005b54904a9a037822c2df5bcbc2@kernel.org |
| 来源 | sched-ext |
明确目的
修复 sched_ext 中一个可导致 CPU 锁死的缺陷:当 BPF 调度器反复将任务插入它没有权限的 cgroup(cid),任务在 reject → reenqueue 之间无限循环。
原有的 SCX_REENQ_LOCAL_MAX_REPEAT 只覆盖 local DSQ 重入队,且是 per-cpu 计数——对 cap rejection 场景完全无效。更严重的是,reenqueue 使用的 irq_work 会自我重触发,优先级高于 timer 向量,导致 stall watchdog 无法运行,最终只能等 NMI hardlockup 探测器来救场。
本 patch 将限制泛化为 per-task 计数器 reenq_cnt,在 scx_do_enqueue_task() 这一所有重入队路径的统一漏斗处递增,任务被选中运行时清零。超过 SCX_REENQ_MAX_REPEAT 后弹出(eject)该任务所属调度器,任务被搁置(stranded)等待 sched exit 清理。
遍历代码
1. 新增 per-task 计数器
// include/linux/sched/ext.h — sched_ext_entity
u32 reenq_cnt; /* reenqueues since last run */
每个 task_struct 的 scx 实体新增 reenq_cnt,跟踪该任务自上次运行以来被重入队的次数。
2. 核心限制逻辑 — scx_do_enqueue_task()
if (enq_flags & SCX_ENQ_REENQ) {
if (++p->scx.reenq_cnt > 1)
__scx_add_event(sch, SCX_EV_REENQ_REPEAT, 1);
if (unlikely(p->scx.reenq_cnt > SCX_REENQ_MAX_REPEAT)) {
__scx_exit(sch, SCX_EXIT_ERROR_REENQ, 0, cpu_of(rq), ...);
return;
}
}
- 所有
SCX_ENQ_REENQ路径都经过这里——local reenqueue、cap rejection、所有来源统一计数。 - v2 改进:
reenq_cnt > 1时才计数事件,即只在连续重入队时才增加SCX_EV_REENQ_REPEAT,首次 reenqueue 不计入。 - 超过
SCX_REENQ_MAX_REPEAT→ 弹出所属调度器,任务被搁置。
3. 清零点 — clr_task_runnable()
if (reset_runnable_at) {
p->scx.flags |= SCX_TASK_RESET_RUNNABLE_AT;
p->scx.reenq_cnt = 0;
}
任务被选中运行时,reenq_cnt 重置。这保证了"中间运行过"的任务不会误触发上限。
4. 删除旧 per-cpu 机制
process_deferred_reenq_locals() 中删除了 seq/cnt/skip 逻辑和 SCX_REENQ_LOCAL_MAX_REPEAT 检查。循环内的递归现由 per-task 上限兜底,不再需要 per-cpu 限制。
5. scx_deferred_reenq_local 结构体精简
删除 seq 和 cnt 字段,只保留 node 和 flags。
6. 新退出原因
SCX_EXIT_ERROR_REENQ, /* a task hit the reenqueue repeat limit without running */
退出原因字符串:"reenqueue limit"。
7. 事件重命名
SCX_EV_REENQ_LOCAL_REPEAT → SCX_EV_REENQ_REPEAT,语义从"local DSQ 重复重入队"扩展为"所有来源的重复重入队"。
ASCII 流程图
BPF scheduler dispatches task
|
v
+-------------------+
| scx_do_enqueue() |
| (SCX_ENQ_REENQ?) |
+-------------------+
| |
No REENQ REENQ set
| |
v v
normal path reenq_cnt++
|
+------------+------------+
| |
reenq_cnt <= 1 reenq_cnt > 1
| |
v v
(no event) SCX_EV_REENQ_REPEAT++
|
+------------+------------+
| |
reenq_cnt <= MAX reenq_cnt > MAX
| |
v v
ops.enqueue() SCX_EXIT_ERROR_REENQ
(let BPF retry) eject owning scheduler
|
v
task stranded,
picked up at sched exit
====== Clear path ======
task picked to run
|
v
clr_task_runnable()
|
v
reenq_cnt = 0
OLD design (per-cpu, local only):
+--------------------------------------------------+
| process_deferred_reenq_locals() |
| seq++ per round, cnt per (seq, drl) |
| only covers local DSQ bounce |
| cap rejection: NO LIMIT → irq_work storm |
+--------------------------------------------------+
NEW design (per-task, all sources):
+--------------------------------------------------+
| scx_do_enqueue_task() [single funnel] |
| reenq_cnt per task_struct |
| covers local + cap + any future reenq source |
| > MAX → eject owning scheduler |
+--------------------------------------------------+
概念类比
想象一个快递分拣中心(BPF 调度器),它把包裹(任务)放到某个区域的传送带(DSQ/cid)上。如果包裹没有权限进入那个区域,安检员(cap rejection)会把它扔回总入口(reenqueue),但分拣中心又把它放回同一个区域——如此无限循环。
旧设计只给"本地传送带回弹"设了每工作台(per-cpu)的次数上限,但"安检拒绝回弹"没有上限。更糟糕的是,回弹机制本身(irq_work)会自我重触发,它的优先级比报警器(stall watchdog)还高,导致报警器被堵住,直到大楼安保系统(NMI hardlockup detector)强制介入。
新设计给每个包裹贴了一个"回弹次数"标签(per-task reenq_cnt),不管谁把它扔回来的都计数。超过上限后,直接关闭那个分拣中心(弹出调度器),包裹暂存(stranded)等善后处理。
Highlight 突出问题
-
reenq_cnt在调度类切换时未清零:Andrea Righi 指出,如果任务从 sched_ext 切换到其他调度类再切回来,或者更换 BPF 调度器 / 子调度器,reenq_cnt可能残留旧值,导致新调度器被误弹出。Tejun 已同意在scx_disable_task()中清零,将在 v3 中修复。 -
文档未同步更新:
Documentation/scheduler/sched-ext.rst仍引用SCX_EV_REENQ_LOCAL_REPEAT,且描述只涉及 local DSQ 场景。v3 将更新。 -
对所有 reenqueue 来源统一计数是否合理:Andrea 质疑是否应只限制内核驱动的重试(
SCX_TASK_REENQ_IMMED和SCX_TASK_REENQ_CAP),Tejun 认为任何来源的数百次重入队都说明有问题,统一计数更安全。 -
SCX_REENQ_MAX_REPEAT的具体值:patch 中类型定义被截断,具体阈值未在 diff 中完整展示,需确认是否仍为 256 或有调整。 -
irq_work 优先级高于 timer 的架构问题:本 patch 做了紧急止血,但更根本的问题是 irq_work 自重触发能阻塞 timer 向量,这一点值得后续关注。
版本演进
| 版本 | 关键改动 |
|---|---|
| v1 → v2 | SCX_EV_REENQ_REPEAT 仅在 reenq_cnt > 1 时递增(即只在连续重入队时计数,首次 reenqueue 不计入事件) |
| v2 → v3(预告) | 在 scx_disable_task() 中清零 reenq_cnt;更新 sched-ext.rst 文档中的事件名和描述 |
与其他相关 patch 系列的关联
- 本 patch 属于 sched_ext 子调度器(sub-scheduler)层级架构的一部分,
sub.c中 cap rejection 的 reenqueue 路径是本次修复的核心触发场景之一。 - 与 stall watchdog 机制相关:原假设"不运行的任务会触发 stall watchdog",但 irq_work 风暴使得 watchdog 无法运行,本 patch 是对这一假设失败场景的补充防护。
一句话总结
修复 sched_ext 中 reenqueue 无限循环导致 CPU 锁死的缺陷,将 per-cpu 的 local-only 限制泛化为 per-task 的全局重入队上限,超限后弹出所属调度器。