0/17 已展开

LLM 分析

sched_ext 带宽受限的 rescue 执行机制 (PATCHSET v2)

系列基线信息

明确目的

子调度器(sub-sched)只持有父级授予的 cids,没有机制保证这些 cids 覆盖任务 affinity。当任务可运行 cpu 不落在任何授权 cid 上时,目前没有好结局:调度器插入请求被 cap-reject,循环 reenqueue 直到重试上限驱逐调度器,或者任务卡住触发 stall watchdog。exiting 任务更糟,会跳过 ops.enqueue() 进入自重排死循环。

本系列在 kernel 侧加入 rescue 执行路径:调度器在可能 cap-reject 的 local DSQ insert 上打 SCX_ENQ_RESCUE 标记,kernel 改用一受限带宽直接驱动任务运行;当救援队列长时间过载时,按救援消耗驱逐真正的肇事 sub-sched 而不是误罚等待者 owner。

遍历代码(按 patch)

按邮件顺序:

0001 — 重命名 scx_local_or_reject_dsq → scx_resolve_local_dsq
仅为改名(cleanup)。后续 patch 让该函数多一种返回目的(rescue DSQ),原名"local or reject"列举了所有结局不够中性。

0002 — 让 ext.c 几个 helper 暴露到 ext.c 之外
set_task_slicetask_unlink_from_dsqmove_local_task_to_local_dsqinit_dsqdump_linescx_sched_all 等从 static 改成全局 scx_xxx,供 sub.c 在救援路径里调用。

0003 — 抽出 __scx_bpf_now(struct rq *rq)
scx_bpf_now() 把 rq 锁定在 this_rq()。抽出 __scx_bpf_now(rq) 后,sub.c 救援路径可以针对被持有 rq lock 的目标 rq 读取时钟。

0004 — 在 dsq move kfunc 中拒绝内部 enq_flags
dsq insert kfunc 之前在 scx_dsq_insert_preamble()__SCX_ENQ_INTERNAL_MASK,但 dsq move kfunc 走的是另一条 vet 路径;BPF 调度器可借此塞入 SCX_ENQ_CLEAR_OPSS 等内部位。把检查挪到 scx_vet_enq_flags(),让所有进入路径都过同一关。vtime move 之前在 vet 前 OR 入了 SCX_ENQ_DSQ_PRIQ,现在改为 vet 之后再置位。

0005 — SCX_ENQ_IGNORE_CAPS 同时豁免 preemption cap
该 flag 标记 kernel 强制 placement,但 preemption 路径仍通过 scx_caps_for_preempt() 检查导致不一致。给 scx_caps_for_preempt()enq_flags 参数,IGNORE_CAPS 时返回 0(不需要任何 cap)。

0006 — 同步 slice 与 dsq_vtime 写入
核心改动:dsq insert kfunc 不再直接写 slice/dsq_vtime,而由 verdict 携带数值,在插入时通过 apply_slice_vtime() 一次性 commit。

static void apply_slice_vtime(struct task_struct *p, u64 slice, u64 vtime, u64 enq_flags)
{
    if (slice) {
        p->scx.slice = slice;
        if (!(enq_flags & SCX_ENQ_SLICE_DFL))
            clear_task_slice_oob(p);
    } else if (!p->scx.slice) {
        p->scx.slice = 1;
    }
    if (enq_flags & SCX_ENQ_DSQ_PRIQ)
        p->scx.dsq_vtime = vtime;
}

并新增 SCX_ENQ_APPLY_SLICE 携带位,让 direct_dispatch/dispatch_to_local_dsq 等场景把预存于 ddsp_slice/ddsp_vtime 的 verdict 数值带到提交点。

0007 — 引入 SCX_TASK_PROTECTED
slice/DSQ 头部位置保护标志。kernel 授予的 slice 在 set_task_slice_keep_oob() 拒绝 BPF 进一步改写;dsq_insert_head() 对 rq-owned DSQ 把 protected 任务留在队首,新任务插到它们后面。新 scx_task_slice_ended() 在 slice 耗尽/dequeue/yield/bypass 时清理 PROTECTED 位。

