sched-ext discussion
[scx_nest] bpf_timer_cancel() unusable from select_cpu()
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_core与nest_select_cpu()->migrate_primary:两个 hunk - 修改文件:
scheds/c/scx_nest.bpf.c - 代码统计:两处 hunk,净增约 20 行(具体行数由 diff 可见)
- Message-ID:
6aecefa9aee94aa3a029b66f91692e7c@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。补丁要解决的就是这个矛盾:
- 把"取消定时器"的副作用从
select_cpu()中彻底移除,避免在 IRQ-disabled 上下文里调用不可用的 helper。 - 改为在定时器回调里通过
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(如enqueue、dispatch)可睡眠,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:3626有lockdep_assert_held(&p->pi_lock),调用方是 IRQ-off 持锁的源头。
- 保留
一句话总结
scx_nest 在 select_cpu()(IRQ-disabled + p->pi_lock 上下文)里调用 bpf_timer_cancel() 不可行,补丁改为清掉 scheduled_compaction 标志并让定时器回调自检作废,从而绕开同步取消限制并修正 compact 计数器语义。