Skip to content

Latest commit

 

History

History
2560 lines (1978 loc) · 99 KB

File metadata and controls

2560 lines (1978 loc) · 99 KB

Linux sched_ext(BPF 可扩展调度器)深度分析

基于 Linux kernel 源码分析,主要文件:

  • kernel/sched/ext.c
  • kernel/sched/ext_internal.h
  • include/linux/sched/ext.h
  • kernel/sched/ext.h
  • kernel/sched/ext_idle.c
  • kernel/sched/ext_idle.h
  • tools/sched_ext/scx_simple.bpf.c
  • tools/sched_ext/scx_flatcg.bpf.c
  • tools/sched_ext/scx_central.bpf.c
  • tools/sched_ext/scx_qmap.bpf.c
  • tools/sched_ext/scx_pair.bpf.c

目录

  1. 设计目标与背景
  2. 整体架构
  3. 核心数据结构
    • 3.1 sched_ext_ops:操作回调表
    • 3.2 sched_ext_entity:任务调度实体
    • 3.3 scx_dispatch_q:分发队列
    • 3.4 scx_sched:调度器实例
    • 3.5 scx_dsp_ctx:分发缓冲区上下文
    • 3.6 scx_rq:per-CPU 运行队列扩展
  4. 任务状态机
    • 4.1 scx_task_state:任务初始化状态
    • 4.2 scx_ops_state:任务所有权状态机
  5. DSQ(Dispatch Queue)体系
    • 5.1 内置 DSQ:SCX_DSQ_GLOBAL / SCX_DSQ_LOCAL
    • 5.2 自定义 DSQ
    • 5.3 DSQ ID 编码格式
    • 5.4 FIFO 与 vtime 优先队列
    • 5.5 DSQ 生命周期管理
  6. 调度回调函数详解
    • 6.1 select_cpu:CPU 亲和性选择
    • 6.2 enqueue:任务入队
    • 6.3 dequeue:任务出队
    • 6.4 dispatch:任务分发
    • 6.5 running / stopping:执行状态通知
    • 6.6 runnable / quiescent:可运行状态通知
    • 6.7 tick:周期性时钟
    • 6.8 yield:主动让出 CPU
    • 6.9 init / exit:调度器生命周期
    • 6.10 init_task / exit_task:任务生命周期
    • 6.11 enable / disable:任务 SCX 开关
    • 6.12 cpu_acquire / cpu_release:CPU 控制权
    • 6.13 update_idle:空闲状态通知
    • 6.14 dump / dump_cpu / dump_task:调试信息转储
  7. 任务分发路径
    • 7.1 直接分发(Direct Dispatch)
    • 7.2 延迟分发
    • 7.3 dispatch_enqueue 实现
    • 7.4 balance_scx 分发循环
  8. 与 CFS 的共存机制
    • 8.1 sched_class 优先级链
    • 8.2 Bypass 模式
    • 8.3 SCX_OPS_SWITCH_PARTIAL:部分切换
  9. Watchdog 超时机制
    • 9.1 设计原理
    • 9.2 双重检测:delayed_work + tick
    • 9.3 scx_exit_info 退出信息收集
  10. kfunc 权限控制
  11. BPF struct_ops 注册机制
    • 11.1 注册与注销流程
    • 11.2 bpf_scx_init_member:字段验证
    • 11.3 BPF 程序可写字段
    • 11.4 scx_enable 执行流程
  12. 典型实现分析
    • 12.1 scx_simple:全局加权 vtime 调度器
    • 12.2 scx_flatcg:扁平化 cgroup 层级调度器
    • 12.3 scx_central:中央 CPU 调度器
    • 12.4 scx_qmap:五级 FIFO 优先级队列
    • 12.5 scx_pair:SMT 感知 cgroup 对调度器
  13. 内置空闲 CPU 选择机制
    • 13.1 scx_idle 模块架构
    • 13.2 SMT 感知空闲 CPU 追踪
    • 13.3 NUMA 和 LLC 拓扑感知
    • 13.4 scx_bpf_select_cpu_dfl:默认 CPU 选择
  14. cgroup 与 cpuset 集成
    • 14.1 CONFIG_EXT_GROUP_SCHED:cgroup 权重支持
    • 14.2 cgroup 生命周期回调
    • 14.3 任务 cgroup 迁移
    • 14.4 带宽控制参数
  15. CPU 热插拔处理
    • 15.1 热插拔序列号机制
    • 15.2 cpu_online / cpu_offline 回调
    • 15.3 handle_hotplug 内核内部处理
  16. 全局变量与模块参数
    • 16.1 核心全局变量
    • 16.2 可调模块参数
    • 16.3 电源管理集成
  17. sysfs 接口
    • 17.1 /sys/kernel/sched_ext 属性
    • 17.2 调度器实例 kobject
  18. 性能事件统计
  19. 调试与诊断
    • 19.1 dump 回调
    • 19.2 print_scx_info
    • 19.3 trace 事件
    • 19.4 scx_softlockup / scx_hardlockup
  20. 开发注意事项
  21. 内核启用流程全景
    • 21.1 scx_enable_workfn 详细步骤
    • 21.2 scx_disable_workfn 清理流程
  22. 并发安全与锁模型
    • 22.1 锁层级
    • 22.2 RCU 的使用
    • 22.3 无锁快速路径
  23. 与上游社区的演进

1. 设计目标与背景

1.1 为什么需要 sched_ext

Linux 内核的调度器长期以 CFS(Completely Fair Scheduler)为主。CFS 作为通用调度器表现良好,但面对特定工作负载时,开发者希望能够定制调度策略。传统方式有三条路:

  1. 修改内核源码:代价高,维护困难,无法迭代部署
  2. 使用 cgroup 和 nice 值:能力有限,无法实现完全定制的调度逻辑
  3. 用户态调度(如 SCHED_DEADLINE):有一定灵活性但局限性多

sched_ext(Scheduler Extensions,缩写 SCX)由 Meta Platforms 的 Tejun Heo 和 David Vernet 于 2022 年提出并实现,核心思想是:允许 BPF 程序作为一个完整的调度器类(sched_class)运行在内核态,获得与 CFS 同等的调度权限,同时利用 BPF 的安全验证器保证内核稳定性

1.2 设计目标

  • 完整的调度策略自定义:BPF 程序可以实现从 CPU 亲和性选择到任务分发的全链路调度逻辑
  • 快速迭代:无需重新编译内核即可加载/卸载新的调度策略
  • 安全性:BPF 验证器确保 BPF 程序不会破坏内核稳定性;watchdog 机制防止 BPF 调度器死锁导致系统挂起
  • 共存性:SCX 在调度类优先级链中处于 CFS 下方,实时任务(RT、DL)仍可抢占
  • 渐进迁移:通过 SCX_OPS_SWITCH_PARTIAL 可以只让部分任务(SCHED_EXT 策略)使用 BPF 调度器,其余任务仍走 CFS

1.3 内核配置

sched_ext 需要启用内核配置选项 CONFIG_SCHED_CLASS_EXT。可通过 kernel/sched/Kconfig 查看依赖关系。

cgroup 组调度支持需要额外启用 CONFIG_EXT_GROUP_SCHED,它依赖 CONFIG_CGROUP_SCHED


2. 整体架构

用户空间 BPF 程序 (scx_simple, scx_flatcg, scx_central, ...)
       |
       | bpf_struct_ops 注册 (bpf_scx_reg)
       v
+--------------------------------------------------+
|              sched_ext 内核框架                   |
|                                                  |
|  +----------------+    +---------------------+  |
|  | sched_ext_ops  |    |    scx_sched 实例    |  |
|  | (回调函数表)   |    | - ops (ops 副本)     |  |
|  | - select_cpu   |    | - global_dsqs[]      |  |
|  | - enqueue      |    | - dsq_hash           |  |
|  | - dequeue      |    | - exit_info          |  |
|  | - dispatch     |    | - has_op bitmap      |  |
|  | - running      |    | - pcpu 统计          |  |
|  | - stopping     |    +---------------------+  |
|  | - ...          |                              |
|  +----------------+                              |
|                                                  |
|  +------------------+  +--------------------+   |
|  |  DSQ 系统         |  |  Watchdog 机制     |   |
|  | SCX_DSQ_GLOBAL   |  | delayed_work       |   |
|  | SCX_DSQ_LOCAL    |  | scx_watchdog_work  |   |
|  | SCX_DSQ_BYPASS   |  | timeout: 30s max   |   |
|  | 用户自定义 DSQ   |  +--------------------+   |
|  +------------------+                            |
|                                                  |
|  +------------------------------------------+   |
|  |           ext_sched_class                 |   |
|  | enqueue_task_scx / dequeue_task_scx       |   |
|  | pick_task_scx / put_prev_task_scx         |   |
|  | select_task_rq_scx / task_tick_scx        |   |
|  | balance_scx (dispatch 循环)               |   |
|  +------------------------------------------+   |
+--------------------------------------------------+
       |
       | sched_class 优先级链
       v
+------------------+
| stop_sched_class |  (最高优先级)
| dl_sched_class   |
| rt_sched_class   |
| fair_sched_class | (CFS)
| ext_sched_class  | <-- sched_ext 在此位置
| idle_sched_class | (最低优先级)
+------------------+

sched_ext 以 ext_sched_class 的形式嵌入调度器类链中,位于 fair_sched_class(CFS)之下、idle_sched_class 之上。这意味着:

  • RT 和 DL 任务可以抢占 SCX 管理的任务
  • CFS 任务(SCHED_NORMAL,未启用 SCX_OPS_SWITCH_PARTIAL)也会被移入 SCX 管理
  • idle 任务优先级最低

3. 核心数据结构

3.1 sched_ext_ops:操作回调表

文件kernel/sched/ext_internal.h,第 272 行

struct sched_ext_ops 是 BPF 调度器与内核之间的契约接口,定义了所有可由 BPF 程序实现的回调函数:

// kernel/sched/ext_internal.h:272
struct sched_ext_ops {
    s32 (*select_cpu)(struct task_struct *p, s32 prev_cpu, u64 wake_flags);
    void (*enqueue)(struct task_struct *p, u64 enq_flags);
    void (*dequeue)(struct task_struct *p, u64 deq_flags);
    void (*dispatch)(s32 cpu, struct task_struct *prev);
    void (*tick)(struct task_struct *p);
    void (*runnable)(struct task_struct *p, u64 enq_flags);
    void (*running)(struct task_struct *p);
    void (*stopping)(struct task_struct *p, bool runnable);
    void (*quiescent)(struct task_struct *p, u64 deq_flags);
    bool (*yield)(struct task_struct *from, struct task_struct *to);
    bool (*core_sched_before)(struct task_struct *a, struct task_struct *b);
    void (*set_weight)(struct task_struct *p, u32 weight);
    void (*set_cpumask)(struct task_struct *p, const struct cpumask *cpumask);
    void (*update_idle)(s32 cpu, bool idle);
    void (*cpu_acquire)(s32 cpu, struct scx_cpu_acquire_args *args);
    void (*cpu_release)(s32 cpu, struct scx_cpu_release_args *args);
    s32 (*init_task)(struct task_struct *p, struct scx_init_task_args *args);
    void (*exit_task)(struct task_struct *p, struct scx_exit_task_args *args);
    void (*enable)(struct task_struct *p);
    void (*disable)(struct task_struct *p);
    void (*dump)(struct scx_dump_ctx *ctx);
    void (*dump_cpu)(struct scx_dump_ctx *ctx, s32 cpu, bool idle);
    void (*dump_task)(struct scx_dump_ctx *ctx, struct task_struct *p);
    // cgroup 相关 (CONFIG_EXT_GROUP_SCHED)
    s32 (*cgroup_init)(struct cgroup *cgrp, struct scx_cgroup_init_args *args);
    void (*cgroup_exit)(struct cgroup *cgrp);
    s32 (*cgroup_prep_move)(struct task_struct *p,
                            struct cgroup *from, struct cgroup *to);
    void (*cgroup_move)(struct task_struct *p,
                        struct cgroup *from, struct cgroup *to);
    void (*cgroup_cancel_move)(struct task_struct *p,
                               struct cgroup *from, struct cgroup *to);
    void (*cgroup_set_weight)(struct cgroup *cgrp, u32 weight);
    void (*cgroup_set_bandwidth)(struct cgroup *cgrp, u64 period_us,
                                  u64 quota_us, u64 burst_us);
    void (*cpu_online)(s32 cpu);
    void (*cpu_offline)(s32 cpu);
    s32 (*init)(void);
    void (*exit)(struct scx_exit_info *info);

    u32 dispatch_max_batch;    // dispatch() 每次最多分发的任务数
    u64 flags;                 // SCX_OPS_* 标志
    u32 timeout_ms;            // watchdog 超时(最大 30000ms)
    u32 exit_dump_len;         // exit_info.dump 缓冲区长度
    u64 hotplug_seq;           // CPU 热插拔序列号检测
    char name[SCX_OPS_NAME_LEN]; // 调度器名称(最多 128 字节)
};

所有回调都是可选的。通过 DECLARE_BITMAP(has_op, SCX_OPI_END) 位图(在 scx_sched 中)追踪哪些回调被实际实现,内核用 SCX_HAS_OP(sch, op) 宏(kernel/sched/ext.c 第 220 行)快速判断:

// kernel/sched/ext.c:220
#define SCX_HAS_OP(sch, op)  test_bit(SCX_OP_IDX(op), (sch)->has_op)

其中 SCX_OP_IDX 定义为:

// kernel/sched/ext_internal.h:8
#define SCX_OP_IDX(op)  (offsetof(struct sched_ext_ops, op) / sizeof(void (*)(void)))

关键操作标志scx_ops_flagskernel/sched/ext_internal.h 第 109 行):

标志 含义
SCX_OPS_KEEP_BUILTIN_IDLE bit 0 即使实现了 update_idle,也保持内置空闲 CPU 跟踪
SCX_OPS_ENQ_LAST bit 1 当 CPU 只剩当前任务时,通过 enqueue 传递而不是自动续期
SCX_OPS_ENQ_EXITING bit 2 允许 enqueue 处理退出中的任务(默认直接分发到 local DSQ)
SCX_OPS_SWITCH_PARTIAL bit 3 只有 SCHED_EXT 策略的任务使用 SCX,SCHED_NORMAL 任务继续用 CFS
SCX_OPS_ENQ_MIGRATION_DISABLED bit 4 迁移禁用的任务也通过 enqueue 处理
SCX_OPS_ALLOW_QUEUED_WAKEUP bit 5 启用 ttwu_queue 唤醒优化
SCX_OPS_BUILTIN_IDLE_PER_NODE bit 6 使用 per-NUMA-node 的空闲 CPU 掩码
SCX_OPS_HAS_CGROUP_WEIGHT bit 16 已废弃,将在 6.18 移除
SCX_OPS_HAS_CPU_PREEMPT bit 56 内部标志,有 cpu_acquire/release 时自动设置

