0/5 已展开

LLM 分析

sched-ext scx_nest:bpf_timer_cancel()select_cpu() 中无法使用

系列概况

  • 标题[scx_nest] bpf_timer_cancel() unusable from select_cpu()
  • 作者:yaoyiqi (A) yaoyiqi@huawei.com,评审者 Zhan Xusheng zhanxusheng@xiaomi.com
  • 版本:单封 patch(无 v1/v2 编号),但 thread 内含三轮评审迭代
  • 规模:仅修改 scheds/c/scx_nest.bpf.c 一个文件,diff 涉及 compact_primary_corenest_select_cpu()->migrate_primary: 两个 hunk
  • 修改文件scheds/c/scx_nest.bpf.c
  • 代码统计:两处 hunk,净增约 20 行(具体行数由 diff 可见)
  • Message-ID6aecefa9aee94aa3a029b66f91692e7c@huawei.com(首封),其余 4 封为回复
  • 完整性:邮件正文只展示 diff 与开头几句话,完整 commit message 与 Signed-off-by 行未截到

补丁目的

scx_nest 调度器在 nest_select_cpu()migrate_primary 分支里调用 bpf_timer_cancel(),试图撤销一个之前为 primary 核心安排的 compact 定时器。但在新内核中,select_cpu() 实际上是在 try_to_wake_up() 路径里、以 raw_spinlock_irqsave(&p->pi_lock) 持有的 IRQ-disabled 上下文里执行的,defer_timer_wq_op() 因此返回 true,bpf_timer_cancel() 的同步路径无法直接取消,反而会回 -ECANCELED。补丁要解决的就是这个矛盾:

  1. 把"取消定时器"的副作用从 select_cpu() 中彻底移除,避免在 IRQ-disabled 上下文里调用不可用的 helper。
  2. 改为在定时器回调里通过 scheduled_compaction 标志位判断是否仍然需要 demote,让悬挂回调自动 short-circuit。
    3.同步修正回调里 stat_inc(CALLBACK_COMPACTED) 的统计位置,让计数器语义保持一致。

旧流程的问题

            +-----------------------------+
migrate_primary (in select_cpu, IRQ off)
            | |
            v |
   bpf_timer_cancel(&timer)  --<-- returns -ECANCELED here
            |                             |
            v                             |
   bpf_timer_set_callback(..., compact_primary_core)
            |                             |
            v                             |
 scx_bpf_error("Failed to cancel pcpu timer")  <-- wrong on -ECANCELED

旧实现假设同步取消总能成功,没有考虑 select_cpu() 是从带 IRQ-disabled 的 p->pi_lock 进入的。当 cancel 路径因 defer_timer_wq_op() 返回 -ECANCELED 时,旧代码把它当成错误上报,但 -ECANCELED 只是"将异步取消"的合法告知。

新流程

migrate_primary (in select_cpu, IRQ off)
            |
            v pcpu_ctx->scheduled_compaction = 0
 /* 不再调用 bpf_timer_cancel() */
            |
            v
   ...继续 select_cpu() 主流程...
            |
            | (延迟一段时间后)
            v
   compact_primary_core timer callback fires
            |
            v
   if (!pcpu_ctx->scheduled_compaction)
 return 0; <-- 旧悬挂请求自动作废
   stat_inc(NEST_STAT(CALLBACK_COMPACTED));
   ...实际 demote 逻辑...

关键点:把 cancel 的同步动作改成"标记作废 + 回调自检"。这把锁外同步工作搬到了原本就会在 timer softirq 里执行的回调里,避开了 IRQ-disabled 限制。

Patch 概览

diff --git a/scheds/c/scx_nest.bpf.c b/scheds/c/scx_nest.bpf.c
@@ -195,16 +195,27 @@ static int compact_primary_core(...)
- stat_inc(NEST_STAT(CALLBACK_COMPACTED));
-    /* ...old comment: timer never cancelled -> demote... */
+    /* 注释更新:select_cpu() 持有 p->pi_lock(raw_spinlock_irqsave),
+       始终 IRQ-disabled,无法同步取消定时器。改为检查 scheduled_compaction */
+    if (!pcpu_ctx->scheduled_compaction)
+        return 0;
+    stat_inc(NEST_STAT(CALLBACK_COMPACTED));
@@ -356,11 +363,14 @@ migrate_primary:
-    if (bpf_timer_cancel(&pcpu_ctx->timer) < 0)
-        scx_bpf_error("Failed to cancel pcpu timer");
-    if (bpf_timer_set_callback(&pcpu_ctx->timer, compact_primary_core))
-        scx_bpf_error("Failed to re-arm pcpu timer");
+    pcpu_ctx->scheduled_compaction = 0;
+    /* 不再调用 bpf_timer_cancel/set_callback */

关键实现

