0/2 已展开

LLM 分析

sched_ext:修复 scx_bpf_dsq_move() 中所有权变更竞态导致的误终止

系列概况

  • 标题:[PATCH sched_ext/for-7.3-fixes] sched_ext: Fix spurious aborts in scx_bpf_dsq_move() on ownership change races
  • 作者:Tejun Heo tj@kernel.org
  • 版本:单版本(for-7.3-fixes 分支,1/1)
  • 规模:1 个文件,13 行新增 / 8 行删除
  • 修改文件kernel/sched/ext/ext.c
  • 代码统计:+13 / -8
  • Message-ID7b3b7e35d462fe061105f09de3a2eba9@kernel.org
  • 完整性:原 patch + maintainer applied 回复,完整闭环

补丁目的

scx_bpf_dsq_move() 在遍历 DSQ 时会对任务做"是否属于当前 scheduler"的所有权检查,若不匹配则调用 scx_error() 中止整个 BPF scheduler。该检查最初是为了防止错误操作他人调度器上的任务。

但任务所有权可能在任意时刻发生变化:任务可能运行后退出并清除关联,也可能被重新归属到另一个 sub-sched。这两种都是良性的竞态,但旧版代码会在竞态窗口内把良性的所有权漂移误判为严重违规,导致整个 BPF scheduler 被无故 abort。

补丁目标:消除这类误终止,保留对真正所有权违规的检测能力。

旧流程的问题

scx_dsq_move() 在函数开头、未加锁时就检查 scx_task_on_sched(sch, p),并把任务已经离开当前 scheduler 的竞态情况直接报告为错误。

caller -> scx_dsq_move()
           |
   check sch->aborting (fail if true)
           |
   check scx_task_on_sched(sch, p)   <-- too early
           |
   acquire src_dsq->lock
           |
   check p still on src_dsq (cursor lost?)

问题在于:即便任务确实已经离开当前 scheduler(良性 race),旧代码也照样 abort。这相当于把"任务暂时换了主人"当成"调度器越权操作",把可恢复的竞态升级成了致命的 scheduler 终止。

新流程

把所有权检查推迟到拿锁之后、"任务仍在源 DSQ" 校验通过之后执行:

caller -> scx_dsq_move()
           |
   check sch->aborting (fail if true)
           |
   acquire src_dsq->lock
           |
   if p not on src_dsq <-- cursor-lost, unlock + return false
           |
   if !scx_task_on_sched(sch, p) <-- now it is real violation
       scx_error() + abort unlock + return

关键洞察:sched_ext 保证"任何所有权变更前都会先从源 DSQ 摘除任务"。因此当我们在 src_dsq->lock 保护下仍然看到 @p 在该 DSQ 上时,调用方一定是先前把它放进去的那一方;若此刻 @p 不属于 @sch,那就是真正的越权。否则(cursor 已经丢失),任务已经离开 DSQ,所有权变化是良性的,直接返回即可。

Patch 概览

仅一个 patch,修改 kernel/sched/ext/ext.c

  • 删除函数开头的所有权检查块(移走而非删掉)。
  • raw_spin_unlock(&src_dsq->lock); goto out; 这一 cursor-lost 早退分支之后,重新插入同一段所有权检查;abort之前先 unlock。
  • 顺手把 scx_root_enable_workfn() 中两处仍引用旧名 sched_ext_free() 的注释改为 sched_ext_dead()

关键实现

static bool scx_dsq_move(struct bpf_iter... )
{
        if (unlikely(READ_ONCE(sch->aborting)))
                return false;

        /* acquire lock and verify cursor */
        raw_spin_lock(&src_dsq->lock);
        ...
        if (!task_on_dsq(p)) {
                raw_spin_unlock(&src_dsq->lock);
                goto out;
        }

        /*
 * @p has been on $src_dsq and can't move anymore.
         * If @p is not on @sch, the caller didn't have authority
         * over @p at the time of the call.
         */
        if (unlikely(!scx_task_on_sched(sch, p))) {
                scx_error(sch,
                          "scx_bpf_dsq_move[_vtime]() on %s[%d] but the task belongs to a different scheduler",
                          p->comm, p->pid);
                raw_spin_unlock(&src_dsq->lock);
                goto out;
        }

        /* @p is still on $src_dsq and stable, determine the destination */
        dst_dsq = find_dsq_for_dispatch(...);
        ...
}