注意:高 8 位(bits 56-63)是内部标志,不包含在 SCX_OPS_ALL_FLAGS 中,BPF 程序不能直接设置(ext_internal.h 第 192 行)。

3.2 sched_ext_entity:任务调度实体

文件include/linux/sched/ext.h,第 163 行

struct sched_ext_entity(通常称为 scx)嵌入在每个 task_struct 中,保存 SCX 调度所需的所有状态:

// include/linux/sched/ext.h:163
struct sched_ext_entity {
    struct scx_dispatch_q  *dsq;           // 当前所在的 DSQ
    struct scx_dsq_list_node dsq_list;     // DSQ 链表节点(调度顺序)
    struct rb_node         dsq_priq;       // vtime 优先级队列红黑树节点
    u32                    dsq_seq;        // DSQ 序列号(用于 BPF 迭代)
    u32                    dsq_flags;      // SCX_TASK_DSQ_ON_PRIQ 等
    u32                    flags;          // SCX_TASK_QUEUED 等(rq lock 保护)
    u32                    weight;         // 任务权重
    s32                    sticky_cpu;     // 强制分发到特定 CPU
    s32                    holding_cpu;    // 跨 CPU 迁移时的锁协调
    s32                    selected_cpu;   // select_cpu() 的返回值
    u32                    kf_mask;        // 允许调用的 kfunc 集合
    struct task_struct     *kf_tasks[2];   // kfunc 操作的目标任务
    atomic_long_t          ops_state;      // 所有权状态机(NONE/QUEUEING/QUEUED/DISPATCHING)

    struct list_head       runnable_node;  // 挂载到 rq->scx.runnable_list
    unsigned long          runnable_at;    // 变为可运行的时间(jiffies),watchdog 使用

    u64                    core_sched_at;  // core-sched 任务排序时间戳
    u64                    ddsp_dsq_id;    // 直接分发目标 DSQ ID
    u64                    ddsp_enq_flags; // 直接分发标志

    /* BPF 程序可修改的字段 */
    u64                    slice;          // 剩余时间片(纳秒),0 触发重新调度
    u64                    dsq_vtime;      // vtime 优先级队列排序键
    bool                   disallow;       // 拒绝该任务切换到 SCX
};

关键字段说明

  • slice:运行时预算(纳秒)。内核每 tick 扣减执行时间(update_curr_scxkernel/sched/ext.c 第 948 行),归零时触发调度事件。BPF 调度器通过 scx_bpf_dsq_insert() 设置时间片,默认值为 SCX_SLICE_DFL = 20msinclude/linux/sched/ext.h 第 30 行)。

  • dsq_vtime:虚拟时间戳,用于 vtime 优先级队列排序。BPF 调度器在 stopping 回调中更新:p->scx.dsq_vtime += (SCX_SLICE_DFL - p->scx.slice) * 100 / p->scx.weight(见 scx_simple.bpf.c 第 124 行)。

  • ops_stateatomic_long_t 类型的状态机,编码任务所有权(详见第 4.2 节)。

  • runnable_at:任务最近一次变为可运行的时间(jiffies),watchdog 用此判断任务是否长时间等待未被调度(详见第 9 节)。

  • disallow:BPF 可写字段,只有在 init_task() 的非 fork 路径中有效,设为 true 后内核会强制将该任务的调度策略恢复为 SCHED_NORMAL

BPF 可写字段的访问控制bpf_scx_btf_struct_access() 函数(kernel/sched/ext.c 第 5306 行)控制 BPF 程序只能写入 scx.slicescx.dsq_vtimescx.disallow 三个字段,其他 task_struct 字段只读:

// kernel/sched/ext.c:5313
if (t == task_struct_type) {
    if (off >= offsetof(struct task_struct, scx.slice) &&
        off + size <= offsetofend(struct task_struct, scx.slice))
        return SCALAR_VALUE;
    if (off >= offsetof(struct task_struct, scx.dsq_vtime) &&
        off + size <= offsetofend(struct task_struct, scx.dsq_vtime))
        return SCALAR_VALUE;
    if (off >= offsetof(struct task_struct, scx.disallow) &&
        off + size <= offsetofend(struct task_struct, scx.disallow))
        return SCALAR_VALUE;
}
return -EACCES;

3.3 scx_dispatch_q:分发队列

文件include/linux/sched/ext.h,第 71 行

// include/linux/sched/ext.h:71
struct scx_dispatch_q {
    raw_spinlock_t         lock;
    struct task_struct __rcu *first_task;  // 无锁快速访问头部任务
    struct list_head       list;           // FIFO 链表
    struct rb_root         priq;           // vtime 优先级红黑树
    u32                    nr;             // 队列中的任务数
    u32                    seq;            // 序列号(BPF 迭代器使用)
    u64                    id;             // DSQ ID
    struct rhash_head      hash_node;      // 哈希表节点(用于用户自定义 DSQ 查找)
    struct llist_node      free_node;      // 延迟释放链表节点
    struct rcu_head        rcu;
};

每个 DSQ 同时维护两个数据结构:

  • list:双向链表,支持 FIFO 调度(list_add_tail 入队尾,list_add 入队头)
  • priq:红黑树,支持 vtime 优先级调度(按 p->scx.dsq_vtime 排序)

注意:内置 DSQ(SCX_DSQ_GLOBALSCX_DSQ_LOCAL)只支持 FIFO,不允许 vtime 排序(kernel/sched/ext.c 第 1039 行的检查)。

DSQ 初始化由 init_dsq() 完成(kernel/sched/ext.c 第 3476 行):

// kernel/sched/ext.c:3476
static void init_dsq(struct scx_dispatch_q *dsq, u64 dsq_id)
{
    memset(dsq, 0, sizeof(*dsq));
    raw_spin_lock_init(&dsq->lock);
    INIT_LIST_HEAD(&dsq->list);
    dsq->id = dsq_id;
}

3.4 scx_sched:调度器实例

文件kernel/sched/ext_internal.h,第 887 行

// kernel/sched/ext_internal.h:887
struct scx_sched {
    struct sched_ext_ops   ops;            // ops 的内核副本
    DECLARE_BITMAP(has_op, SCX_OPI_END);   // 已实现的回调位图

    struct rhashtable      dsq_hash;       // 用户自定义 DSQ 哈希表
    struct scx_dispatch_q  **global_dsqs;  // per-NUMA-node 全局 DSQ 数组
    struct scx_sched_pcpu __percpu *pcpu;  // per-CPU 统计数据

    bool                   warned_zero_slice:1;
    bool                   warned_deprecated_rq:1;

    atomic_t               exit_kind;      // 退出原因(SCX_EXIT_*)
    struct scx_exit_info   *exit_info;     // 详细退出信息

    struct kobject         kobj;           // /sys/kernel/sched_ext 接口
    struct kthread_worker  *helper;        // disable 工作队列
    struct irq_work        error_irq_work;
    struct kthread_work    disable_work;
    struct rcu_work        rcu_work;
};

全局 DSQ 按 NUMA 节点分割(global_dsqs 数组),scx_alloc_and_add_sched() 在启用时为每个 NUMA 节点分配一个(kernel/sched/ext.c 第 4865 行):

// kernel/sched/ext.c:4865
for_each_node_state(node, N_POSSIBLE) {
    dsq = kzalloc_node(sizeof(*dsq), GFP_KERNEL, node);
    init_dsq(dsq, SCX_DSQ_GLOBAL);
    sch->global_dsqs[node] = dsq;
}

这避免了 bypass 模式下所有 CPU 竞争同一把锁造成活锁,同时具备 NUMA 局部性优化效果。

任务所属的全局 DSQ 由 find_global_dsq() 查找(kernel/sched/ext.c 第 248 行):

// kernel/sched/ext.c:248
static struct scx_dispatch_q *find_global_dsq(struct scx_sched *sch,
                                               struct task_struct *p)
{
    return sch->global_dsqs[cpu_to_node(task_cpu(p))];
}

3.5 scx_dsp_ctx:分发缓冲区上下文

文件kernel/sched/ext.c,第 119 行

dispatch() 回调执行期间,BPF 程序通过 scx_bpf_dsq_insert() 批量提交任务到一个 per-CPU 分发缓冲区,而不是直接操作 DSQ(这样可以延迟加锁,提高批处理效率):

// kernel/sched/ext.c:110
struct scx_dsp_buf_ent {
    struct task_struct  *task;
    unsigned long       qseq;
    u64                 dsq_id;
    u64                 enq_flags;
};

// kernel/sched/ext.c:119
struct scx_dsp_ctx {
    struct rq    *rq;
    u32          cursor;    // 下一个写入位置
    u32          nr_tasks;  // 当前缓冲的任务数
    struct scx_dsp_buf_ent buf[]; // 弹性数组
};

缓冲区大小由 scx_dsp_max_batch 控制(等于 ops.dispatch_max_batch 或默认的 SCX_DSP_DFL_MAX_BATCH = 32)。flush_dispatch_buf()dispatch() 返回后将缓冲区中的任务实际插入目标 DSQ。

3.6 scx_rq:per-CPU 运行队列扩展

每个 CPU 的 struct rq 中内嵌了 struct scx_rq 字段,保存 SCX 相关的 per-CPU 状态:

struct scx_rq {
    struct scx_dispatch_q  local_dsq;      // 本地 DSQ(直接执行队列)
    struct scx_dispatch_q  bypass_dsq;     // bypass 模式专用 DSQ
    struct list_head       runnable_list;  // 可运行任务列表(watchdog 使用)
    struct list_head       ddsp_deferred_locals; // 延迟的 local DSQ 分发列表
    unsigned long          ops_qseq;       // 操作序列号(QUEUEING 状态使用)
    u64                    extra_enq_flags; // 额外的入队标志
    u32                    flags;          // SCX_RQ_* 标志
    u32                    cpuperf_target; // CPU 性能目标(DVFS 控制)
    bool                   cpu_released;
    struct irq_work        deferred_irq_work;
    struct balance_callback bal_cb;
};

关键标志(scx_rq_flags):

标志 含义
SCX_RQ_ONLINE CPU 在线,BPF 调度器可见
SCX_RQ_CAN_STOP_TICK 允许停止 tick(tickless 模式)
SCX_RQ_BAL_PENDING 有待处理的 balance 回调
SCX_RQ_IN_WAKEUP 正在唤醒任务(task_woken_scx 会处理 deferred)
SCX_RQ_IN_BALANCE 正在执行 balance(balance_scx 中)
SCX_RQ_BAL_KEEP balance 决定继续执行当前任务
SCX_RQ_BYPASSING bypass 模式激活
SCX_RQ_BAL_CB_PENDING balance 回调待添加

4. 任务状态机

4.1 scx_task_state:任务初始化状态

文件include/linux/sched/ext.h,第 98 行

// include/linux/sched/ext.h:98
enum scx_task_state {
    SCX_TASK_NONE,       // ops.init_task() 尚未调用
    SCX_TASK_INIT,       // ops.init_task() 成功,但任务可被取消(fork 路径)
    SCX_TASK_READY,      // 完全初始化,但不在 sched_ext 管理下
    SCX_TASK_ENABLED,    // 完全初始化,且在 sched_ext 管理下
};

这 4 个状态存储在 scx_entity.flags 的第 8-9 位(SCX_TASK_STATE_SHIFT = 8SCX_TASK_STATE_BITS = 2)。

状态转换由 scx_set_task_state() 管理(kernel/sched/ext.c 第 2845 行),该函数内含非法转换的 WARN_ONCE 检测:

NONE --> INIT   : ops.init_task() 调用时(fork 路径,fork=true)
INIT --> READY  : fork 完成后(fork 成功提交)
INIT --> NONE   : fork 被取消(ops.exit_task() 调用,cancelled=true)
READY --> ENABLED : 任务切换到 sched_ext(ops.enable() 调用)
ENABLED --> READY : 任务离开 sched_ext(ops.disable() 调用)
READY --> NONE  : 任务退出(ops.exit_task() 调用,cancelled=false)

非 fork 路径(调度器加载时批量初始化已有任务)会跳过 INIT 状态,直接从 NONE 进入 READY

4.2 scx_ops_state:任务所有权状态机

文件kernel/sched/ext_internal.h,第 1037 行

这是 sched_ext 最精妙的设计之一,用于在 SCX 内核核心与 BPF 调度器之间安全地转移任务所有权:

// kernel/sched/ext_internal.h:1141
enum scx_ops_state {
    SCX_OPSS_NONE,         // 任务由 SCX 内核核心拥有
    SCX_OPSS_QUEUEING,     // 正在从内核核心移交给 BPF 调度器
    SCX_OPSS_QUEUED,       // 任务由 BPF 调度器拥有(在某个 DSQ 上)
    SCX_OPSS_DISPATCHING,  // 正在从 BPF 调度器移回内核核心
};