// 关键判断:定时器回调里检测 scheduled_compaction 是否仍为有效请求
static int compact_primary_core(void *map, int *key, struct bpf_timer *timer)
{
    /* The core may have been re-promoted to the primary nest while this
     * timer was pending (see migrate_primary in nest_select_cpu()). We no
     * longer cancel the timer from there: select_cpu() is invoked with
     * p->pi_lock held (its callers take it via raw_spinlock_irqsave() and
     * thus always run with local IRQs disabled, regardless of the entry
     * context), where the timer-cancel synchronization isn't available.
     * Instead the pending callback detects that scheduled_compaction was
     * cleared and bails out. Only demote the core if a compaction is still
     * actually scheduled.
     */
    if (!pcpu_ctx->scheduled_compaction)
        return 0;
    stat_inc(NEST_STAT(CALLBACK_COMPACTED));
    /* ...真正 demote 逻辑... */
}
// select_cpu() 的 IRQ-disabled 上下文路径(Zhan Xusheng 在评审中给出)
// kernel/sched/core.c:4302  try_to_wake_up()
//   scoped_guard(raw_spinlock_irqsave)
//   select_task_rq() @4403
// -> sched_class->select_task_rq  @ 3629
//     -> select_task_rq_scx()
// -> SCX_CALL_OP_TASK_RET(sch, select_cpu, ...)  ext.c:3530/3558

// kernel/sched/core.c:4957  wake_up_new_task()
//   raw_spin_lock_irqsave()
//   select_task_rq()  @ 4968

// kernel/sched/core.c:5639  sched_exec()
//   scoped_guard(raw_spinlock_irqsave)
//   select_task_rq()  @ 5640

// 所有路径都满足 irqs_disabled(),所以 defer_timer_wq_op() 永远 true。

类比

这就像已经按下电梯"关门"按钮的乘客,现在被重新叫回"开门"。

原方案是"现在立刻用钥匙把关门定时器拆掉"——但你正好站在消防通道里,手里没钥匙(IRQ-disabled),强行去拆只会得到一个"稍后异步拆"的回执(-ECANCELED)。

新方案改成了:先在门口贴张"刚才那个请求作废"的便条(清 scheduled_compaction),等电梯控制器巡检到定时器到期时,自己看到便条就知道"哦,那个人改主意了,关门动作取消"。

这是从"强行同步取消"变成"标记 +回调自检"的协作模式,把无法在锁外做的事搬进了锁内本来就允许的回调里。

       原方案                       新方案
   select_cpu 时                  select_cpu 时
   +----------------+ +----------------+
   | 强行拆定时器   |             | 贴"作废"便条   |
   | 钥匙没有       |             | scheduled_     |
   | 失败->报错     |             | compaction=0   |
   +----------------+             +----------------+
                                          |
                                          v
                                  定时器到期回调
                                  +----------------+
                                  | 看到便条?      |
                                  | return 0       |
                                  | 否则 demote    |
                                  +----------------+

Highlight:风险与注意点

  • stat_inc 顺序错误(旧版位置):回调里如果先 stat_inc(CALLBACK_COMPACTED) 再检查 scheduled_compaction,会把"已经作废的悬挂到期"误计为一次真实 compact,并且 SCHEDULED_COMPACTION != CANCELLED_COMPACTION + CALLBACK_COMPACTED 的等式会失衡。补丁已把 stat_inc 移到 if 之后。
  • scheduled_compaction 的清零与重设时机:在 migrate_primary 里只是清掉标志,下一次 select_cpu 重新调度 compaction 时会再次置位,但前提是所有置位路径都被审计过——例如 dispatch 路径或 hotplug 处理里是否也有相同隐患,需要全局复查。
  • upstream 的对偶 race:upstream kernel 的回调只在末尾清 scheduled_compaction,如果 cancel 落在回调执行期间,同样可能误触发;这版只在 scx_nest 用户态调度器层规避,没修上游,但 README/changelog 里值得提一笔。
  • 不一定要套到所有 scx 调度器:仅当 BPF 程序需要在 select_cpu 里操纵 bpf_timer 才需要类似改造;其他 ops(如 enqueuedispatch)可睡眠,cancel 是可用的。
  • IRQ-disabled 的真正来源:是 p->pi_lock 调用方持锁带来的,不是 wakeup 由中断驱动;sched_exec() 也不是 wakeup,但同样落在 IRQ off 路径上。这点决定了"非睡眠上下文"这一描述是不够精确的。

版本变化

虽然邮件未声明版本号,整条 thread 实际上是一份 patch 在评审中的三轮迭代:

  • 第 1 版(index 1):核心改动到位,注释里写"non-sleepable context"。
  • 第 2 版(index 3 反馈后):注释改为更精确的"select_cpu() runs in the task wakeup path ... can be entered with local IRQs disabled or from hardirq contexts",并提前把 stat_inc(CALLBACK_COMPACTED) 移到 if 之后。
  • 第 3 版(index 4 反馈后,index 5)
    • 保留 stat_inc 下移;
    • 注释进一步收紧到"p->pi_lock held (its callers take it via raw_spinlock_irqsave())",强调"always run with local IRQs disabled, regardless of the entry context",去掉了"can";
    • 明确"非睡眠"并非本质,select_task_rq()core.c:3626lockdep_assert_held(&p->pi_lock),调用方是 IRQ-off 持锁的源头。

一句话总结

scx_nestselect_cpu()(IRQ-disabled + p->pi_lock 上下文)里调用 bpf_timer_cancel() 不可行,补丁改为清掉 scheduled_compaction 标志并让定时器回调自检作废,从而绕开同步取消限制并修正 compact 计数器语义。