注释方面,把 scx_root_enable_workfn() 内两处 sched_ext_free() 改成 sched_ext_dead(),保持与当前代码命名一致。

类比

把这套机制想象成小区门禁:业主名单(所有权)会动态更新(搬家、退租),但每户业主一旦离开都会先归还门禁卡(先从 DSQ 出队)。

旧版保安在门口就比对名单(任务是否属于本小区),只要名单晚到一秒就拉响警报,把"住户临时外出"误判成"外人闯入"。

新版则要求:先确认此人还在小区内(lock + 仍在 DSQ),再比对名单——若人在小区却查不到业主身份,才是真入侵;若人已经出门了,只是名单刷新慢一点,安安静静关门即可。

ASCII 流程图

+----------------------------------------------------------+
| Old path: ownership check too early -> spurious abort    |
+----------------------------------------------------------+
scx_dsq_move(p)
    |
    +-- sch->aborting ? ---------- yes --> return false
    | no
    +-- scx_task_on_sched(sch,p) ? - no --> scx_error + ABORT  (false alarm)
    |                                yes
    +-- lock(src_dsq) --> cursor lost ? -> return false
    +-- dispatch ...

+----------------------------------------------------------+
| New path: lock first, then verify ownership              |
+----------------------------------------------------------+
scx_dsq_move(p)
    |
    +-- sch->aborting ? ---------- yes --> return false
    |                                 no
    +-- lock(src_dsq) --> cursor lost ? -> return false  (benign exit)
    |                                 no
    +-- scx_task_on_sched(sch,p) ? - no --> scx_error + ABORT  (real violation)
    |                                yes
    +-- dispatch ...

+----------------------------------------------------------+
| Ownership-change timing invariant                        |
+----------------------------------------------------------+
ownership change = dequeue first --> then re-attach to new sch
                          ^^^^^^^^        ^^^^^^^^^^^^^^^^^^^
 visible under lock     only visible lock-less

Highlight:风险与注意点

  • 判断语义变了:原来 abort 条件是"调用时任务不在 sch 上";新版是"在 lock 下任务仍在 src_dsq,但又不属于 sch"。二者在 race 窗口里结果截然不同,BPF scheduler 作者需要重新理解这条错误信息的触发条件。
  • 错误信息仍保留原文:补丁只是把检查挪位置,错误文本 scx_bpf_dsq_move[_vtime]() on %s[%d] but the task belongs to a different scheduler 不变,便于日志/告警向后兼容。
  • 注释修复是顺手活:把 sched_ext_free() 改成 sched_ext_dead() 表明这是一个长期遗留问题,未来在文档或注释里若再次出现旧名字,应一并清理,避免误导。
  • 回归测试:建议社区在 scx 测试套件里加 race 复现——例如任务在 dispatch 迭代期间被 rehome 到另一个 sub-sched,验证新版不会 abort。
  • 潜在 follow-upscx_dsq_move_vtime() 与该函数共享同一路径(错误信息里两个名字并列),需要确认其调用路径是否有同样的提前检查;若存在需一并后移。

版本变化

本系列只有 v1(for-7.3-fixes 分支),随后被 maintainer 直接打上"Applied to sched_ext/for-7.3-fixes",无后续迭代。

一句话总结

scx_dsq_move() 中过早的"任务所有权"检查推迟到加锁且确认任务仍在源 DSQ 之后,使良性所有权变更不再触发 scheduler abort,同时顺手修掉两处仍引用旧名 sched_ext_free() 的陈旧注释。