状态机图(引自源码注释,ext_internal.h 第 1046 行):

      .------------> NONE (owned by SCX core)
      |               |           ^
      |       enqueue |           | direct dispatch
      |               v           |
      |           QUEUEING -------'
      |               |
      |       enqueue |
      |     completes |
      |               v
      |            QUEUED (owned by BPF scheduler)
      |               |
      |      dispatch |
      |               |
      |               v
      |          DISPATCHING
      |               |
      |      dispatch |
      |     completes |
      `---------------'

关键设计点

  1. QSEQ(队列序列号)SCX_OPSS_QUEUED 状态的高位嵌入一个序列号(SCX_OPSS_QSEQ_SHIFT = 2)。当任务被出队后再次入队时,QSEQ 递增(kernel/sched/ext.c 第 1396 行):
// kernel/sched/ext.c:1396
qseq = rq->scx.ops_qseq++ << SCX_OPSS_QSEQ_SHIFT;
atomic_long_set(&p->scx.ops_state, SCX_OPSS_QUEUEING | qseq);
  1. 内存序:从 QUEUEING/DISPATCHINGNONE/QUEUED 的转换必须使用 atomic_long_set_release(),等待方使用 atomic_long_read_acquire()ext_internal.h 第 1099 行),确保内存可见性。

  2. 忙等待:当分发路径需要等待 QUEUEINGDISPATCHING 状态结束时,使用 wait_ops_state() 函数进行忙等待(kernel/sched/ext.c 第 759 行):

// kernel/sched/ext.c:759
static void wait_ops_state(struct task_struct *p, unsigned long opss)
{
    do {
        cpu_relax();
    } while (atomic_long_read_acquire(&p->scx.ops_state) == opss);
}

5. DSQ(Dispatch Queue)体系

5.1 内置 DSQ:SCX_DSQ_GLOBAL / SCX_DSQ_LOCAL

文件include/linux/sched/ext.h,第 53 行

// include/linux/sched/ext.h:53
enum scx_dsq_id_flags {
    SCX_DSQ_FLAG_BUILTIN  = 1LLU << 63,
    SCX_DSQ_FLAG_LOCAL_ON = 1LLU << 62,

    SCX_DSQ_INVALID   = SCX_DSQ_FLAG_BUILTIN | 0,
    SCX_DSQ_GLOBAL    = SCX_DSQ_FLAG_BUILTIN | 1,
    SCX_DSQ_LOCAL     = SCX_DSQ_FLAG_BUILTIN | 2,
    SCX_DSQ_BYPASS    = SCX_DSQ_FLAG_BUILTIN | 3,
    SCX_DSQ_LOCAL_ON  = SCX_DSQ_FLAG_BUILTIN | SCX_DSQ_FLAG_LOCAL_ON,
    SCX_DSQ_LOCAL_CPU_MASK = 0xffffffffLLU,
};

各内置 DSQ 的用途:

DSQ 描述
SCX_DSQ_GLOBAL 全局共享 FIFO 队列,按 NUMA 节点分片。任何 CPU 均可消费
SCX_DSQ_LOCAL 每个 CPU 私有的本地 FIFO 队列(rq->scx.local_dsq)。任务在此直接执行
SCX_DSQ_LOCAL_ON | cpu 将任务分发到指定 CPU 的本地 DSQ
SCX_DSQ_BYPASS 内部 bypass 模式专用 DSQ,BPF 调度器不可见
SCX_DSQ_INVALID 哨兵值,表示无效 DSQ(初始化状态)

本地 DSQ 的角色:内核调度器通过 pick_task_scx() 从本地 DSQ 取任务执行。BPF 调度器的 dispatch() 回调应将全局/用户 DSQ 中的任务移动到本地 DSQ(通过 scx_bpf_dsq_move_to_local())。

5.2 自定义 DSQ

BPF 调度器可以创建任意数量的用户自定义 DSQ,每个 DSQ 有唯一的 64 位 ID(不含最高位,即不与内置 DSQ 冲突)。

// scx_simple.bpf.c:39
#define SHARED_DSQ 0

// 在 ops.init() 中创建
ret = scx_bpf_create_dsq(SHARED_DSQ, -1);

用户 DSQ 存储在 scx_sched.dsq_hash 哈希表中,通过 find_user_dsq() 查找(kernel/sched/ext.c 第 254 行):

// kernel/sched/ext.c:254
static struct scx_dispatch_q *find_user_dsq(struct scx_sched *sch, u64 dsq_id)
{
    return rhashtable_lookup(&sch->dsq_hash, &dsq_id, dsq_hash_params);
}

用户 DSQ 支持两种模式:

  • FIFO(默认):按入队顺序消费
  • vtime 优先级队列:通过 scx_bpf_dsq_insert_vtime()p->scx.dsq_vtime 排序

注意:同一 DSQ 不能混用两种模式,内核会检查并报错(kernel/sched/ext.c 第 1056-1063 行)。

5.3 DSQ ID 编码格式

  Bits: [63] [62] [61..32] [31 ..  0]
        [ B] [ L] [   R  ] [    V   ]

  B: 1 = 内置 DSQ,0 = 用户自定义 DSQ
  L: 1 = LOCAL_ON DSQ(指向特定 CPU 的本地 DSQ)
  R: 保留
  V: 对 LOCAL_ON DSQ 为 CPU 编号(SCX_DSQ_LOCAL_CPU_MASK);
     对其他内置 DSQ 为预定义值(1=GLOBAL, 2=LOCAL, 3=BYPASS)

5.4 FIFO 与 vtime 优先队列

dispatch_enqueue() 函数(kernel/sched/ext.c 第 1017 行)实现了 DSQ 的入队逻辑:

if (enq_flags & SCX_ENQ_DSQ_PRIQ) {
    // vtime 优先队列入队:插入红黑树并维护 list 按 vtime 有序
    p->scx.dsq_flags |= SCX_TASK_DSQ_ON_PRIQ;
    rb_add(&p->scx.dsq_priq, &dsq->priq, scx_dsq_priq_less);
    // ...
} else {
    // FIFO 入队:普通尾插(或 SCX_ENQ_HEAD 时头插)
    if (enq_flags & (SCX_ENQ_HEAD | SCX_ENQ_PREEMPT))
        list_add(&p->scx.dsq_list.node, &dsq->list);
    else
        list_add_tail(&p->scx.dsq_list.node, &dsq->list);
}

vtime 比较使用 time_before64()kernel/sched/ext.c 第 974 行),正确处理 64 位时间戳回绕:

static bool scx_dsq_priq_less(struct rb_node *node_a, const struct rb_node *node_b)
{
    // ...
    return time_before64(a->scx.dsq_vtime, b->scx.dsq_vtime);
}

5.5 DSQ 生命周期管理

创建:BPF 程序调用 scx_bpf_create_dsq(dsq_id, node) kfunc,内核分配 scx_dispatch_q 并插入 dsq_hash

销毁:BPF 程序调用 scx_bpf_destroy_dsq(dsq_id) kfunc,对应内核的 destroy_dsq() 函数(kernel/sched/ext.c 第 3496 行):

  1. dsq_hash 中移除(rhashtable_remove_fast
  2. dsq->id 设为 SCX_DSQ_INVALID,阻止新的入队
  3. 通过 llist + irq_work 延迟释放(free_dsq_irq_workext.c 第 3494 行)
  4. 实际释放通过 kfree_rcu() 保证 RCU 安全

注意:如果试图销毁仍有任务的 DSQ,destroy_dsq() 会调用 scx_error() 触发调度器错误退出(ext.c 第 3510 行)。


6. 调度回调函数详解

6.1 select_cpu:CPU 亲和性选择

// kernel/sched/ext_internal.h:298
s32 (*select_cpu)(struct task_struct *p, s32 prev_cpu, u64 wake_flags);
  • 调用时机:任务被唤醒(ttwu,try-to-wake-up)时
  • 参数prev_cpu 为任务上次运行的 CPU;wake_flags 包含 SCX_WAKE_FORKSCX_WAKE_TTWUSCX_WAKE_SYNC
  • 返回值:建议的目标 CPU 编号(不最终决定,内核可能使用 fallback CPU)
  • 特殊能力:可在此回调中直接调用 scx_bpf_dsq_insert() 实现 direct dispatch,跳过后续的 enqueue() 回调
  • 限制:单 CPU 任务(migration disabled)不会触发此回调

典型用法(来自 scx_simple.bpf.c 第 55 行):

s32 BPF_STRUCT_OPS(simple_select_cpu, struct task_struct *p,
                   s32 prev_cpu, u64 wake_flags)
{
    bool is_idle = false;
    s32 cpu = scx_bpf_select_cpu_dfl(p, prev_cpu, wake_flags, &is_idle);
    if (is_idle) {
        stat_inc(0);
        scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL, SCX_SLICE_DFL, 0);
    }
    return cpu;
}

scx_centralselect_cpu 更简单——直接返回中央 CPU,让唤醒集中到 central_cpu(scx_central.bpf.c 第 90 行):

s32 BPF_STRUCT_OPS(central_select_cpu, struct task_struct *p,
                   s32 prev_cpu, u64 wake_flags)
{
    /* 将唤醒引导到 central CPU,尽量不打扰其他 CPU */
    return central_cpu;
}

唤醒优化(SCX_OPS_ALLOW_QUEUED_WAKEUP:当设置此标志时,启用 ttwu_queue 优化——内核通过 IPI 将 enqueue() 推迟到目标 CPU 上执行。此时 select_cpu() 在某些竞争场景下可能被跳过,BPF 调度器必须能够处理没有 select_cpu 前置的 enqueue()ext_internal.h 第 155-169 行)。

6.2 enqueue:任务入队

// kernel/sched/ext_internal.h:313
void (*enqueue)(struct task_struct *p, u64 enq_flags);
  • 调用时机:任务需要进入 BPF 调度器的等待队列时(do_enqueue_task 的 BPF 路径,kernel/sched/ext.c 第 1348 行)
  • 参数enq_flags 包含 SCX_ENQ_WAKEUPSCX_ENQ_PREEMPTSCX_ENQ_LAST
  • 要求:必须调用 scx_bpf_dsq_insert() 将任务放入某个 DSQ,否则任务将饥饿
  • 跳过条件
    • 任务在 select_cpu() 中已经 direct dispatch
    • 任务在退出中(PF_EXITING)且未设置 SCX_OPS_ENQ_EXITING
    • 任务的 migration 已禁用且未设置 SCX_OPS_ENQ_MIGRATION_DISABLED

do_enqueue_task 的完整流程(kernel/sched/ext.c 第 1348 行):

do_enqueue_task(rq, p, enq_flags, sticky_cpu)
    |
    +-- sticky_cpu == cpu_of(rq)?  --> local_norefill (直接加入本地 DSQ,无 slice 刷新)
    |
    +-- !scx_rq_online?  --> local (CPU 下线,直接加入本地 DSQ)
    |
    +-- scx_rq_bypassing?  --> bypass (bypass 模式,加入 bypass_dsq)
    |
    +-- ddsp_dsq_id != SCX_DSQ_INVALID?  --> direct (direct dispatch 已在 select_cpu 中完成)
    |
    +-- PF_EXITING 且 !SCX_OPS_ENQ_EXITING?  --> local
    |
    +-- migration_disabled 且 !SCX_OPS_ENQ_MIGRATION_DISABLED?  --> local
    |
    +-- !SCX_HAS_OP(enqueue)?  --> global (无 enqueue 回调,直接入全局 DSQ)
    |
    +-- 设置 QUEUEING 状态,调用 ops.enqueue()
    |
    +-- ddsp_dsq_id != SCX_DSQ_INVALID?  --> direct (enqueue 内 direct dispatch)
    |
    +-- ops_state == QUEUED?  --> 已由 enqueue 放入 DSQ
    |
    +-- fallback  --> 放入全局 DSQ(enqueue 未处理)

enq_flags 关键标志

标志 描述
SCX_ENQ_WAKEUP 任务从睡眠唤醒
SCX_ENQ_PREEMPT 分发到 local DSQ 时触发抢占(当前任务 slice 置零)
SCX_ENQ_LAST CPU 上只剩此任务(需要 SCX_OPS_ENQ_LAST 标志激活)
SCX_ENQ_REENQ 任务从 local DSQ 被重新入队(来自 scx_bpf_reenqueue_local()
SCX_ENQ_HEAD 插入 DSQ 头部而非尾部
SCX_ENQ_DSQ_PRIQ 使用 vtime 优先级队列模式
SCX_ENQ_CLEAR_OPSS 入队时清除 ops_state(内部使用)

6.3 dequeue:任务出队

// kernel/sched/ext_internal.h:329
void (*dequeue)(struct task_struct *p, u64 deq_flags);
  • 调用时机:从 BPF 调度器侧移除任务时(ops_dequeuekernel/sched/ext.c 第 1523 行),常见于优先级/亲和性变更
  • 注意:SCX 内核核心自动跟踪任务的 BPF 所有权状态,可安全忽略无效的分发,因此此回调不实现也不会导致崩溃,但可能导致调度位置不正确

deq_flags 关键标志

标志 描述
SCX_DEQ_SLEEP 任务进入睡眠
SCX_DEQ_CORE_SCHED_EXEC core-sched 层决定立即执行该任务
SCX_DEQ_MIGRATING 任务正在迁移到其他 CPU

6.4 dispatch:任务分发

// kernel/sched/ext_internal.h:352
void (*dispatch)(s32 cpu, struct task_struct *prev);
  • 调用时机:CPU 的 local DSQ 为空,需要补充任务时(类似 CFS 的 balance()
  • 参数cpu 为需要任务的 CPU;prev 为刚刚切换出去的前一个 SCX 任务(若非 NULL 且 SCX_TASK_QUEUED 置位,则任务尚未入队,可直接续期)
  • 职责:调用 scx_bpf_dsq_move_to_local() 将某个 DSQ 的任务移入 CPU 的 local DSQ

典型实现(来自 scx_simple.bpf.c 第 90 行):

void BPF_STRUCT_OPS(simple_dispatch, s32 cpu, struct task_struct *prev)
{
    scx_bpf_dsq_move_to_local(SHARED_DSQ);
}

每次调用 scx_bpf_dsq_insert() 且未跟随 scx_bpf_dsq_move_to_local() 时,消耗一个 "dispatch slot",最大批次数由 ops.dispatch_max_batch 控制(默认 SCX_DSP_DFL_MAX_BATCH = 32,见 ext_internal.h 第 11 行)。

dispatch 循环的防死锁保护kernel/sched/ext.c 第 2254 行):若 dispatch() 重复提交无法运行的任务(不满足亲和性等),导致循环次数超过 SCX_DSP_MAX_LOOPS = 32,内核会通过 scx_kick_cpu() 推迟重试并跳出循环,避免无限占用 rq 锁。

6.5 running / stopping:执行状态通知

void (*running)(struct task_struct *p);
void (*stopping)(struct task_struct *p, bool runnable);
  • running:任务开始在 CPU 上运行时调用(set_next_task_scx
  • stopping:任务停止执行时调用(put_prev_task_scx),runnable 参数表示任务是否仍然可运行

注意:这两个回调可能在不同 CPU 上调用(源码注释,ext_internal.h 第 401 和 419 行):当任务的亲和性变化时,触发这些回调的调度路径可能运行在与任务目标 CPU 不同的 CPU 上。因此,在回调中应使用 scx_bpf_task_cpu(p) 获取任务真正要使用的 CPU,而不是 smp_processor_id()

scx_central 展示了正确的用法(scx_central.bpf.c 第 238-249 行):

void BPF_STRUCT_OPS(central_running, struct task_struct *p)
{
    s32 cpu = scx_bpf_task_cpu(p);  // 注意:使用 task_cpu,不是 smp_processor_id
    u64 *started_at = ARRAY_ELEM_PTR(cpu_started_at, cpu, nr_cpu_ids);
    if (started_at)
        *started_at = scx_bpf_now() ?: 1;  /* 0 indicates idle */
}

void BPF_STRUCT_OPS(central_stopping, struct task_struct *p, bool runnable)
{
    s32 cpu = scx_bpf_task_cpu(p);
    u64 *started_at = ARRAY_ELEM_PTR(cpu_started_at, cpu, nr_cpu_ids);
    // ... 计算运行时间用于周期性抢占检测
}

vtime 更新示例(来自 scx_simple.bpf.c 第 110-125 行):

void BPF_STRUCT_OPS(simple_running, struct task_struct *p)
{
    // 推进全局 vtime(任务开始执行时)
    if (time_before(vtime_now, p->scx.dsq_vtime))
        vtime_now = p->scx.dsq_vtime;
}

void BPF_STRUCT_OPS(simple_stopping, struct task_struct *p, bool runnable)
{
    // 按反比权重累加执行时间到 dsq_vtime
    // slice = 0 时表示时间片耗尽(yield 也会触发)
    p->scx.dsq_vtime += (SCX_SLICE_DFL - p->scx.slice) * 100 / p->scx.weight;
}

6.6 runnable / quiescent:可运行状态通知

void (*runnable)(struct task_struct *p, u64 enq_flags);
void (*quiescent)(struct task_struct *p, u64 deq_flags);

这对回调与 enqueue/dequeue 相关但不严格对应:

  • runnable:任务变为可运行时通知(唤醒、从其他 CPU 迁移、属性变更恢复)。每次 running() 之前必有至少一次 runnable(),但 runnable() 不一定跟随 enqueue()(如远端 CPU 分发时)。
  • quiescent:任务变为不可运行时通知(睡眠、迁移、属性变更暂时移出队列)。

这对回调主要用于跟踪任务的宏观状态(如统计任务在各 cgroup 的运行时间),而 enqueue/dequeue 用于实际的队列管理。

两对回调的对比:

enqueue/dequeue  --> 控制任务在 DSQ 中的存放位置(调度策略层面)
runnable/quiescent --> 通知任务的可运行状态变化(统计与跟踪层面)

任务唤醒流程:
  runnable() --> select_cpu() --> enqueue() --> [等待] --> running()

任务睡眠流程:
  stopping() --> dequeue() --> quiescent()

6.7 tick:周期性时钟

void (*tick)(struct task_struct *p);
  • 调用时机:每 1/HZ 秒(约 4ms on HZ=250 配置),在 task_tick_scx() 中调用(kernel/sched/ext.c 第 2797 行)
  • 用途:BPF 调度器可在此处更新状态、决定是否抢占(将 p->scx.slice 设为 0 可立即触发重调度)
// kernel/sched/ext.c:2800
static void task_tick_scx(struct rq *rq, struct task_struct *curr, int queued)
{
    update_curr_scx(rq);
    if (scx_rq_bypassing(rq)) {
        curr->scx.slice = 0;
        touch_core_sched(rq, curr);
    } else if (SCX_HAS_OP(sch, tick)) {
        SCX_CALL_OP_TASK(sch, SCX_KF_REST, tick, rq, curr);
    }
    if (!curr->scx.slice)
        resched_curr(rq);
}

scx_central 利用此机制实现无 tick 操作:使用 SCX_SLICE_INF 无限时间片,并通过一个单独的 BPF timer 定期检查所有 CPU 并发送 SCX_KICK_PREEMPTscx_central.bpf.c 注释第 15-22 行)。

6.8 yield:主动让出 CPU

bool (*yield)(struct task_struct *from, struct task_struct *to);
  • to == NULLfrom 让出 CPU 给其他可运行任务(sched_yield 系统调用路径)
  • to != NULLfrom 希望让出 CPU 给 to(yield-to 语义),返回 true 表示成功

若 BPF 调度器未实现 yield,内核默认行为是将当前任务的 slice 清零,触发重新调度(kernel/sched/ext.c 中的 yield_task_scx)。

6.9 init / exit:调度器生命周期

s32 (*init)(void);
void (*exit)(struct scx_exit_info *info);
  • init:BPF 调度器加载时调用,可睡眠,适合分配资源。返回非零值终止加载
  • exit:BPF 调度器卸载时调用(包括 init 失败的情况),通过 info 获取退出原因

重要:即使 init() 失败,exit() 也可能被调用(SCX_EFLAG_INITIALIZED 标志未置位时)。这允许 BPF 调度器在 exit() 中进行一致性清理,无需区分 init() 是否成功。

6.10 init_task / exit_task:任务生命周期

s32 (*init_task)(struct task_struct *p, struct scx_init_task_args *args);
void (*exit_task)(struct task_struct *p, struct scx_exit_task_args *args);
  • init_task:任务 fork 或调度器加载时为每个任务初始化,可睡眠。args->fork = true 表示 fork 路径
  • exit_task:任务退出或调度器卸载时清理。args->cancelled = true 表示任务从未进入 SCX 运行(fork 被取消,或初始化过程中失败)
// kernel/sched/ext_internal.h:197
struct scx_init_task_args {
    bool  fork;       // true: fork 路径; false: 调度器加载路径
#ifdef CONFIG_EXT_GROUP_SCHED
    struct cgroup  *cgroup;  // 任务加入的 cgroup
#endif
};

struct scx_exit_task_args {
    bool cancelled;   // true: 任务从未在 SCX 下运行
};

6.11 enable / disable:任务 SCX 开关

void (*enable)(struct task_struct *p);
void (*disable)(struct task_struct *p);

每次任务进入/离开 SCX 管理时调用(与 init_task/exit_task 不同,这对回调可以多次触发)。enabledisable 总是成对出现。

典型用法(来自 scx_simple.bpf.c 第 127 行):

void BPF_STRUCT_OPS(simple_enable, struct task_struct *p)
{
    p->scx.dsq_vtime = vtime_now;  // 初始化 vtime,防止饥饿
}

不初始化 dsq_vtime 的危险:新任务 dsq_vtime = 0,在全局 vtime 推进后,新任务将永远排在所有旧任务之前,破坏公平性并可能导致旧任务饥饿。

6.12 cpu_acquire / cpu_release:CPU 控制权

void (*cpu_acquire)(s32 cpu, struct scx_cpu_acquire_args *args);
void (*cpu_release)(s32 cpu, struct scx_cpu_release_args *args);
  • cpu_release:CPU 被更高优先级调度类抢占时通知(args->reason 指示原因)
// kernel/sched/ext_internal.h:227
enum scx_cpu_preempt_reason {
    SCX_CPU_PREEMPT_RT,      // rt_sched_class 抢占
    SCX_CPU_PREEMPT_DL,      // dl_sched_class 抢占
    SCX_CPU_PREEMPT_STOP,    // stop_sched_class 抢占
    SCX_CPU_PREEMPT_UNKNOWN, // 未知原因
};
  • cpu_acquire:CPU 从更高优先级调度类归还时通知

注意:这两个回调已被标记为废弃(kernel/sched/ext.c 第 4970 行):

if (ops->cpu_acquire || ops->cpu_release)
    pr_warn("ops->cpu_acquire/release() are deprecated, use sched_switch TP instead\n");

推荐改用内核 tracepoint sched:sched_switch 替代,在用户空间监控 CPU 被高优先级类占用的情况。

scx_qmap 展示了对 cpu_release 的使用——当 RT 任务抢占 CPU 时,将当前 CPU 上的任务重新入队(防止任务因等待 RT 任务完成而饥饿)。

6.13 update_idle:空闲状态通知

void (*update_idle)(s32 cpu, bool idle);

CPU 进入或离开空闲状态时调用。实现此回调会默认禁用内置的空闲 CPU 跟踪(内置跟踪驱动 scx_bpf_select_cpu_dfl() 等 helper),除非同时设置 SCX_OPS_KEEP_BUILTIN_IDLE

约束:如果设置了 SCX_OPS_BUILTIN_IDLE_PER_NODE 但没有内置 idle 跟踪(即实现了 update_idle 且没有 SCX_OPS_KEEP_BUILTIN_IDLE),内核会报错(kernel/sched/ext.c 第 4961 行)。

6.14 dump / dump_cpu / dump_task:调试信息转储

void (*dump)(struct scx_dump_ctx *ctx);
void (*dump_cpu)(struct scx_dump_ctx *ctx, s32 cpu, bool idle);
void (*dump_task)(struct scx_dump_ctx *ctx, struct task_struct *p);

当 BPF 调度器因错误退出时,这三个回调提供调试信息:

  • dump:全局状态转储(调度器级别的统计、内部状态)
  • dump_cpu:每个 CPU 的状态。若 idle == true 且此回调无输出,该 CPU 会被跳过
  • dump_task:每个可运行任务的状态

转储上下文(scx_dump_ctx)提供触发转储的时间和原因:

// kernel/sched/ext_internal.h:256
struct scx_dump_ctx {
    enum scx_exit_kind  kind;
    s64                 exit_code;
    const char          *reason;
    u64                 at_ns;       // 转储时的纳秒时间戳
    u64                 at_jiffies;  // 转储时的 jiffies
};

BPF 程序使用 scx_bpf_dump() kfunc 写入转储缓冲区,内容最终放入 scx_exit_info.dump(最大 exit_dump_len 字节,默认 32768)。


7. 任务分发路径

7.1 直接分发(Direct Dispatch)

直接分发是一种优化路径,允许在 select_cpu()enqueue() 回调中通过调用 scx_bpf_dsq_insert() 直接将任务放入 DSQ,跳过正常的 BPF 队列管理流程。

实现机制(kernel/sched/ext.c 第 1255-1284 行):

// kernel/sched/ext.c:1255
static void mark_direct_dispatch(struct scx_sched *sch,
                                 struct task_struct *ddsp_task,
                                 struct task_struct *p, u64 dsq_id,
                                 u64 enq_flags)
{
    // 标记 direct dispatch 已发生(用 ERR_PTR 标记 per-CPU 变量)
    __this_cpu_write(direct_dispatch_task, ERR_PTR(-ESRCH));
    // 记录目标 DSQ 和标志到任务的 ddsp 字段
    p->scx.ddsp_dsq_id = dsq_id;
    p->scx.ddsp_enq_flags = enq_flags;
}

per-CPU 变量 direct_dispatch_taskkernel/sched/ext.c 第 99 行)在 select_cpu/enqueue 调用前设置为当前任务指针,调用后检查是否被替换为 ERR_PTR,若是则触发直接分发路径。

直接分发到 SCX_DSQ_LOCAL 的特殊语义:若在 select_cpu() 中直接分发到 SCX_DSQ_LOCAL,任务进入的是 select_cpu() 返回的 CPU 的本地 DSQ,而非调用者所在 CPU 的本地 DSQ。这是实现高效唤醒分发的关键。

7.2 延迟分发

当在 enqueue 路径(持有当前 CPU 的 rq lock)中尝试将任务分发到远端 CPU 的 local DSQ 时,无法直接加锁远端 rq(会死锁)。此时使用延迟分发(kernel/sched/ext.c 第 1303 行):

// 将任务加入 rq->scx.ddsp_deferred_locals
list_add_tail(&p->scx.dsq_list.node, &rq->scx.ddsp_deferred_locals);
schedule_deferred_locked(rq);

schedule_deferred_locked() 有三种触发路径(kernel/sched/ext.c 第 861 行):

  1. SCX_RQ_IN_WAKEUP 路径:若当前在 task_woken_scx() 中,该函数返回后自动处理
  2. SCX_RQ_IN_BALANCE 路径:在 balance 结束时处理(设置 SCX_RQ_BAL_CB_PENDING
  3. irq_work 路径:通过 irq_work_queue() 在 IRQ 重新使能时处理

process_ddsp_deferred_locals() 函数(kernel/sched/ext.c 第 2279 行)实际处理延迟列表,对每个延迟任务获取目标 CPU 的 rq 锁后真正插入 local DSQ。

7.3 dispatch_enqueue 实现

文件kernel/sched/ext.c,第 1017 行

dispatch_enqueue(sch, dsq, p, enq_flags)
       |
       +-- 非 local DSQ: 加 dsq->lock
       |
       +-- 检查 vtime 或 FIFO 模式不能混用
       |
       +-- vtime (SCX_ENQ_DSQ_PRIQ)?
       |     |-- 插入红黑树 (rb_add, scx_dsq_priq_less)
       |     +-- 更新 list 的 vtime 顺序(dsq_list.priq_pos)
       |
       +-- FIFO?
       |     |-- SCX_ENQ_HEAD/PREEMPT: list_add (头部插入)
       |     +-- 其他: list_add_tail (尾部插入)
       |
       +-- 更新 dsq->seq, p->scx.dsq_seq
       +-- dsq_mod_nr(dsq, 1) 更新计数
       +-- p->scx.dsq = dsq
       +-- 清除 ops_state (SCX_ENQ_CLEAR_OPSS)
       |
       +-- local DSQ: local_dsq_post_enq() 处理抢占逻辑
       +-- 非 local DSQ: 释放 dsq->lock

抢占逻辑local_dsq_post_enqkernel/sched/ext.c 第 993 行):

若入队标志包含 SCX_ENQ_PREEMPT 且当前 CPU 正在运行另一个 SCX 任务,则将当前任务的 slice 清零并调用 resched_curr(),触发立即重调度。这是 sched_ext 中实现任务抢占的标准方式。

7.4 balance_scx 分发循环

文件kernel/sched/ext.c,第 2200 行

balance_scx() 是 SCX 的核心调度函数,对应 CFS 的 pick_next_task()

// 简化的 balance_scx 流程
balance_scx(rq, prev, rf)
    |
    +-- 标记 SCX_RQ_IN_BALANCE
    |
    +-- local_dsq.nr > 0?  --> has_tasks (直接使用本地队列任务)
    |
    +-- consume_global_dsq()?  --> has_tasks
    |
    +-- scx_rq_bypassing?
    |     +-- consume bypass_dsq  --> has_tasks  no_tasks
    |
    +-- !SCX_HAS_OP(dispatch)  !scx_rq_online?  --> no_tasks
    |
    +-- 分发循环 (do ... while (dspc->nr_tasks)):
    |     +-- SCX_CALL_OP(dispatch, cpu, prev)
    |     +-- flush_dispatch_buf(sch, rq)
    |     +-- prev->scx.slice > 0?  --> SCX_RQ_BAL_KEEP
    |     +-- local_dsq.nr > 0?  --> has_tasks
    |     +-- consume_global_dsq?  --> has_tasks
    |     +-- nr_loops == 0?  --> scx_kick_cpu + break
    |
    +-- no_tasks:
    |     prev_on_rq  (!ENQ_LAST  bypassing)?
    |       --> SCX_RQ_BAL_KEEP (续期当前任务)
    |
    +-- 清除 SCX_RQ_IN_BALANCE

SCX_RQ_BAL_KEEP 语义:当 dispatch 找到任务后不立即切换,而是先检查 prev 是否仍有 slice,若有则继续执行 prev(避免无谓的上下文切换)。


8. 与 CFS 的共存机制

8.1 sched_class 优先级链

sched_ext 的 ext_sched_class 在内核调度类链中的精确位置:

stop_sched_class   (migration/stop 线程)
     ||
dl_sched_class     (SCHED_DEADLINE)
     ||
rt_sched_class     (SCHED_FIFO, SCHED_RR)
     ||
fair_sched_class   (SCHED_NORMAL, SCHED_BATCH - CFS)
     ||
ext_sched_class    (SCHED_NORMAL with SCX, or SCHED_EXT)
     ||
idle_sched_class   (swapper/0)

ext_sched_class 定义(kernel/sched/ext.c 第 3441 行):

DEFINE_SCHED_CLASS(ext) = {
    .enqueue_task    = enqueue_task_scx,
    .dequeue_task    = dequeue_task_scx,
    .yield_task      = yield_task_scx,
    .yield_to_task   = yield_to_task_scx,
    .wakeup_preempt  = wakeup_preempt_scx,
    .pick_task       = pick_task_scx,
    .put_prev_task   = put_prev_task_scx,
    .set_next_task   = set_next_task_scx,
    .select_task_rq  = select_task_rq_scx,
    .task_woken      = task_woken_scx,
    .set_cpus_allowed = set_cpus_allowed_scx,
    .rq_online       = rq_online_scx,
    .rq_offline      = rq_offline_scx,
    .task_tick       = task_tick_scx,
    .switching_to    = switching_to_scx,
    .switched_from   = switched_from_scx,
    .switched_to     = switched_to_scx,
    .reweight_task   = reweight_task_scx,
    .prio_changed    = prio_changed_scx,
    .update_curr     = update_curr_scx,
};

源码注释(kernel/sched/ext.c 第 3430 行)解释了几个刻意省略的操作:

  • wakeup_preempt:SCX 中的抢占通过清零 slice + resched_curr() 实现,不使用此 hook
  • migrate_task_rq:任务到 CPU 的映射是瞬态的,不需要
  • task_fork/dead:需要对所有任务生效(包括非 SCX 任务),故从 sched core 直接调用

SCX_OPS_SWITCH_PARTIAL 下的 CFS/SCX 共存

未设置 SCX_OPS_SWITCH_PARTIAL:
    SCHED_NORMAL 任务 --> ext_sched_class (全部由 BPF 调度)
    SCHED_EXT 任务    --> ext_sched_class

设置 SCX_OPS_SWITCH_PARTIAL:
    SCHED_NORMAL 任务 --> fair_sched_class (保持 CFS)
    SCHED_EXT 任务    --> ext_sched_class  (BPF 调度)

8.2 Bypass 模式

Bypass 模式是 sched_ext 的安全兜底机制,当 BPF 调度器被禁用或出现错误时自动激活:

// kernel/sched/ext.c:4121 附近
static void scx_bypass(bool bypass)
{
    // bypass 激活时:
    // - 使用更短的时间片 (SCX_SLICE_BYPASS = 5ms, 可通过模块参数调整)
    // - 所有任务进入 per-CPU 的 bypass_dsq (全局 FIFO)
    // - 忽略 ops.select_cpu(), ops.enqueue(), ops.dispatch()
    // - 启用 bypass 负载均衡定时器(默认每 500ms 运行一次,
    //   由 scx_bypass_lb_intv_us 控制)
}

bypass 模式通过 rq->scx.flags |= SCX_RQ_BYPASSING(per-rq 标志)标记,并在 scx_rq_bypassing() 函数中快速检查:

// kernel/sched/ext_internal.h:1180
static inline bool scx_rq_bypassing(struct rq *rq)
{
    return unlikely(rq->scx.flags & SCX_RQ_BYPASSING);
}

bypass 使用嵌套深度计数(scx_bypass_depthkernel/sched/ext.c 第 36 行)而非简单布尔值,支持多个 bypass 原因同时激活:

bypass 触发条件

  • BPF 调度器正在被加载(scx_enable() 期间)
  • BPF 调度器正在被卸载(scx_disable() 期间)
  • watchdog 检测到任务饥饿(触发 SCX_EXIT_ERROR_STALL
  • BPF 程序主动调用 scx_bpf_error()
  • 管理员通过 sysrq S 触发(SCX_EXIT_SYSRQ
  • 系统进入电源管理状态(suspend/hibernate,见 scx_pm_handler

bypass 负载均衡:bypass 期间,常规的 SCX 负载均衡(dispatch() 回调)被暂停,由内置的 bypass LB 定时器(scx_bypass_lb_intv_us = 500ms)代劳,确保任务不会因 bypass 而永久困在某个 CPU(ext_internal.h 第 27 行)。

8.3 SCX_OPS_SWITCH_PARTIAL:部分切换

通过设置 ops.flags |= SCX_OPS_SWITCH_PARTIAL,只有调度策略为 SCHED_EXT 的任务(通过 sched_setscheduler(2) 显式设置)进入 SCX 管理。这允许在生产系统上渐进地将特定任务迁移到 BPF 调度器,同时保留其他任务的 CFS 行为。

使用此标志时,scx_setscheduler_class() 函数(kernel/sched/ext.c 第 259 行)负责在任务策略变更时选择正确的调度类:

// kernel/sched/ext.c:259
static const struct sched_class *scx_setscheduler_class(struct task_struct *p)
{
    if (p->sched_class == &stop_sched_class)
        return &stop_sched_class;
    return __setscheduler_class(p->policy, p->prio);
}

9. Watchdog 超时机制

9.1 设计原理

BPF 调度器是不可信的——它可能因为 bug 而忘记调度某些任务,导致任务无限期等待。sched_ext 的 watchdog 机制监控每个可运行的 SCX 任务,若等待超时则强制终止 BPF 调度器并回退到 bypass 模式。

关键常量(kernel/sched/ext_internal.h 第 13 行):

SCX_WATCHDOG_MAX_TIMEOUT = 30 * HZ  // 最大超时 30 秒

BPF 调度器可通过 ops.timeout_ms 自定义超时值(不超过 30000ms)。若未指定,默认为最大值。

9.2 双重检测:delayed_work + tick

watchdog 使用两层独立的检测机制:

层 1:delayed_workkernel/sched/ext.c 第 2756 行)

static void scx_watchdog_workfn(struct work_struct *work)
{
    WRITE_ONCE(scx_watchdog_timestamp, jiffies);  // 更新存活时间戳

    for_each_online_cpu(cpu) {
        if (unlikely(check_rq_for_timeouts(cpu_rq(cpu))))
            break;  // 发现超时任务,触发 scx_exit()
        cond_resched();
    }
    // 每隔 timeout/2 运行一次
    queue_delayed_work(system_unbound_wq, to_delayed_work(work),
                       READ_ONCE(scx_watchdog_timeout) / 2);
}

check_rq_for_timeouts() 遍历 rq 的 runnable_list,检查每个可运行任务的 runnable_atkernel/sched/ext.c 第 2740 行):

if (unlikely(time_after(jiffies,
                         last_runnable + READ_ONCE(scx_watchdog_timeout)))) {
    u32 dur_ms = jiffies_to_msecs(jiffies - last_runnable);
    scx_exit(sch, SCX_EXIT_ERROR_STALL, 0,
             "%s[%d] failed to run for %u.%03us",
             p->comm, p->pid, dur_ms / 1000, dur_ms % 1000);
}

层 2:tick 检测kernel/sched/ext.c 第 2772 行)

每个 CPU 的 tick handler (scx_tick()) 会检查 scx_watchdog_timestamp,如果 delayed_work 本身也卡住了(ksoftirqd 无法运行),则在 tick 中触发错误:

void scx_tick(struct rq *rq)
{
    last_check = READ_ONCE(scx_watchdog_timestamp);
    if (unlikely(time_after(jiffies,
                            last_check + READ_ONCE(scx_watchdog_timeout)))) {
        scx_exit(sch, SCX_EXIT_ERROR_STALL, 0,
                 "watchdog failed to check in for %u.%03us", ...);
    }
}

watchdog 时间线

    t=0: 任务入队(runnable_at = jiffies)
         delayed_work 每 timeout/2 运行,更新 scx_watchdog_timestamp
    t=timeout/2: 第一次 delayed_work 检测,任务尚未超时
    t=timeout: 第二次 delayed_work 检测,发现超时 --> SCX_EXIT_ERROR_STALL
               tick 同时检测 scx_watchdog_timestamp 是否也超时

9.3 scx_exit_info 退出信息收集

文件kernel/sched/ext_internal.h,第 84 行

// kernel/sched/ext_internal.h:84
struct scx_exit_info {
    enum scx_exit_kind  kind;       // 退出类别(见下表)
    s64                 exit_code;  // 用户自定义退出码
    u64                 flags;      // SCX_EFLAG_INITIALIZED 等
    const char          *reason;    // 退出原因的文本描述
    unsigned long       *bt;        // 错误退出时的内核栈回溯(最多 64 帧)
    u32                 bt_len;
    char                *msg;       // 详细错误消息(最多 1024 字节)
    char                *dump;      // BPF 调度器的 dump 输出(默认 32768 字节)
};

退出类别(scx_exit_kind):

常量 含义
0 SCX_EXIT_NONE 未退出
1 SCX_EXIT_DONE 正常完成(内部使用)
64 SCX_EXIT_UNREG 用户空间主动卸载
65 SCX_EXIT_UNREG_BPF BPF 程序调用 scx_bpf_exit() 主动退出
66 SCX_EXIT_UNREG_KERN 内核触发的卸载(如 CPU hotplug)
67 SCX_EXIT_SYSRQ sysrq S 触发
1024 SCX_EXIT_ERROR 运行时错误
1025 SCX_EXIT_ERROR_BPF scx_bpf_error() 触发
1026 SCX_EXIT_ERROR_STALL watchdog 检测到任务饥饿

退出码格式(scx_exit_code):

Bits: [63  ..  48 47   ..  32 31 .. 0]
      [ SYS ACT ] [ SYS RSN ] [ USR  ]

SCX_ECODE_RSN_HOTPLUG  = 1LLU << 32  // CPU hotplug 触发
SCX_ECODE_ACT_RESTART  = 1LLU << 48  // 建议重启调度器

BPF 调度器在 exit 回调中接收 scx_exit_info,可通过 UEI_RECORD() 宏将信息传递给用户空间(见 scx_simple.bpf.c 第 145 行)。


10. kfunc 权限控制

sched_ext 实现了一套细粒度的 kfunc(内核函数)调用权限控制系统,确保 BPF 程序只能在合法的上下文中调用特定的内核函数。

权限掩码scx_kf_maskinclude/linux/sched/ext.h 第 120 行):

enum scx_kf_mask {
    SCX_KF_UNLOCKED   = 0,        // 可睡眠上下文,无 rq 锁
    SCX_KF_CPU_RELEASE = 1 << 0,  // ops.cpu_release() 上下文
    SCX_KF_DISPATCH   = 1 << 1,   // ops.dispatch() 上下文
    SCX_KF_ENQUEUE    = 1 << 2,   // ops.enqueue() 和 ops.select_cpu() 上下文
    SCX_KF_SELECT_CPU = 1 << 3,   // ops.select_cpu() 上下文
    SCX_KF_REST       = 1 << 4,   // 其他持 rq 锁的操作
};

嵌套规则:kfunc 只允许在更高掩码值的回调中嵌套调用。scx_kf_allow() 使用 higher_bits() 检测非法嵌套(kernel/sched/ext.c 第 275-283 行):

// kernel/sched/ext.c:275
static __always_inline void scx_kf_allow(u32 mask)
{
    /* nesting is allowed only in increasing scx_kf_mask order */
    WARN_ONCE((mask | higher_bits(mask)) & current->scx.kf_mask,
              "invalid nesting current->scx.kf_mask=0x%x mask=0x%x\n",
              current->scx.kf_mask, mask);
    current->scx.kf_mask |= mask;
    barrier();
}

locked rq 追踪:为了让 kfunc 知道当前持有哪个 rq 的锁(避免重复加锁导致死锁),内核维护了一个 per-CPU 变量 scx_locked_rq_statekernel/sched/ext.c 第 297 行):

DEFINE_PER_CPU(struct rq *, scx_locked_rq_state);

SCX_CALL_OP 宏调用之前,update_locked_rq(rq) 将当前 rq 记录在此变量中,kfunc 可通过 scx_kf_allowed_on_arg_tasks() 等检查安全地操作相关的 rq。

调用宏kernel/sched/ext.c 第 311-383 行):

  • SCX_CALL_OP(sch, mask, op, rq, args...) — 调用不返回值的 ops 回调
  • SCX_CALL_OP_RET(sch, mask, op, rq, args...) — 调用返回值的 ops 回调
  • SCX_CALL_OP_TASK(sch, mask, op, rq, task, args...) — 针对特定任务的回调(设置 kf_tasks
  • SCX_CALL_OP_TASK_RETSCX_CALL_OP_2TASKS_RET — 变体

kfunc 按上下文的可用性

kfunc UNLOCKED REST ENQUEUE SELECT_CPU DISPATCH
scx_bpf_create_dsq Y N N N N
scx_bpf_destroy_dsq Y N N N N
scx_bpf_dsq_insert N Y Y Y Y
scx_bpf_dsq_move_to_local N N N N Y
scx_bpf_select_cpu_dfl N N N Y N
scx_bpf_kick_cpu N Y Y Y Y
scx_bpf_error Y Y Y Y Y

11. BPF struct_ops 注册机制

11.1 注册与注销流程

sched_ext 使用 bpf_struct_ops 机制将 BPF 程序注册为调度器回调。

注册流程kernel/sched/ext.c 第 5406-5418 行):

// BPF 程序通过 bpf_struct_ops 加载时调用
static int bpf_scx_reg(void *kdata, struct bpf_link *link)
{
    return scx_enable(kdata, link);  // 启动 BPF 调度器
}

// 卸载时调用
static void bpf_scx_unreg(void *kdata, struct bpf_link *link)
{
    scx_disable(SCX_EXIT_UNREG);
    // ...
}

BPF verifier 操作bpf_scx_verifier_opskernel/sched/ext.c 第 5328 行):

static const struct bpf_verifier_ops bpf_scx_verifier_ops = {
    .get_func_proto    = bpf_base_func_proto,  // 基础 BPF helper
    .is_valid_access   = bpf_scx_is_valid_access,  // 字段访问验证
    .btf_struct_access = bpf_scx_btf_struct_access, // 结构体字段写权限
};

11.2 bpf_scx_init_member:字段验证

文件kernel/sched/ext.c,第 5334 行

在 BPF 程序加载时,内核从用户空间复制并验证 sched_ext_ops 的各个字段:

  • dispatch_max_batch:不超过 INT_MAX
  • flags:只允许 SCX_OPS_ALL_FLAGS 定义的标志(高 8 位为内部标志,不可设置)
  • name:合法的 BPF 对象名(非空,通过 bpf_obj_name_cpy 验证)
  • timeout_msmsecs_to_jiffies 后不超过 SCX_WATCHDOG_MAX_TIMEOUT
  • exit_dump_len:0 时使用默认值 SCX_EXIT_DUMP_DFL_LEN = 32768
  • hotplug_seq:允许调度器在已发生热插拔事件时检测并做出反应

11.3 BPF 程序可写字段

通过 bpf_scx_btf_struct_access() 控制(kernel/sched/ext.c 第 5306 行),BPF 程序只能写入 task_struct 的以下三个字段:

  • scx.slice:调整任务时间片
  • scx.dsq_vtime:设置 vtime 优先级键
  • scx.disallow:在 init_task() 中拒绝任务使用 SCX

其他所有字段对 BPF 程序只读(返回 -EACCES)。

11.4 scx_enable 执行流程

文件kernel/sched/ext.c,第 4976 行

由于 scx_enable() 必须以 RT 优先级执行(避免在 fair-class 饱和时被饿死),它通过专用 kthread_worker 执行(scx_enable_workfn):

// kernel/sched/ext.c:4976
/*
 * scx_enable() is offloaded to a dedicated system-wide RT kthread to avoid
 * starvation. During the READY -> ENABLED task switching loop, the calling
 * thread's sched_class gets switched from fair to ext. As fair has higher
 * priority than ext, the calling thread can be indefinitely starved under
 * fair-class saturation, leading to a system hang.
 */

scx_enable_workfn 的执行步骤(kernel/sched/ext.c 第 4989 行):

  1. mutex_lock(&scx_enable_mutex) — 保证单实例
  2. alloc_kick_syncs() — 分配 per-CPU kick 同步数组(O(nr_cpu_ids^2))
  3. scx_alloc_and_add_sched(ops) — 分配 scx_sched、全局 DSQ、per-CPU 统计
  4. 状态设为 SCX_ENABLING
  5. cpus_read_lock() — 固定 CPU 拓扑
  6. rcu_assign_pointer(scx_root, sch) — 使调度器实例可见
  7. 激活 bypass 模式(scx_bypass(true)
  8. 遍历所有任务,调用 scx_init_task()TASK_NONE -> TASK_READY
  9. 调用 ops.init()
  10. scx_cgroup_init() — 初始化 cgroup 状态
  11. 遍历所有任务,调用 scx_enable_task()TASK_READY -> TASK_ENABLED
  12. 状态设为 SCX_ENABLEDscx_switching_all 更新
  13. 停止 bypass(scx_bypass(false)
  14. 启动 watchdog

不支持热更新bpf_scx_updatekernel/sched/ext.c 第 5428 行):sched_ext 不支持在不卸载当前调度器的情况下更新 BPF 程序(返回 -EOPNOTSUPP),因为 init() 可能失败,无法保证热更新的原子性。


12. 典型实现分析

12.1 scx_simple:全局加权 vtime 调度器

文件tools/sched_ext/scx_simple.bpf.c

这是 sched_ext 最基础的参考实现,展示了如何构建一个工作的调度器:

架构

所有 CPU                      SHARED_DSQ (vtime 优先级队列)
    |                               |
    | select_cpu() 选到空闲 CPU     | enqueue() 将任务按 vtime 插入
    |   --> direct dispatch 到      |
    |       SCX_DSQ_LOCAL           |
    |                               |
    | dispatch() 从 SHARED_DSQ      |
    |   --> 移动到 local DSQ        |

关键设计

  1. select_cpu:使用内置 scx_bpf_select_cpu_dfl() 选择 CPU。若找到空闲 CPU,直接 dispatch 到 SCX_DSQ_LOCAL,跳过 enqueue

  2. enqueue

    • FIFO 模式:scx_bpf_dsq_insert(p, SHARED_DSQ, SCX_SLICE_DFL, enq_flags)
    • vtime 模式:基于 p->scx.dsq_vtime,限制最多积累一个 slice 的空闲信用:
      if (time_before(vtime, vtime_now - SCX_SLICE_DFL))
          vtime = vtime_now - SCX_SLICE_DFL;
      scx_bpf_dsq_insert_vtime(p, SHARED_DSQ, SCX_SLICE_DFL, vtime, enq_flags);
  3. dispatch:简单地 scx_bpf_dsq_move_to_local(SHARED_DSQ) 移动一个任务到当前 CPU

  4. running/stopping:维护全局 vtime_nowp->scx.dsq_vtime

  5. enable:设置 p->scx.dsq_vtime = vtime_now 防止新任务饥饿

适用场景:CPU 拓扑均匀(统一 L3 缓存),无抢占需求的简单工作负载

12.2 scx_flatcg:扁平化 cgroup 层级调度器

文件tools/sched_ext/scx_flatcg.bpf.c

scx_flatcg 实现了基于 cgroup 权重的 CPU 控制,但通过"扁平化"cgroup 层级来避免逐层调度带来的开销。

核心思想

给定 cgroup 层级(括号内为权重):

R + A (100) + B (100)
  |           + C (100)
  + D (200)

传统分层调度需要两级决策:先在 A 与 D 之间选择,再在 B 与 C 之间选择。

扁平化方案:直接计算每个叶子 cgroup 在整个系统中的份额:

  • B: 100/(100+100) × 100/(100+200) = 1/6
  • C: 1/6(同理)
  • D: 200/(100+200) = 2/3

所有叶子 cgroup 以这些折算后的权重在同一层竞争,消除了多层遍历。

数据结构

// tools/sched_ext/scx_flatcg.bpf.c:82
struct fcg_cpu_ctx {
    u64 cur_cgid;   // 当前 CPU 正在服务的 cgroup ID
    u64 cur_at;     // 当前 cgroup 开始服务的时间
};

// per-cgroup 状态(通过 BPF map 存储)
struct fcg_cgrp_ctx {
    u32 weight;          // 折算后的权重
    u64 cvtime;          // cgroup 虚拟时间(用于调度顺序)
    // ...
};

调度流程

  1. dispatch() 先选择下一个应该服务的 cgroup(基于 cvtime 虚拟时间)
  2. 再从该 cgroup 的 DSQ 中取出一个任务

性能表现(源码注释,scx_flatcg.bpf.c 第 34 行):

  • 相比 CFS(禁用 CPU controller):性能持平或略优(~3%)
  • 相比 CFS(启用 CPU controller,4 层嵌套,2:1 权重比):高出 ~10%

局限性:对 cgroup 同时激活的"惊群"场景处理不佳,低优先级父 cgroup 下同时唤醒大量任务时,可能短暂超出其 CPU 配额。

12.3 scx_central:中央 CPU 调度器

文件tools/sched_ext/scx_central.bpf.c

scx_central 展示了三个重要的 sched_ext 特性:

a. 集中式调度决策

所有调度决策由唯一的 central CPU 做出。其他 CPU 在本地 DSQ 为空时设置 cpu_gimme_task[cpu] = true,并通过 SCX_KICK_PREEMPT 通知 central CPU。

所有 CPU(非 central):
  dispatch() --> 设置 gimme=true --> 踢 central_cpu
       ^                                 |
       |         (IPI)                   v
       `<-- central_cpu 分发任务 --> SCX_DSQ_LOCAL_ON | cpu

