0/4 已展开

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-ID7f80005b54904a9a037822c2df5bcbc2@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_structscx 实体新增 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 结构体精简

删除 seqcnt 字段,只保留 nodeflags

6. 新退出原因

SCX_EXIT_ERROR_REENQ, /* a task hit the reenqueue repeat limit without running */

退出原因字符串:"reenqueue limit"

7. 事件重命名

SCX_EV_REENQ_LOCAL_REPEATSCX_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 突出问题

  1. reenq_cnt 在调度类切换时未清零:Andrea Righi 指出,如果任务从 sched_ext 切换到其他调度类再切回来,或者更换 BPF 调度器 / 子调度器,reenq_cnt 可能残留旧值,导致新调度器被误弹出。Tejun 已同意在 scx_disable_task() 中清零,将在 v3 中修复。

  2. 文档未同步更新Documentation/scheduler/sched-ext.rst 仍引用 SCX_EV_REENQ_LOCAL_REPEAT,且描述只涉及 local DSQ 场景。v3 将更新。

  3. 对所有 reenqueue 来源统一计数是否合理:Andrea 质疑是否应只限制内核驱动的重试(SCX_TASK_REENQ_IMMEDSCX_TASK_REENQ_CAP),Tejun 认为任何来源的数百次重入队都说明有问题,统一计数更安全。

  4. SCX_REENQ_MAX_REPEAT 的具体值:patch 中类型定义被截断,具体阈值未在 diff 中完整展示,需确认是否仍为 256 或有调整。

  5. irq_work 优先级高于 timer 的架构问题:本 patch 做了紧急止血,但更根本的问题是 irq_work 自重触发能阻塞 timer 向量,这一点值得后续关注。


版本演进

版本关键改动
v1 → v2SCX_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 的全局重入队上限,超限后弹出所属调度器。