0008 — 带宽受限的 rescue 执行(核心特性)

  • 新增内置 DSQ SCX_DSQ_RESCUESCX_ENQ_RESCUE flag。
  • scx_resolve_local_dsq() 里:当 cap 检查失败且 *enq_flags & SCX_ENQ_RESCUE,把任务放进 rescue DSQ,记录 rescue_at = jiffies
  • 每 CPU 一个 token bucket,预算为 rescue_bandwidth_ppt (默认 20,即 2%),每个救援获得 rescue_quantum_us (默认 5ms) 的 slice;队列满则按等待者数均分(最低 1ms)。
  • scx_rescue_charge()update_curr_scx() 中按实际执行时间扣 bucket。
  • 救援期任务挂在 local DSQ 尾部,不抢优先级,调度器可正常 preempt/reslice。
  • 服务超时则升级为 protected execution(SCX_TASK_PROTECTED)抢占当前任务。
  • 暴露 sched_ext_ops 两个 root-only 旋钮:rescue_bandwidth_pptrescue_quantum_us,取值受 SCX_RESCUE_MIN/MAX_* 范围约束。
  • SCX_RESCUE_DISABLE 关闭救援:带 RESCUE 的 insert 按普通 cap-reject 走。
  • 新事件 SCX_EV_SUB_RESCUE 统计救援触发次数;SCX_OPS_OPEN() 加 compat 修复让旧 kernel 不识别新字段时报错。

0009 — 过载时驱逐救援消耗最多的 sub-sched

  • 新增 SCX_EXIT_ERROR_RESCUE 退出原因。
  • scx_sched_pcpurescue_avgrescue_avg_at (jiffies_64),每次救援 charge 时 decay_avg() + delta_exec
  • scx_rescue_check_overload() 由 rescue timer 触发:当队首等待者 rescue_at + overload_threshold 仍未获得服务,按 rescue_avg 选最大者驱逐;设有 kill_at grace 期防止反复踢人。
  • scx_rescue_set_knobs() 根据预算周期算 overload_after(clamp 到 1s–15s)与 decay_halflife,并在 watchdog timeout ≤ 过载阈值时 pr_warn,避免 scheduler 先于救援机制被 stall-kill。

0010 — 同步 tools 自动生成的 enum 头文件
机械更新 enum_defs.autogen.henums.autogen.bpf.henums.autogen.h,新增 HAVE_SCX_* 宏与 SCX_ENQ_RESCUE

0011 — scx_qmap 对 pinned 任务做 idle check
pick_direct_dispatch_cid() 之前对 nr_cpus_allowed == 1 的 pinned 任务直接走 prev_cid 直派,共享 cid 上的直派被强制 IMMED 可能落到忙 cpu 而被 kernel 回弹,进入 reenqueue 循环。删除该旁路,让 pinned 任务也走 idle 检测。always_enq_immed rodata 镜像随之删除(已无读者);-I 仍设 SCX_OPS_ALWAYS_ENQ_IMMED

0012 — scx_qmap 接入 rescue 支持

  • qmap_enqueue 检测任务可运行 cid 与 self cids 无交集时,强制 IMMED 派到首个允许 cid 的 local DSQ 并附 SCX_ENQ_RESCUE
  • 新函数 scan_shared_dsq() 替代 dispatch_highpri():扫描 SHARED_DSQ 时,把 stranded 任务救援出去;HIGHPRI_DSQ cull 后若变 stranded 也走 rescue。
  • 命令行加 -B PPT(救援带宽,0 映射 SCX_RESCUE_DISABLE)和 -q US(quantum)。
  • 新增 nr_rescue_dsp 统计并在 hier 状态行打印 rescue=+N

ASCII 流程图

              task 唤醒 / reenqueue
                       │
                       ▼
        scx_resolve_local_dsq(sch, rq, p, *enq_flags)
                       │
              cap 检查 scx_missing_caps
                ┌──────┴──────┐
              通过           失败 + RESCUE
                │                │
                ▼                ▼
         SCX_DSQ_LOCAL    SCX_DSQ_RESCUE  (记录 rescue_at)
                │                │
                ▼                ▼
       正常调度路径        rescue 桶/计时器
                                │
                          每 tick 检查
                                │
                ┌───────────────┼──────────────────┐
                ▼               ▼                  ▼
           slice=0         仍有等待者           超载过载阈值
                │               │                  │
                ▼               ▼                  ▼
        rescuee 结束      token 允许 →         选 rescue_avg
        或提升              put_prev requeue     最大的 sub-sched
                           运行 quantum             并 __scx_exit
                                                     (ERROR_RESCUE)
 排队/调度器视角 ─────────────── 调度器控制面 ─────────────── rescuee 视角
                              ┌────────────────┐
   dsq_insert_head()    ──►   │ 保护队首任务    │ ◄── dsq_insert_head()
                              │ SCX_TASK_PROT. │
   BPF 试图改写其 slice ──►   │   拒绝 + 计 EV   │
                              └────────────────┘

   ┌─────────────┐    到达 rescue 队列     ┌─────────────┐
   │ RESCUE 队列 │  ────────────────────► │   rescuee   │
   │  尾插/无优先 │                        │  普通调度器 │
   └─────────────┘                        │  可 preempt │
                                          └─────────────┘
      反馈回路: 救援消耗 → 平均值 → 过载判定
   rescue_charge() ─► pcpu->rescue_avg = decay_avg() + delta
                              │
                              ▼
                     scx_rescue_check_overload()
                              │
              头等待 rescue_at 早于 overload_after 且
              rq->scx.rescue.kill_at 已过 grace
                              │
                              ▼
              遍历 scx_sched_all, 选 avg 最大且非 EXITING
                              │
                              ▼
                   __scx_exit(victim, ERROR_RESCUE)