central CPU 的 dispatch() 负责扫描所有 CPU 的请求并分发(scx_central.bpf.c 第 182 行):

void BPF_STRUCT_OPS(central_dispatch, s32 cpu, struct task_struct *prev)
{
    if (cpu == central_cpu) {
        // 为所有需要任务的 CPU 分发
        bpf_for(cpu, 0, nr_cpu_ids) {
            bool *gimme;
            if (!scx_bpf_dispatch_nr_slots()) break;
            gimme = ARRAY_ELEM_PTR(cpu_gimme_task, cpu, nr_cpu_ids);
            if (!gimme || !*gimme) continue;
            if (dispatch_to_cpu(cpu)) *gimme = false;
        }
        // 为 central CPU 自身分发
        dispatch_to_cpu(central_cpu);
    } else {
        // 非 central CPU:等待 central CPU 分发
        ARRAY_ELEM_PTR(cpu_gimme_task, cpu, nr_cpu_ids)[0] = true;
        scx_bpf_kick_cpu(central_cpu, SCX_KICK_PREEMPT);
    }
}

b. Tickless 操作

使用 SCX_SLICE_INF 无限时间片使任务在 CONFIG_NO_HZ_FULL 内核上不产生 tick。通过 BPF timer 定期触发 SCX_KICK_PREEMPT 实现强制调度(scx_central.bpf.c 第 15-22 行)。

