基于 Linux kernel 源码分析,主要文件:
kernel/sched/ext.ckernel/sched/ext_internal.hinclude/linux/sched/ext.hkernel/sched/ext.hkernel/sched/ext_idle.ckernel/sched/ext_idle.htools/sched_ext/scx_simple.bpf.ctools/sched_ext/scx_flatcg.bpf.ctools/sched_ext/scx_central.bpf.ctools/sched_ext/scx_qmap.bpf.ctools/sched_ext/scx_pair.bpf.c
- 设计目标与背景
- 整体架构
- 核心数据结构
- 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 运行队列扩展
- 3.1
- 任务状态机
- 4.1
scx_task_state:任务初始化状态 - 4.2
scx_ops_state:任务所有权状态机
- 4.1
- 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 生命周期管理
- 5.1 内置 DSQ:
- 调度回调函数详解
- 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:调试信息转储
- 6.1
- 任务分发路径
- 7.1 直接分发(Direct Dispatch)
- 7.2 延迟分发
- 7.3
dispatch_enqueue实现 - 7.4
balance_scx分发循环
- 与 CFS 的共存机制
- 8.1
sched_class优先级链 - 8.2 Bypass 模式
- 8.3
SCX_OPS_SWITCH_PARTIAL:部分切换
- 8.1
- Watchdog 超时机制
- 9.1 设计原理
- 9.2 双重检测:delayed_work + tick
- 9.3
scx_exit_info退出信息收集
- kfunc 权限控制
- BPF struct_ops 注册机制
- 11.1 注册与注销流程
- 11.2
bpf_scx_init_member:字段验证 - 11.3 BPF 程序可写字段
- 11.4
scx_enable执行流程
- 典型实现分析
- 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 对调度器
- 12.1
- 内置空闲 CPU 选择机制
- 13.1
scx_idle模块架构 - 13.2 SMT 感知空闲 CPU 追踪
- 13.3 NUMA 和 LLC 拓扑感知
- 13.4
scx_bpf_select_cpu_dfl:默认 CPU 选择
- 13.1
- cgroup 与 cpuset 集成
- 14.1
CONFIG_EXT_GROUP_SCHED:cgroup 权重支持 - 14.2 cgroup 生命周期回调
- 14.3 任务 cgroup 迁移
- 14.4 带宽控制参数
- 14.1
- CPU 热插拔处理
- 15.1 热插拔序列号机制
- 15.2
cpu_online/cpu_offline回调 - 15.3
handle_hotplug内核内部处理
- 全局变量与模块参数
- 16.1 核心全局变量
- 16.2 可调模块参数
- 16.3 电源管理集成
- sysfs 接口
- 17.1
/sys/kernel/sched_ext属性 - 17.2 调度器实例 kobject
- 17.1
- 性能事件统计
- 调试与诊断
- 19.1 dump 回调
- 19.2
print_scx_info - 19.3 trace 事件
- 19.4
scx_softlockup/scx_hardlockup
- 开发注意事项
- 内核启用流程全景
- 21.1
scx_enable_workfn详细步骤 - 21.2
scx_disable_workfn清理流程
- 21.1
- 并发安全与锁模型
- 22.1 锁层级
- 22.2 RCU 的使用
- 22.3 无锁快速路径
- 与上游社区的演进
Linux 内核的调度器长期以 CFS(Completely Fair Scheduler)为主。CFS 作为通用调度器表现良好,但面对特定工作负载时,开发者希望能够定制调度策略。传统方式有三条路:
- 修改内核源码:代价高,维护困难,无法迭代部署
- 使用 cgroup 和 nice 值:能力有限,无法实现完全定制的调度逻辑
- 用户态调度(如
SCHED_DEADLINE):有一定灵活性但局限性多
sched_ext(Scheduler Extensions,缩写 SCX)由 Meta Platforms 的 Tejun Heo 和 David Vernet 于 2022 年提出并实现,核心思想是:允许 BPF 程序作为一个完整的调度器类(sched_class)运行在内核态,获得与 CFS 同等的调度权限,同时利用 BPF 的安全验证器保证内核稳定性。
- 完整的调度策略自定义:BPF 程序可以实现从 CPU 亲和性选择到任务分发的全链路调度逻辑
- 快速迭代:无需重新编译内核即可加载/卸载新的调度策略
- 安全性:BPF 验证器确保 BPF 程序不会破坏内核稳定性;watchdog 机制防止 BPF 调度器死锁导致系统挂起
- 共存性:SCX 在调度类优先级链中处于 CFS 下方,实时任务(RT、DL)仍可抢占
- 渐进迁移:通过
SCX_OPS_SWITCH_PARTIAL可以只让部分任务(SCHED_EXT 策略)使用 BPF 调度器,其余任务仍走 CFS
sched_ext 需要启用内核配置选项 CONFIG_SCHED_CLASS_EXT。可通过 kernel/sched/Kconfig 查看依赖关系。
cgroup 组调度支持需要额外启用 CONFIG_EXT_GROUP_SCHED,它依赖 CONFIG_CGROUP_SCHED。
用户空间 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 任务优先级最低
文件: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_flags,kernel/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 行)。
文件: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_scx,kernel/sched/ext.c第 948 行),归零时触发调度事件。BPF 调度器通过scx_bpf_dsq_insert()设置时间片,默认值为SCX_SLICE_DFL = 20ms(include/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_state:atomic_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.slice、scx.dsq_vtime 和 scx.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;文件: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_GLOBAL、SCX_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;
}文件: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))];
}文件: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。
每个 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 回调待添加 |
文件: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 = 8,SCX_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。
文件: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 |
`---------------'
关键设计点:
- 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);-
内存序:从
QUEUEING/DISPATCHING到NONE/QUEUED的转换必须使用atomic_long_set_release(),等待方使用atomic_long_read_acquire()(ext_internal.h第 1099 行),确保内存可见性。 -
忙等待:当分发路径需要等待
QUEUEING或DISPATCHING状态结束时,使用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);
}文件: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())。
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 行)。
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)
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);
}创建: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 行):
- 从
dsq_hash中移除(rhashtable_remove_fast) - 将
dsq->id设为SCX_DSQ_INVALID,阻止新的入队 - 通过
llist+ irq_work 延迟释放(free_dsq_irq_work,ext.c第 3494 行) - 实际释放通过
kfree_rcu()保证 RCU 安全
注意:如果试图销毁仍有任务的 DSQ,destroy_dsq() 会调用 scx_error() 触发调度器错误退出(ext.c 第 3510 行)。
// 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_FORK、SCX_WAKE_TTWU、SCX_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_central 的 select_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 行)。
// 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_WAKEUP、SCX_ENQ_PREEMPT、SCX_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(内部使用) |
// kernel/sched/ext_internal.h:329
void (*dequeue)(struct task_struct *p, u64 deq_flags);- 调用时机:从 BPF 调度器侧移除任务时(
ops_dequeue,kernel/sched/ext.c第 1523 行),常见于优先级/亲和性变更 - 注意:SCX 内核核心自动跟踪任务的 BPF 所有权状态,可安全忽略无效的分发,因此此回调不实现也不会导致崩溃,但可能导致调度位置不正确
deq_flags 关键标志:
| 标志 | 描述 |
|---|---|
SCX_DEQ_SLEEP |
任务进入睡眠 |
SCX_DEQ_CORE_SCHED_EXEC |
core-sched 层决定立即执行该任务 |
SCX_DEQ_MIGRATING |
任务正在迁移到其他 CPU |
// 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 锁。
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;
}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()
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_PREEMPT(scx_central.bpf.c 注释第 15-22 行)。
bool (*yield)(struct task_struct *from, struct task_struct *to);to == NULL:from让出 CPU 给其他可运行任务(sched_yield系统调用路径)to != NULL:from希望让出 CPU 给to(yield-to 语义),返回true表示成功
若 BPF 调度器未实现 yield,内核默认行为是将当前任务的 slice 清零,触发重新调度(kernel/sched/ext.c 中的 yield_task_scx)。
s32 (*init)(void);
void (*exit)(struct scx_exit_info *info);init:BPF 调度器加载时调用,可睡眠,适合分配资源。返回非零值终止加载exit:BPF 调度器卸载时调用(包括init失败的情况),通过info获取退出原因
重要:即使 init() 失败,exit() 也可能被调用(SCX_EFLAG_INITIALIZED 标志未置位时)。这允许 BPF 调度器在 exit() 中进行一致性清理,无需区分 init() 是否成功。
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 下运行
};void (*enable)(struct task_struct *p);
void (*disable)(struct task_struct *p);每次任务进入/离开 SCX 管理时调用(与 init_task/exit_task 不同,这对回调可以多次触发)。enable 和 disable 总是成对出现。
典型用法(来自 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 推进后,新任务将永远排在所有旧任务之前,破坏公平性并可能导致旧任务饥饿。
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 任务完成而饥饿)。
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 行)。
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)。
直接分发是一种优化路径,允许在 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_task(kernel/sched/ext.c 第 99 行)在 select_cpu/enqueue 调用前设置为当前任务指针,调用后检查是否被替换为 ERR_PTR,若是则触发直接分发路径。
直接分发到 SCX_DSQ_LOCAL 的特殊语义:若在 select_cpu() 中直接分发到 SCX_DSQ_LOCAL,任务进入的是 select_cpu() 返回的 CPU 的本地 DSQ,而非调用者所在 CPU 的本地 DSQ。这是实现高效唤醒分发的关键。
当在 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 行):
SCX_RQ_IN_WAKEUP路径:若当前在task_woken_scx()中,该函数返回后自动处理SCX_RQ_IN_BALANCE路径:在 balance 结束时处理(设置SCX_RQ_BAL_CB_PENDING)- irq_work 路径:通过
irq_work_queue()在 IRQ 重新使能时处理
process_ddsp_deferred_locals() 函数(kernel/sched/ext.c 第 2279 行)实际处理延迟列表,对每个延迟任务获取目标 CPU 的 rq 锁后真正插入 local DSQ。
文件: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_enq,kernel/sched/ext.c 第 993 行):
若入队标志包含 SCX_ENQ_PREEMPT 且当前 CPU 正在运行另一个 SCX 任务,则将当前任务的 slice 清零并调用 resched_curr(),触发立即重调度。这是 sched_ext 中实现任务抢占的标准方式。
文件: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_BALANCESCX_RQ_BAL_KEEP 语义:当 dispatch 找到任务后不立即切换,而是先检查 prev 是否仍有 slice,若有则继续执行 prev(避免无谓的上下文切换)。
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()实现,不使用此 hookmigrate_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 调度)
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_depth,kernel/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 行)。
通过设置 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);
}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)。若未指定,默认为最大值。
watchdog 使用两层独立的检测机制:
层 1:delayed_work(kernel/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_at(kernel/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 是否也超时
文件: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 行)。
sched_ext 实现了一套细粒度的 kfunc(内核函数)调用权限控制系统,确保 BPF 程序只能在合法的上下文中调用特定的内核函数。
权限掩码(scx_kf_mask,include/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_state(kernel/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_RET、SCX_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 |
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_ops,kernel/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, // 结构体字段写权限
};文件:kernel/sched/ext.c,第 5334 行
在 BPF 程序加载时,内核从用户空间复制并验证 sched_ext_ops 的各个字段:
dispatch_max_batch:不超过INT_MAXflags:只允许SCX_OPS_ALL_FLAGS定义的标志(高 8 位为内部标志,不可设置)name:合法的 BPF 对象名(非空,通过bpf_obj_name_cpy验证)timeout_ms:msecs_to_jiffies后不超过SCX_WATCHDOG_MAX_TIMEOUTexit_dump_len:0 时使用默认值SCX_EXIT_DUMP_DFL_LEN = 32768hotplug_seq:允许调度器在已发生热插拔事件时检测并做出反应
通过 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)。
文件: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 行):
mutex_lock(&scx_enable_mutex)— 保证单实例alloc_kick_syncs()— 分配 per-CPU kick 同步数组(O(nr_cpu_ids^2))scx_alloc_and_add_sched(ops)— 分配scx_sched、全局 DSQ、per-CPU 统计- 状态设为
SCX_ENABLING cpus_read_lock()— 固定 CPU 拓扑rcu_assign_pointer(scx_root, sch)— 使调度器实例可见- 激活 bypass 模式(
scx_bypass(true)) - 遍历所有任务,调用
scx_init_task()(TASK_NONE->TASK_READY) - 调用
ops.init() scx_cgroup_init()— 初始化 cgroup 状态- 遍历所有任务,调用
scx_enable_task()(TASK_READY->TASK_ENABLED) - 状态设为
SCX_ENABLED,scx_switching_all更新 - 停止 bypass(
scx_bypass(false)) - 启动 watchdog
不支持热更新(bpf_scx_update,kernel/sched/ext.c 第 5428 行):sched_ext 不支持在不卸载当前调度器的情况下更新 BPF 程序(返回 -EOPNOTSUPP),因为 init() 可能失败,无法保证热更新的原子性。
文件: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 |
关键设计:
-
select_cpu:使用内置scx_bpf_select_cpu_dfl()选择 CPU。若找到空闲 CPU,直接 dispatch 到SCX_DSQ_LOCAL,跳过enqueue。 -
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);
- FIFO 模式:
-
dispatch:简单地scx_bpf_dsq_move_to_local(SHARED_DSQ)移动一个任务到当前 CPU -
running/stopping:维护全局vtime_now和p->scx.dsq_vtime -
enable:设置p->scx.dsq_vtime = vtime_now防止新任务饥饿
适用场景:CPU 拓扑均匀(统一 L3 缓存),无抢占需求的简单工作负载
文件: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 虚拟时间(用于调度顺序)
// ...
};调度流程:
dispatch()先选择下一个应该服务的 cgroup(基于 cvtime 虚拟时间)- 再从该 cgroup 的 DSQ 中取出一个任务
性能表现(源码注释,scx_flatcg.bpf.c 第 34 行):
- 相比 CFS(禁用 CPU controller):性能持平或略优(~3%)
- 相比 CFS(启用 CPU controller,4 层嵌套,2:1 权重比):高出 ~10%
局限性:对 cgroup 同时激活的"惊群"场景处理不佳,低优先级父 cgroup 下同时唤醒大量任务时,可能短暂超出其 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_PREEMPT(scx_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,并安全地跳过。
文件: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 的任务使用 SCXstall_user_nth/stall_kernel_nth:每第 N 个用户/内核任务故意不入队,触发 watchdog 超时,用于测试 watchdog 机制
文件: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 IDdraining:排空标志
调度逻辑(scx_pair.bpf.c 第 63-80 行):
- 检查
pair_ctx.draining状态 - 若当前 cgroup 时间片耗尽或队列空,切换到新 cgroup(需等待另一个 CPU 完成当前 cgroup 的任务)
- 使用
bpf_kptr_xchg()等原子操作协调两个 CPU 的决策
这种设计保证了在任意时刻,一个物理核的两个逻辑 CPU 只运行同一 cgroup 的任务(或空闲)。
文件: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]
文件:kernel/sched/ext_idle.c,第 77 行
scx_idle_test_and_clear_cpu() 在 CPU 被选中时同时更新 cpu 和 smt 位图:
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 核的调度(避免一个物理核的两个线程分别运行不同任务,降低缓存污染)。
文件: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(兜底)
内置 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 时立即执行,无需等待其他任务完成。
当内核编译了 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;
}文件: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()。
当任务从一个 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 行)。
文件: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 回调中更新折算权重来实现层级化带宽控制。
文件: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 节)。
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 资源。
当 CPU 热插拔发生时(kernel/sched/ext.c 中的 scx_rq_online/scx_rq_offline),内核会:
- 设置/清除
SCX_RQ_ONLINE标志 - 调用
ops.cpu_online()/ops.cpu_offline() - 递增
scx_hotplug_seq - 若
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 行)。
文件: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 调度器(即使当前已卸载),适合安全审计场景。
文件: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);
}文件: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 行)。
文件:kernel/sched/ext.c,第 3613-3665 行
sched_ext 通过 kset 在 /sys/kernel/sched_ext 暴露全局只读属性:
| 属性文件 | 读取内容 | 说明 |
|---|---|---|
state |
disabled/enabling/enabled/disabling |
当前调度器状态 |
switch_all |
0 或 1 |
是否切换所有任务(!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()]);
}当 BPF 调度器启用时,scx_sched.kobj 会在 /sys/kernel/sched_ext/<scheduler_name> 下挂载调度器特定的属性(如 ops 名称、统计数据等)。这个接口在调度器卸载时自动移除。
文件: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_EXITINGSCX_EV_DISPATCH_KEEP_LAST高说明 CPU 饥饿严重,dispatch()无法及时提供任务
BPF 调度器可实现 dump、dump_cpu、dump_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);
}文件: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 调度器导致的挂起问题,以及任务等待了多久。
sched_ext 通过 include/trace/events/sched_ext.h 定义了若干 trace 点(如 trace_sched_ext_event、trace_sched_ext_dump、trace_sched_ext_bypass_lb),可通过 ftrace 或 perf 工具采集。
trace 点激活:
# 激活 sched_ext 相关 trace
echo 1 > /sys/kernel/debug/tracing/events/sched_ext/enable
# 查看 trace
cat /sys/kernel/debug/tracing/trace文件:include/linux/sched/ext.h,第 232-234 行
专为 lockup detector 提供的接口,可以在 softlockup 和 hardlockup 检测中获取 sched_ext 相关的上下文信息,帮助区分是 BPF 调度器 bug 还是其他内核问题。
当 softlockup 触发时,print_scx_info() 会被调用,输出当前调度器名称和任务的等待时长,便于快速定位。
技术上所有回调都是可选的,但一个有意义的调度器通常需要:
enqueue:将任务放入某个 DSQ(否则所有任务都直接进入全局 DSQ)dispatch:从 DSQ 取任务到 local DSQ(否则全局 DSQ 任务永远无法执行)init/exit:资源管理
若 BPF 调度器遗漏了某个任务(不将其 dispatch),watchdog 会在超时后终止调度器。在开发阶段,建议设置较小的 timeout_ms(如 5000ms)以快速发现问题。
除了标注为 "sleepable" 的回调(init、exit、init_task、exit_task、CPU hotplug 相关、cgroup 相关),所有其他回调在持有 rq 锁的上下文中运行,不能睡眠,不能调用可能阻塞的函数。
使用 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 队列中将优先于所有活跃任务,可能造成短暂的不公平性。
在 init_task() 中(非 fork 路径)可设置 p->scx.disallow = true,拒绝该任务切换到 SCX 策略。若任务已是 SCHED_EXT,其策略会被强制恢复为 SCHED_NORMAL,并通过 /sys/kernel/sched_ext/nr_rejected 计数。
注意:disallow 只在 init_task() 的非 fork 路径(即调度器加载时初始化已有任务时)生效;fork 路径中设置此字段无效。
若系统启用了 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
}源码注释(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 调度器。
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 行)实现同步,避免需要时再分配。
文件: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)
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
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 替代)
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 读临界区后才真正释放内存。
sched_ext 在多处使用无锁技术优化热路径:
scx_rq_bypassing()快速检查:使用unlikely()标记,编译器生成分支预测优化代码SCX_HAS_OP()位图检查:test_bit()是原子操作但无需锁direct_dispatch_taskper-CPU 变量:在select_cpu/enqueue调用前后标记,避免额外的状态传递__scx_enabledstatic key:使用 Linux static key 机制,禁用状态下零开销- ops_state atomic:任务所有权转移使用
atomic_long_cmpxchg,无锁实现跨 CPU 状态机
- 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向多实例方向演进
以下特性已被标记为废弃或移除:
| 特性 | 状态 | 替代方案 |
|---|---|---|
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() |
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 分析生成