概念类比

把 sched_ext 的 sub-sched 与 cap 机制想成 一栋楼的会议室预订系统:每个 sub-sched 是某个部门,只能用公司分给本部门的会议室(cids)。如果某员工(task)只接受某些楼层的会议室,而本部门没拿到那几层,他就没地方开会。旧版系统只会让员工原地等待、再尝试(reenqueue 循环),直到看门大爷(watchdog)把整个部门赶走。

新引入的"rescue"相当于 物业值班室:当部门确实订不到合适的会议室时,员工可以去物业的共享小会议室(rescue DSQ)开短会,物业只给一小块时间(默认 2% CPU 时间),但保证员工有进展。如果某部门老是把员工推到物业会议室,物业会按"过去一周向物业借了多少时间"算账,把最爱借的部门请走(驱逐肇事 sub-sched),而不是冤枉借不到会议室的员工所在部门。

SCX_TASK_PROTECTED 类似 "请勿打扰"门牌:kernel 给救援任务开门后,BPF 调度器不能擅自压缩其会议时间,也不能把它从队首挤掉;会议结束(slice 用尽)门牌自动摘下。

Highlight 突出问题

  • 救援只对 root 默认开启:子调度器若要享受 kernel 兜底,必须在 root 层配 rescue_bandwidth_ppt;root 默认 2% 是保守值,对 latency-critical 负载可能仍嫌激进。
  • SCX_RESCUE_DISABLE 与 cap-reject 的语义等价:关闭后所有 RESCUE insert 退回普通 cap-reject,可能进入 reenqueue 循环直到退出调度器;qmap -B 0 已演示该路径。
  • overload_after 与 watchdog timeout 不匹配时会先被 stall-kill:kernel 主动 pr_warn,但不会强制更改配置;sub-sched 作者仍需自己调高 watchdog 或降低救援预算周期。
  • 救援升级为 protected 时仍可被调度器 rebalance:升级仅限 slice 与 DSQ 头部保护;调度器仍可移动任务到其他 cpu,需要结合 sub-sched 的 cap 设计。
  • 新增 SCX_ENQ_APPLY_SLICE 路径改动语义:BPF 调度器如果旧逻辑依赖 dsq insert kfunc 直接修改 p->scx.slice,升级后行为改变;scx_qmap 已经适配,其它示例调度器需要相应更新。
  • 救援消耗按实际 CPU 时间计费:slice 被调度器主动缩短不会延长救援;可能放大 sub-sched 之间的资源争议。

版本演进 (v1 → v2)

  • v2 引用 sashiko AI 评审意见:
    • scx_sched_all 移出 CONFIG_EXT_SUB_SCHED 块,因定义是无条件的(0002)。
    • rescue_at / rescue_avg_atjiffies_64,避免 32 位算术回绕(0009)。
    • 删除无人读的 always_enq_immed rodata 镜像(0011)。
  • 第 8 个 patch(rescue 主体)单独发了 v2:补 SCX_OPS_OPEN() 对新 ops 字段的兼容处理,让 cpu-form 调度器能在旧 kernel 加载。

与其他 patch 系列的关联

  • 基于 sched_ext/for-7.3 之上,与 EXT_SUB_SCHED 子调度器、cap 体系、SCX_DSQ_* 内置 DSQ 紧密耦合。
  • 上游依赖 cap / SCX_TASK_QUEUED / SCX_TASK_IN_CUSTODY 等语义;本系列未改 cap 检查本身,仅在 cap 拒绝后提供兜底。
  • sched_ext: Add SCX_TASK_PROTECTED(0007)共同形成"kernel 保护 + BPF 不可改写"的对称面。
  • scx_qmap 既是功能示范,也是救援路径的压力测试:-B 0 让 qmap 走纯 cap-reject 路径,-B 20(默认)走 rescue 路径,便于对比。

一句话总结

本系列为 sched_ext 子调度器在"任务无授权 cid 可跑"这种绝境提供 kernel 兜底的带宽受限救援执行、protected 升级与肇事驱逐三件套,同时修缮 slice/dsq_vtime 写入并发与内部 flag 校验,整组改动让 stranded 任务不再以调度器被 watchdog 驱逐告终。