c. 内核线程抢占

每个 CPU 绑定的内核线程(PF_KTHREAD && nr_cpus_allowed == 1)直接插入本地 DSQ 并使用 SCX_ENQ_PREEMPTscx_central.bpf.c 第 114 行):

if ((p->flags & PF_KTHREAD) && p->nr_cpus_allowed == 1) {
    scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL, SCX_SLICE_INF,
                       enq_flags | SCX_ENQ_PREEMPT);
    return;
}

这保证 ksoftirqd 等关键内核线程不会被用户线程阻塞(包括 BPF timer 执行依赖的 softirq)。

pid 查找机制central_q 是一个 BPF_MAP_TYPE_QUEUE,存储的是任务 PID 而非指针。在 dispatch_to_cpu() 中通过 bpf_task_from_pid() 重新查找任务(scx_central.bpf.c 第 144 行)。这意味着任务在 enqueue()dispatch() 之间退出时,bpf_task_from_pid() 会返回 NULL,计入 nr_lost_pids,并安全地跳过。

12.4 scx_qmap:五级 FIFO 优先级队列

文件tools/sched_ext/scx_qmap.bpf.c

scx_qmap 实现了五级优先级的 FIFO 调度:

// tools/sched_ext/scx_qmap.bpf.c:27
enum consts {
    SHARED_DSQ   = 0,
    HIGHPRI_DSQ  = 1,
    HIGHPRI_WEIGHT = 8668,  // -20 nice 对应的权重
};

