sched-ext discussion
[PATCH v3 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 |
| 版本 | v3 |
| 规模 | 7 文件,51 增 44 删 |
| Message-ID | 6649066b805d5660b598f3155c30daac@kernel.org |
| 来源 | sched-ext |
| 后续补丁 | tools/sched_ext/include: Regenerate enum_defs.autogen.h |
明确目的
一个行为异常的 BPF 调度器会反复将任务插入到它没有 capability 的 CID(capacity ID)上,导致任务在 reject → reenqueue 之间无限循环。
原本认为这是安全的——因为一直不运行的任务会触发 stall watchdog。但实际上,reenqueue 使用的 irq_work 会自我重触发,且优先级高于 timer 向量,会阻塞 CPU 上包括 stall 检测与恢复在内的所有其他工作,直到 NMI hardlockup detector 才能打断。
本补丁将原有的仅限 local DSQ 的每 CPU 重复上限 SCX_REENQ_LOCAL_MAX_REPEAT,泛化为所有重入队来源共享的每任务计数器 reenq_cnt,超过阈值后弹出所属调度器。
遍历代码
1. 新增每任务计数器
在 struct sched_ext_entity 中新增 u32 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, ...);
return;
}
}
- 所有重入队路径都经过
scx_do_enqueue_task(),因此这里只需一处检查。 SCX_EV_REENQ_REPEAT只在连续重入队时才计数(v2 修正)。- 超过
SCX_REENQ_MAX_REPEAT后弹出调度器,任务被"搁浅"(stranded),在 sched exit 期间被回收。
3. 计数清零 — clr_task_runnable() 与 scx_disable_task()
- 任务被选中运行时,在
clr_task_runnable()中清零。 - 任务离开调度器控制时(sched class 切换、调度器替换、子调度器迁移),在
scx_disable_task()中清零——这是 v3 新增的修复(Andrea Righi 提出)。
4. 简化 process_deferred_reenq_locals()
移除原有的 per-CPU drl->cnt / drl->seq / SCX_REENQ_LOCAL_MAX_REPEAT 逻辑,递归边界现由 per-task 的 reenq_cnt 在 scx_do_enqueue_task() 中统一保证。
5. 新退出原因 SCX_EXIT_ERROR_REENQ
case SCX_EXIT_ERROR_REENQ:
return "reenqueue limit";
6. 事件重命名
SCX_EV_REENQ_LOCAL_REPEAT → SCX_EV_REENQ_REPEAT,语义从"local DSQ 重入队"扩展为"所有来源的重入队"。
ASCII 流程图
BPF 调度器将任务插入 CID
|
[capability 检查]
/ \
通过 拒绝(reject)
| |
运行任务 reenqueue (irq_work)
| |
reenq_cnt=0 reenq_cnt++
| |
| [reenq_cnt > MAX?]
| / \
| 否 是
| | |
| 继续循环 SCX_EXIT_ERROR_REENQ
| | 弹出所属调度器
| | 任务被搁浅
\ /
\ /
------
(irq_work 优先级 > timer → 阻塞 stall watchdog)
旧设计 (per-CPU, 仅 local): 新设计 (per-task, 全来源):
┌──────────────────────┐ ┌──────────────────────┐
│ drl->cnt per-CPU │ │ task->scx.reenq_cnt │
│ 仅限 local DSQ │ │ 覆盖所有 reenqueue │
│ 弹出整个层级 │ │ 只弹出所属调度器 │
│ SCX_REENQ_LOCAL_ │ │ SCX_REENQ_MAX_REPEAT │
│ MAX_REPEAT │ │ SCX_EXIT_ERROR_REENQ │
└──────────────────────┘ └──────────────────────┘
概念类比
想象一个快递分拣中心。快递员(BPF 调度器)把包裹放到一个货架(CID)上,但这个货架需要特定权限才能放。如果快递员没有权限却反复把包裹放上去,系统就会把包裹弹回(reject),然后快递员又放上去,形成无限循环。
旧做法是:每个分拣台(per-CPU)装一个计数器,只监控"本地货架"的重复放置。如果超限,就把整个分拣中心关掉——即使只有一个员工出错。
新做法是:每个包裹(per-task)贴一个标签,记录它被弹回了几次。不管是什么原因被弹回,只要超过上限,就只开除那个出错的快递员(弹出所属调度器),其他员工不受影响。
Highlight 突出问题
- irq_work 优先级陷阱:reenqueue 使用
irq_work,它自我重触发后优先级高于 timer 向量,能阻塞 stall watchdog。这意味着看似"无害"的无限循环实际上是硬锁死的温床——这是本补丁最核心的动机。 - v2→v3 的关键修复:
scx_disable_task()中清零reenq_cnt非常重要。如果不清零,任务在调度器切换或子调度器迁移后,计数器可能携带旧值,导致新调度器被误弹出。这是 Andrea Righi 发现的。 - Sashiko bot 误报:bot 检测到
enum_defs.autogen.h中HAVE_SCX_REENQ_LOCAL_MAX_REPEAT被替换为HAVE_SCX_REENQ_MAX_REPEAT,但认为types.h中旧枚举名仍存在。Tejun 解释该重命名在同一个分支的前一个 commit 中,bot 的 review base 不完整。 - 归属性改进:旧方案 per-CPU 计数会影响整个层级结构,即使只是子调度器出错。新方案 per-task 计数精确归因到所属调度器,只弹出肇事者。
版本演进
| 版本 | 关键改动 |
|---|---|
| v1 → v2 | SCX_EV_REENQ_REPEAT 只在重入队导致另一次重入队时才计数,而非每次重入队都计 |
| v2 → v3 | 在 scx_disable_task() 中清零 reenq_cnt,防止计数器跨调度器切换/子调度器迁移残留(Andrea Righi);更新 sched-ext.rst 中的过时引用 |
与其他补丁系列的关联
- 后续补丁
tools/sched_ext/include: Regenerate enum_defs.autogen.h是机械性跟进,重新生成用户空间头文件以反映本补丁引入的枚举重命名及其他累积变更。 - 本补丁是 sched_ext 子调度器(sub-scheduler)层级支持的一部分,
scx_disable_task()的清零逻辑与子调度器迁移场景直接相关。
一句话总结
将 sched_ext 的重入队循环保护从 per-CPU、仅限 local DSQ 的旧方案,升级为 per-task、覆盖所有来源的通用方案,超限时精确弹出所属调度器而非整个层级,修复了 irq_work 优先级导致 stall watchdog 被阻塞的硬锁死风险。