五个队列(queue0 ~ queue4)使用 BPF_MAP_TYPE_QUEUE 存储任务 PID,根据任务的 compound weight 决定放入哪个队列。每个 CPU 的 dispatch() 按队列索引循环,高索引队列每轮分发更多任务:

queue0: 分发 1 个/轮
queue1: 分发 2 个/轮
queue2: 分发 4 个/轮
queue3: 分发 8 个/轮
queue4: 分发 16 个/轮

特殊功能演示

  • highpri_boosting:高优先级任务使用独立的 HIGHPRI_DSQ,并通过 scx_bpf_dsq_move_to_local() 优先消费
  • disallow_tgid:展示在 init_task() 中使用 p->scx.disallow 拒绝特定 tgid 的任务使用 SCX
  • stall_user_nth / stall_kernel_nth:每第 N 个用户/内核任务故意不入队,触发 watchdog 超时,用于测试 watchdog 机制

12.5 scx_pair:SMT 感知 cgroup 对调度器

文件tools/sched_ext/scx_pair.bpf.c

scx_pair 演示了 core-sched 语义——保证兄弟 SMT 线程(同物理核的两个逻辑 CPU)总是执行同一 cgroup 的任务,防止跨 cgroup 的侧信道攻击。

工作原理

每对 CPU(pair_cpu)共享一个 pair_ctx 结构,包含:

  • active_mask:两个 CPU 的活跃状态位图
  • cgid:当前服务的 cgroup ID
  • draining:排空标志

调度逻辑scx_pair.bpf.c 第 63-80 行):

  1. 检查 pair_ctx.draining 状态
  2. 若当前 cgroup 时间片耗尽或队列空,切换到新 cgroup(需等待另一个 CPU 完成当前 cgroup 的任务)
  3. 使用 bpf_kptr_xchg() 等原子操作协调两个 CPU 的决策

这种设计保证了在任意时刻,一个物理核的两个逻辑 CPU 只运行同一 cgroup 的任务(或空闲)。


13. 内置空闲 CPU 选择机制

13.1 scx_idle 模块架构

文件kernel/sched/ext_idle.c

内置空闲 CPU 选择是 sched_ext 的核心辅助功能,实现了拓扑感知的 CPU 空闲追踪:

// kernel/sched/ext_idle.c:15
static DEFINE_STATIC_KEY_FALSE(scx_builtin_idle_enabled);
static DEFINE_STATIC_KEY_FALSE(scx_builtin_idle_per_node);
static DEFINE_STATIC_KEY_FALSE(scx_selcpu_topo_llc);   // LLC 感知优化
static DEFINE_STATIC_KEY_FALSE(scx_selcpu_topo_numa);  // NUMA 感知优化

空闲 CPU 追踪使用两级位图结构:

// kernel/sched/ext_idle.c:32
struct scx_idle_cpus {
    cpumask_var_t cpu;   // 空闲 CPU 位图
    cpumask_var_t smt;   // 整个 SMT cluster 均空闲的 CPU 位图
};

全局空闲位图(未启用 per-node 时):scx_idle_global_masks per-node 空闲位图(启用 SCX_OPS_BUILTIN_IDLE_PER_NODE 时):scx_idle_node_masks[node]

13.2 SMT 感知空闲 CPU 追踪

文件kernel/sched/ext_idle.c,第 77 行

scx_idle_test_and_clear_cpu() 在 CPU 被选中时同时更新 cpusmt 位图:

static bool scx_idle_test_and_clear_cpu(int cpu)
{
    int node = scx_cpu_node_if_enabled(cpu);
    struct cpumask *idle_cpus = idle_cpumask(node)->cpu;

#ifdef CONFIG_SCHED_SMT
    if (sched_smt_active()) {
        const struct cpumask *smt = cpu_smt_mask(cpu);
        struct cpumask *idle_smts = idle_cpumask(node)->smt;
        // 清除整个 SMT cluster 的 SMT 空闲标志
        // 无论是否成功获取 CPU,SMT 掩码都要清除
        // (防止 scx_pick_idle_cpu 陷入无限循环)
    }
#endif
    return cpumask_test_and_clear_cpu(cpu, idle_cpus);
}

SMT 位图的作用:scx_bpf_select_cpu_dfl() 优先选择 smt 位图中的 CPU(整个物理核空闲),而非仅 cpu 位图中的 CPU(只有一个逻辑线程空闲)。这减少了跨 SMT 核的调度(避免一个物理核的两个线程分别运行不同任务,降低缓存污染)。

13.3 NUMA 和 LLC 拓扑感知

文件kernel/sched/ext_idle.c,第 231-280 行

内置 CPU 选择支持三种拓扑感知策略(通过 static_key 控制,仅在检测到多 LLC 域或多 NUMA 节点时激活):

// kernel/sched/ext_idle.c:231
static unsigned int llc_weight(s32 cpu)
{
    struct sched_domain *sd = rcu_dereference(per_cpu(sd_llc, cpu));
    if (!sd) return 0;
    return sd->span_weight;
}

static struct cpumask *llc_span(s32 cpu) { ... }
static unsigned int numa_weight(s32 cpu) { ... }
static struct cpumask *numa_span(s32 cpu) { ... }

CPU 选择优先级(从高到低):

1. 同 LLC 域内的空闲 SMT CPU(整个物理核空闲)
2. 同 NUMA 节点内的空闲 SMT CPU
3. 全局空闲 SMT CPU
4. 同 LLC 域内的空闲 CPU
5. 同 NUMA 节点内的空闲 CPU
6. 全局空闲 CPU
7. prev_cpu 或任意可用 CPU(兜底)

13.4 scx_bpf_select_cpu_dfl:默认 CPU 选择

内置 CPU 选择 kfunc 的标准实现,在 select_cpu() 回调中调用:

// 典型调用模式
s32 BPF_STRUCT_OPS(my_select_cpu, struct task_struct *p,
                   s32 prev_cpu, u64 wake_flags)
{
    bool is_idle = false;
    s32 cpu;

    cpu = scx_bpf_select_cpu_dfl(p, prev_cpu, wake_flags, &is_idle);
    if (is_idle) {
        // 找到空闲 CPU,直接 direct dispatch
        scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL, SCX_SLICE_DFL, 0);
    }
    return cpu;
}

is_idle = true 意味着找到了一个空闲 CPU,任务可以直接在下一次 balance 时立即执行,无需等待其他任务完成。


14. cgroup 与 cpuset 集成

14.1 CONFIG_EXT_GROUP_SCHED:cgroup 权重支持

当内核编译了 CONFIG_EXT_GROUP_SCHED 时,sched_ext 支持 cgroup CPU 权重感知。BPF 调度器通过 scx_ops 中的 cgroup 回调集与 cgroup 子系统交互。

cgroup 上下文传递:当 init_task() 在 fork 路径中调用时,scx_init_task_args.cgroup 字段指向任务所属的 cgroup(kernel/sched/ext.c 第 2832 行):

// kernel/sched/ext.c:2832
#define SCX_INIT_TASK_ARGS_CGROUP(tg)  .cgroup = tg_cgrp(tg),

辅助函数 tg_cgrp() 处理 CGROUP_SCHED 禁用和 autogroup 等边界情况(kernel/sched/ext.c 第 2819 行):

static struct cgroup *tg_cgrp(struct task_group *tg)
{
    // autogroup 的 tg->css.cgroup 为 NULL,视为 root cgroup
    if (tg && tg->css.cgroup)
        return tg->css.cgroup;
    else
        return &cgrp_dfl_root.cgrp;
}

14.2 cgroup 生命周期回调

文件kernel/sched/ext_internal.h,第 618-709 行

// cgroup 创建时
s32 (*cgroup_init)(struct cgroup *cgrp, struct scx_cgroup_init_args *args);

// cgroup 销毁时
void (*cgroup_exit)(struct cgroup *cgrp);

scx_cgroup_init_args 包含 cgroup 的初始权重:

// kernel/sched/ext_internal.h:217
struct scx_cgroup_init_args {
    u32 weight;  // cgroup 权重 [1..10000]
};

cgroup 初始化由 scx_cgroup_init() 协调(kernel/sched/ext.c 第 3535 行),在 scx_enable() 期间遍历所有现有 cgroup,为每个 cgroup 调用 ops.cgroup_init()

14.3 任务 cgroup 迁移

当任务从一个 cgroup 迁移到另一个时,内核通过三步回调保证原子性:

cgroup_prep_move(p, from, to)  --> 准备阶段(可睡眠,可分配资源)
cgroup_move(p, from, to)       --> 提交迁移(任务被出队,不可睡眠)
cgroup_cancel_move(p, from, to) --> 取消迁移(prep 后 move 失败时调用)

这个三段式设计允许 BPF 调度器在 prep_move 中分配 per-cgroup 资源(如目标 cgroup 的 DSQ),在 move 中原子切换,在失败时回滚(ext_internal.h 第 643-679 行)。

14.4 带宽控制参数

文件kernel/sched/ext_internal.h,第 218-225 行

若 BPF 调度器希望实现 CPU 带宽控制(类似 cgroup cpu.max),可通过 cgroup_set_bandwidth() 回调接收 cgroup 的带宽参数:

struct scx_cgroup_init_args {
    u32  weight;        // cgroup 权重 [1..10000]
    u64  bw_period_us;  // 带宽控制周期(微秒)
    u64  bw_quota_us;   // 带宽配额(微秒)
    u64  bw_burst_us;   // 突发配额(微秒,来自 cpu.max.burst)
};

scx_flatcg 中通过在 cgroup_set_weight 回调中更新折算权重来实现层级化带宽控制。


15. CPU 热插拔处理

15.1 热插拔序列号机制

文件kernel/sched/ext.c,第 52 行

static atomic_long_t scx_hotplug_seq = ATOMIC_LONG_INIT(0);

每次 CPU 上线或下线时,scx_hotplug_seq 递增。BPF 调度器可在 ops.hotplug_seq 字段记录加载时的热插拔序列号,若调度器发现序列号不匹配(说明有 CPU 热插拔发生),可以主动退出并请求重启(通过 SCX_ECODE_RSN_HOTPLUG | SCX_ECODE_ACT_RESTART)。

用户空间可通过 /sys/kernel/sched_ext/hotplug_seq 监控此值(见第 17 节)。

15.2 cpu_online / cpu_offline 回调

void (*cpu_online)(s32 cpu);
void (*cpu_offline)(s32 cpu);

这两个回调在 CPU 热插拔时通知 BPF 调度器:

  • cpu_online:CPU 上线后调用(可睡眠),BPF 调度器可开始向此 CPU 分发任务
  • cpu_offline:CPU 下线前调用(可睡眠),BPF 调度器应停止向此 CPU 分发任务

这两个回调与 init/exit 一样是可睡眠回调,允许分配/释放 per-CPU 资源。

15.3 handle_hotplug 内核内部处理

当 CPU 热插拔发生时(kernel/sched/ext.c 中的 scx_rq_online/scx_rq_offline),内核会:

  1. 设置/清除 SCX_RQ_ONLINE 标志
  2. 调用 ops.cpu_online()/ops.cpu_offline()
  3. 递增 scx_hotplug_seq
  4. ops.hotplug_seq 不为 0 且与当前序列号不匹配,触发 SCX_EXIT_UNREG_KERN 并带 SCX_ECODE_RSN_HOTPLUG | SCX_ECODE_ACT_RESTART

在 CPU 下线时,其本地 DSQ 和 bypass_dsq 中的任务会通过 reenq_local() 被重新入队到其他 CPU(kernel/sched/ext.c 第 198 行)。


16. 全局变量与模块参数

16.1 核心全局变量

文件kernel/sched/ext.c,第 20-76 行

static struct scx_sched __rcu *scx_root;     // 当前调度器实例(RCU 保护)

static DEFINE_RAW_SPINLOCK(scx_tasks_lock);  // 保护 scx_tasks 链表
static LIST_HEAD(scx_tasks);                 // 所有任务链表(fork 到 free)

static DEFINE_MUTEX(scx_enable_mutex);       // 序列化 enable/disable 操作
DEFINE_STATIC_KEY_FALSE(__scx_enabled);      // SCX 是否激活(fast path)
DEFINE_STATIC_PERCPU_RWSEM(scx_fork_rwsem); // 与 fork 路径同步

static atomic_t scx_enable_state_var;        // SCX_DISABLED/ENABLING/ENABLED/DISABLING
static int scx_bypass_depth;                 // bypass 嵌套深度
static bool scx_aborting;                    // enable 失败时的回滚标志
static bool scx_init_task_enabled;           // init_task 路径是否激活
static bool scx_switching_all;               // 是否切换所有任务(!SWITCH_PARTIAL)

static atomic_long_t scx_nr_rejected;        // 被 disallow 拒绝的任务数
static atomic_long_t scx_hotplug_seq;        // CPU 热插拔序列号
static atomic_long_t scx_enable_seq;         // 调度器加载次数(单调递增)

scx_enable_seq 可用于检测系统是否曾经使用过自定义 SCX 调度器(即使当前已卸载),适合安全审计场景。

16.2 可调模块参数

文件kernel/sched/ext.c,第 159-191 行

通过 /sys/module/sched_ext/parameters/ 暴露:

参数 默认值 范围 含义
slice_bypass_us 5000 (5ms) 100us ~ 100ms bypass 模式下的任务时间片
bypass_lb_intv_us 500000 (500ms) 0 ~ 10s bypass 负载均衡间隔(0 禁用)

这些参数主要用于调试,正常调度不受影响:

// kernel/sched/ext.c:159
static u64 scx_slice_dfl = SCX_SLICE_DFL;
static unsigned int scx_slice_bypass_us = SCX_SLICE_BYPASS / NSEC_PER_USEC;
static unsigned int scx_bypass_lb_intv_us = SCX_BYPASS_LB_DFL_INTV_US;

参数有范围限制(通过 param_set_uint_minmax),防止过大或过小的值破坏系统:

// kernel/sched/ext.c:163
static int set_slice_us(const char *val, const struct kernel_param *kp)
{
    return param_set_uint_minmax(val, kp, 100, 100 * USEC_PER_MSEC);
}

16.3 电源管理集成

文件kernel/sched/ext.c,第 5742 行

sched_ext 通过 PM notifier 与 Linux 电源管理集成:

static int scx_pm_handler(struct notifier_block *nb,
                           unsigned long event, void *ptr)
{
    switch (event) {
    case PM_HIBERNATION_PREPARE:
    case PM_SUSPEND_PREPARE:
    case PM_RESTORE_PREPARE:
        scx_bypass(true);   // 进入 bypass 模式
        break;
    case PM_POST_HIBERNATION:
    case PM_POST_SUSPEND:
    case PM_POST_RESTORE:
        scx_bypass(false);  // 退出 bypass 模式
        break;
    }
    return NOTIFY_OK;
}

原因:SCX 调度器通常有用户空间组件(如守护进程)参与调度决策,而 PM 操作会冻结用户空间。若 BPF 调度器依赖用户空间组件来避免饥饿,则在 suspend 期间可能导致调度停滞,因此自动切换到 bypass 模式(源码注释 ext.c 第 5745-5749 行)。


17. sysfs 接口

17.1 /sys/kernel/sched_ext 属性

文件kernel/sched/ext.c,第 3613-3665 行

sched_ext 通过 kset 在 /sys/kernel/sched_ext 暴露全局只读属性:

属性文件 读取内容 说明
state disabled/enabling/enabled/disabling 当前调度器状态
switch_all 01 是否切换所有任务(!SCX_OPS_SWITCH_PARTIAL
nr_rejected 整数 disallow 拒绝进入 SCX 的任务数
hotplug_seq 整数 CPU 热插拔序列号
enable_seq 整数 调度器加载次数(单调递增)
// kernel/sched/ext.c:3619
static ssize_t scx_attr_state_show(struct kobject *kobj,
                                    struct kobj_attribute *ka, char *buf)
{
    return sysfs_emit(buf, "%s\n", scx_enable_state_str[scx_enable_state()]);
}

17.2 调度器实例 kobject

当 BPF 调度器启用时,scx_sched.kobj 会在 /sys/kernel/sched_ext/<scheduler_name> 下挂载调度器特定的属性(如 ops 名称、统计数据等)。这个接口在调度器卸载时自动移除。


18. 性能事件统计

文件kernel/sched/ext_internal.h,第 824 行

scx_event_stats 结构记录各类调度事件的计数:

struct scx_event_stats {
    s64 SCX_EV_SELECT_CPU_FALLBACK;         // select_cpu 返回了无效 CPU,使用 fallback
    s64 SCX_EV_DISPATCH_LOCAL_DSQ_OFFLINE;  // 分发到 local DSQ 时目标 CPU 已下线
    s64 SCX_EV_DISPATCH_KEEP_LAST;          // 无其他任务可运行,当前任务续期执行
    s64 SCX_EV_ENQ_SKIP_EXITING;            // 跳过退出任务的 enqueue(直接到 local DSQ)
    s64 SCX_EV_ENQ_SKIP_MIGRATION_DISABLED; // 跳过迁移禁用任务的 enqueue
    s64 SCX_EV_REFILL_SLICE_DFL;            // 时间片被默认值填充的次数
    s64 SCX_EV_BYPASS_DURATION;             // bypass 模式累计时长(纳秒)
    s64 SCX_EV_BYPASS_DISPATCH;             // bypass 模式下的分发次数
    s64 SCX_EV_BYPASS_ACTIVATE;             // bypass 模式激活次数
};

事件计数通过 per-CPU 变量减少开销(scx_sched_pcpu.event_stats),全局汇总通过 scx_bpf_events() kfunc 实现。

更新使用以下宏:

  • scx_add_event(sch, name, cnt):通用(允许抢占)
  • __scx_add_event(sch, name, cnt):快速路径(禁用抢占时使用)

调试价值

  • SCX_EV_BYPASS_ACTIVATE 大于 0 说明调度器曾经发生错误或被干预
  • SCX_EV_ENQ_SKIP_EXITING 高说明大量短命任务,考虑启用 SCX_OPS_ENQ_EXITING
  • SCX_EV_DISPATCH_KEEP_LAST 高说明 CPU 饥饿严重,dispatch() 无法及时提供任务

19. 调试与诊断

19.1 dump 回调

BPF 调度器可实现 dumpdump_cpudump_task 三个回调,在错误退出时提供额外的调试信息。这些信息会附加到 scx_exit_info.dump 缓冲区中。

BPF 程序使用 scx_bpf_dump() kfunc 写入转储内容:

// 示例:在 dump_task 中输出任务的 vtime
void BPF_STRUCT_OPS(my_dump_task, struct scx_dump_ctx *ctx,
                    struct task_struct *p)
{
    scx_bpf_dump("task %s[%d] vtime=%llu", p->comm, p->pid, p->scx.dsq_vtime);
}

19.2 print_scx_info

文件kernel/sched/ext.c,第 5708 行

在内核打印任务状态时(如 RCU stall、hung task 报告),print_scx_info() 会输出:

  • 当前 sched_ext 调度器的名称和状态
  • 目标任务的 runnable_at 时间戳(+/-Xms 格式)
// kernel/sched/ext.c:5726
printk("%sSched_ext: %s (%s%s), task: runnable_at=%s",
       log_lvl, sch->ops.name, scx_enable_state_str[state], all,
       runnable_at_buf);

这帮助系统管理员快速判断是否是 BPF 调度器导致的挂起问题,以及任务等待了多久。

19.3 trace 事件

sched_ext 通过 include/trace/events/sched_ext.h 定义了若干 trace 点(如 trace_sched_ext_eventtrace_sched_ext_dumptrace_sched_ext_bypass_lb),可通过 ftraceperf 工具采集。

trace 点激活:

# 激活 sched_ext 相关 trace
echo 1 > /sys/kernel/debug/tracing/events/sched_ext/enable

# 查看 trace
cat /sys/kernel/debug/tracing/trace

19.4 scx_softlockup / scx_hardlockup

文件include/linux/sched/ext.h,第 232-234 行

专为 lockup detector 提供的接口,可以在 softlockup 和 hardlockup 检测中获取 sched_ext 相关的上下文信息,帮助区分是 BPF 调度器 bug 还是其他内核问题。

当 softlockup 触发时,print_scx_info() 会被调用,输出当前调度器名称和任务的等待时长,便于快速定位。


20. 开发注意事项

20.1 必须实现的回调

技术上所有回调都是可选的,但一个有意义的调度器通常需要:

  • enqueue:将任务放入某个 DSQ(否则所有任务都直接进入全局 DSQ)
  • dispatch:从 DSQ 取任务到 local DSQ(否则全局 DSQ 任务永远无法执行)
  • init / exit:资源管理

20.2 任务饥饿与 fallback

若 BPF 调度器遗漏了某个任务(不将其 dispatch),watchdog 会在超时后终止调度器。在开发阶段,建议设置较小的 timeout_ms(如 5000ms)以快速发现问题。

20.3 不可阻塞的回调

除了标注为 "sleepable" 的回调(initexitinit_taskexit_task、CPU hotplug 相关、cgroup 相关),所有其他回调在持有 rq 锁的上下文中运行,不能睡眠,不能调用可能阻塞的函数。

20.4 vtime 溢出保护

使用 p->scx.dsq_vtime 时要注意时间回绕。scx_dsq_priq_less() 使用 time_before64() 正确处理溢出,但 BPF 程序自身的 vtime 计算也应注意:

// scx_simple.bpf.c:82 - 防止空闲任务积累过多 vtime 信用
if (time_before(vtime, vtime_now - SCX_SLICE_DFL))
    vtime = vtime_now - SCX_SLICE_DFL;

不加此限制的后果:长时间休眠的任务重新唤醒时 dsq_vtime 远小于 vtime_now,在 vtime 队列中将优先于所有活跃任务,可能造成短暂的不公平性。

20.5 disallow 字段

init_task() 中(非 fork 路径)可设置 p->scx.disallow = true,拒绝该任务切换到 SCX 策略。若任务已是 SCHED_EXT,其策略会被强制恢复为 SCHED_NORMAL,并通过 /sys/kernel/sched_ext/nr_rejected 计数。

注意disallow 只在 init_task() 的非 fork 路径(即调度器加载时初始化已有任务时)生效;fork 路径中设置此字段无效。

20.6 core-sched 支持

若系统启用了 CONFIG_SCHED_CORE(防侧信道的核心调度),可实现 ops.core_sched_before() 回调来自定义兄弟 SMT 线程的执行顺序。若未实现,默认按 core_sched_at 时间戳 FIFO 排序(touch_core_sched() 在任务开始运行时更新此时间戳,kernel/sched/ext.c 第 911 行):

// kernel/sched/ext.c:911
static void touch_core_sched(struct rq *rq, struct task_struct *p)
{
    lockdep_assert_rq_held(rq);
#ifdef CONFIG_SCHED_CORE
    // 在时间片耗尽时更新时间戳,实现全局 FIFO 顺序
    if (sched_core_disabled())
        p->scx.core_sched_at = rq_clock_task(rq);
    else
        p->scx.core_sched_at = rq_clock_task(rq_of(rq->core));
#endif
}

20.7 多调度器(开发中)

源码注释(kernel/sched/ext.c 第 12 行)提到 sched_ext 正在实现多调度器支持:

/*
 * NOTE: sched_ext is in the process of growing multiple scheduler support
 * and scx_root usage is in a transitional state...
 */
static struct scx_sched __rcu *scx_root;

当前架构只支持一个全局 BPF 调度器实例(scx_root),多调度器支持会允许不同任务组使用不同的 BPF 调度器。

20.8 scx_bpf_kick_cpu 语义

BPF 程序可通过 scx_bpf_kick_cpu(cpu, flags) 主动唤醒或抢占其他 CPU:

标志 含义
SCX_KICK_IDLE 仅唤醒空闲 CPU(有任务时)
SCX_KICK_PREEMPT 抢占目标 CPU 上的当前任务(立即重调度)
SCX_KICK_WAIT 等待目标 CPU 完成一次调度循环后返回

SCX_KICK_WAIT 使用 per-CPU 的 scx_kick_syncs 数组(O(nr_cpu_ids^2) 分配,kernel/sched/ext.c 第 85 行)实现同步,避免需要时再分配。


21. 内核启用流程全景

21.1 scx_enable_workfn 详细步骤

文件kernel/sched/ext.c,第 4989 行

scx_enable_workfn()
  |
  +-- mutex_lock(scx_enable_mutex)
  +-- alloc_kick_syncs() -- O(nr_cpu_ids^2) 内存分配
  +-- scx_alloc_and_add_sched(ops):
  |     alloc scx_sched
  |     alloc exit_info (msg + dump 缓冲区)
  |     init dsq_hash (rhashtable)
  |     alloc global_dsqs[nr_node_ids]
  |     alloc pcpu 统计
  |     alloc helper kthread_worker
  |
  +-- 状态: DISABLED --> ENABLING
  +-- 清零 scx_nr_rejected
  +-- 初始化所有 CPU 的 cpuperf_target = SCX_CPUPERF_ONE
  |
  +-- cpus_read_lock()  -- 稳定 CPU 拓扑
  +-- rcu_assign_pointer(scx_root, sch)  -- 使调度器可见
  +-- scx_bypass(true)  -- 激活 bypass
  |
  +-- scx_fork_rwsem 写锁  -- 阻止新 fork
  +-- for_each_task:
  |     scx_init_task(p, tg, fork=false)
  |       --> ops.init_task(p, {fork=false, cgroup=...})
  |       --> 状态: NONE --> READY
  +-- 释放 scx_fork_rwsem
  |
  +-- ops.init()  -- BPF 调度器初始化
  +-- scx_cgroup_init(sch)  -- 遍历 cgroup, ops.cgroup_init()
  |
  +-- scx_fork_rwsem 写锁
  +-- for_each_task:
  |     scx_enable_task(p)
  |       --> ops.enable(p)
  |       --> 状态: READY --> ENABLED
  |       --> 将任务迁移到 ext_sched_class
  +-- scx_switching_all = !SCX_OPS_SWITCH_PARTIAL
  +-- 释放 scx_fork_rwsem
  |
  +-- 状态: ENABLING --> ENABLED
  +-- static_branch_enable(__scx_enabled)
  +-- cpus_read_unlock()
  |
  +-- scx_bypass(false)  -- 停止 bypass
  +-- 启动 watchdog delayed_work
  +-- mutex_unlock(scx_enable_mutex)

21.2 scx_disable_workfn 清理流程

disable 过程是 enable 的逆序,同样通过 kthread_worker 执行:

scx_disable_workfn()
  |
  +-- 停止 watchdog
  +-- 状态: ENABLED --> DISABLING
  +-- scx_bypass(true)
  |
  +-- scx_fork_rwsem 写锁
  +-- for_each_task:
  |     scx_disable_task(p)
  |       --> ops.disable(p)
  |       --> 状态: ENABLED --> READY
  |       --> 将任务迁移回 fair_sched_class(或按 policy 选择)
  +-- scx_switching_all = false
  +-- 释放 scx_fork_rwsem
  |
  +-- scx_cgroup_exit(sch)  -- ops.cgroup_exit() for all cgroups
  +-- ops.exit(exit_info)   -- BPF 调度器清理
  |
  +-- scx_fork_rwsem 写锁
  +-- for_each_task:
  |     scx_exit_task(p)
  |       --> ops.exit_task(p, {cancelled=false})
  |       --> 状态: READY --> NONE
  +-- 释放 scx_fork_rwsem
  |
  +-- rcu_assign_pointer(scx_root, NULL)
  +-- 状态: DISABLING --> DISABLED
  +-- static_branch_disable(__scx_enabled)
  +-- scx_bypass(false)
  |
  +-- 通过 rcu_work 异步释放 scx_sched:
  |     free kick_syncs
  |     free DSQ hash 中所有用户 DSQ
  |     free global_dsqs
  |     free pcpu 统计
  |     free exit_info
  |     kfree scx_sched

22. 并发安全与锁模型

22.1 锁层级

sched_ext 遵守以下锁层级(按加锁顺序,不可逆序):

scx_enable_mutex        (最外层,enable/disable 序列化)
  |
  +-- cpus_read_lock()  (CPU 热插拔保护)
  |
  +-- scx_fork_rwsem    (fork 路径同步)
  |
  +-- rq->lock          (per-CPU 运行队列锁,可以嵌套多个但有特定顺序)
  |     |
  |     +-- dsq->lock   (DSQ 锁,低于 rq 锁)
  |
  +-- scx_tasks_lock    (任务列表锁,可在任意上下文获取)

禁止的操作

  • 持有 rq 锁时不能获取 scx_enable_mutex
  • 持有 dsq 锁时不能获取 rq 锁
  • 在 BPF ops 回调中不能直接加 rq 锁(使用 kfunc 替代)

22.2 RCU 的使用

  • scx_root:通过 rcu_assign_pointer/rcu_dereference 访问;调度器附属的任务可以直接裸访问(注释:ext.c 第 14 行)
  • 用户自定义 DSQ:通过 rhashtable 的 RCU 保护查找(find_user_dsq 在 rcu_read_lock 内调用)
  • scx_kick_syncs:通过 RCU 保护的 per-CPU 指针访问

延迟释放:用户自定义 DSQ 通过 kfree_rcu() 安全释放(kernel/sched/ext.c 第 3491 行),确保所有读者退出 RCU 读临界区后才真正释放内存。

22.3 无锁快速路径

sched_ext 在多处使用无锁技术优化热路径:

  1. scx_rq_bypassing() 快速检查:使用 unlikely() 标记,编译器生成分支预测优化代码
  2. SCX_HAS_OP() 位图检查test_bit() 是原子操作但无需锁
  3. direct_dispatch_task per-CPU 变量:在 select_cpu/enqueue 调用前后标记,避免额外的状态传递
  4. __scx_enabled static key:使用 Linux static key 机制,禁用状态下零开销
  5. ops_state atomic:任务所有权转移使用 atomic_long_cmpxchg,无锁实现跨 CPU 状态机

23. 与上游社区的演进

23.1 版本历史

  • v6.11(2024 年 9 月):sched_ext 首次合并进 Linux 主线,由 Tejun Heo (Meta) 和 Andrea Righi (NVIDIA) 主导
  • v6.12:修复 watchdog、cgroup 集成等早期 bug,增加 SCX_OPS_BUILTIN_IDLE_PER_NODE
  • v6.13:增加 scx_bpf_now()scx_bpf_dsq_move()(更灵活的 DSQ 间移动),改进 ext_idle.c 拓扑感知
  • v7.0(开发中):多调度器支持,scx_root 向多实例方向演进

23.2 废弃特性

以下特性已被标记为废弃或移除:

特性 状态 替代方案
ops.cpu_acquire/release() 废弃(v6.13) sched:sched_switch tracepoint
SCX_OPS_HAS_CGROUP_WEIGHT 废弃(v6.14 前移除) 自动检测
scx_bpf_dispatch() 重命名 scx_bpf_dsq_insert()
scx_bpf_consume() 重命名 scx_bpf_dsq_move_to_local()

23.3 生态系统

sched_ext 的用户空间工具链由独立的 scx 仓库维护(与内核主线分离),包括:

  • scx_rusty:基于 Rust 的多层次调度器,支持 NUMA 感知负载均衡
  • scx_lavd:Latency-Aware Virtual Deadline 调度器,针对游戏/桌面工作负载
  • scx_bpfland:面向低延迟的通用调度器
  • scx_p2dq:基于 Pull-Push Dispatch Queue 的调度器,重点优化 LLC/NUMA 局部性
  • 工具scxtop(类似 htop 的调度器监控工具)、veristat 兼容性

参考资料

  • 内核文档:Documentation/scheduler/sched-ext.rst
  • 核心实现:kernel/sched/ext.c(约 5800+ 行)
  • 内部数据结构:kernel/sched/ext_internal.h
  • 空闲 CPU 追踪:kernel/sched/ext_idle.c
  • 公共接口:include/linux/sched/ext.h
  • 内部接口:kernel/sched/ext.h
  • trace 事件:include/trace/events/sched_ext.h
  • 参考实现:
    • tools/sched_ext/scx_simple.bpf.c:最小可用调度器(全局 vtime)
    • tools/sched_ext/scx_flatcg.bpf.c:扁平化 cgroup 调度器
    • tools/sched_ext/scx_central.bpf.c:中央 CPU 集中调度
    • tools/sched_ext/scx_qmap.bpf.c:五级 FIFO 优先级队列
    • tools/sched_ext/scx_pair.bpf.c:SMT 感知 cgroup 对调度器
  • 测试用例:tools/testing/selftests/sched_ext/
  • 版权:Meta Platforms, Inc. (2022),NVIDIA (2024)
    • 主要作者:Tejun Heo、David Vernet、Andrea Righi

由 Claude Code 分析生成