基于 Linux 6.18.26 内核源码(
kernel/sched/ext.c)分析。系列文章:
前言
struct sched_ext_ops 代表一个 BPF 调度器,其中定义了多个回调函数(hook)。当一个 task 被 sched_ext 调度器正式接管时触发 enable 回调。
与 init_task 的区别在于:init_task 无论 task 是否被 sched_ext 接管都会触发,而 enable 只在 task 真正落入 sched_ext 管理时才触发。可以理解为 init_task 是"登记",enable 是"上岗"。
本文将围绕 enable 推动的状态转换、底层机制、三种触发场景展开分析,并给出 scx_simple 调度器中的实战示例。
一、从 READY 到 ENABLED:enable 推动的最后一步
第一篇文章 已经介绍了 task 在 sched_ext 中的四种状态(NONE → INIT → READY → ENABLED),其中 init_task 负责 NONE → INIT 的推进,内核代码将 INIT 推进到 READY。本文聚焦最后一步:READY → ENABLED。
这一步由 scx_enable_task() 完成,它调用 BPF 调度器的 enable 回调,然后将 task 状态设为 SCX_TASK_ENABLED。至此 task 正式被 sched_ext 管理,后续的 enqueue、dispatch、running、stopping 等 hook 才会对其生效。
init_task 对所有 task 生效,而 enable 只对调度类为 ext_sched_class 的 task 生效。如果一个 task 全程运行在 CFS 或 RT 下,它只会被 init_task "登记",永远不会被 enable "上岗"。
那么,一个 task 的调度类是如何变成 ext_sched_class 的?这在三种场景下发生:
三种触发场景
注册调度器时
批量切换已有 task
fork 新 task 时
自动继承
系统调用
主动切换调度类
共享底层机制
scx_setscheduler_class
设置调度类
scx_enable_task
完成接管
enable hook
BPF 回调
这三种场景虽然入口不同,但最终都通过相同的底层机制完成 enable。下面先看这些共享的底层机制,再逐一分析三种场景。
二、enable 的底层机制
2.1 scx_setscheduler_class调度类
scx_setscheduler_class 为 task 计算应该使用的调度类:
c
// kernel/sched/ext.c
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);
}
这个函数逻辑很简单:如果 task 当前是 stop_sched_class(内核 migration 线程等),则保持不变------这类内核线程不能被 sched_ext 接管;否则调用 __setscheduler_class 根据优先级和策略返回对应的调度类。
__setscheduler_class 按优先级逐级判断,当 task_should_scx 返回 true 时返回 ext_sched_class:
c
// kernel/sched/core.c
const struct sched_class *__setscheduler_class(int policy, int prio)
{
if (dl_prio(prio))
return &dl_sched_class;
if (rt_prio(prio))
return &rt_sched_class;
if (task_should_scx(policy))
return &ext_sched_class;
return &fair_sched_class;
}
task_should_scx 决定一个 task 是否应该被 sched_ext 管理:
c
// kernel/sched/ext.c
bool task_should_scx(int policy)
{
if (!scx_enabled() ||
unlikely(scx_enable_state() == SCX_DISABLING))
return false;
if (READ_ONCE(scx_switching_all))
return true;
return policy == SCHED_EXT;
}
整个调度类选择逻辑可以用下面的流程图概括:
是
否
是
否
是
否
是
否
scx_setscheduler_class(p)
p→sched_class ==
stop_sched_class?
返回 stop_sched_class
__setscheduler_class(p→policy, p→prio)
dl_prio(prio)?
返回 dl_sched_class
rt_prio(prio)?
返回 rt_sched_class
task_should_scx(policy)?
返回 ext_sched_class
返回 fair_sched_class
2.2 接管的核心:scx_enable_task
task 的调度类设置为 ext_sched_class 后,scx_enable_task 负责完成接管:
c
// kernel/sched/ext.c
static void scx_enable_task(struct task_struct *p)
{
struct scx_sched *sch = scx_root;
struct rq *rq = task_rq(p);
u32 weight;
lockdep_assert_rq_held(rq);
// 在调用 enable 之前先设置权重,
// 这样 BPF 调度器的 enable 回调可以读到正确的 scx.weight
if (task_has_idle_policy(p))
weight = WEIGHT_IDLEPRIO;
else
weight = sched_prio_to_weight[p->static_prio - MAX_RT_PRIO];
p->scx.weight = sched_weight_to_cgroup(weight);
if (SCX_HAS_OP(sch, enable))
SCX_CALL_OP_TASK(sch, SCX_KF_REST, enable, rq, p);
scx_set_task_state(p, SCX_TASK_ENABLED);
if (SCX_HAS_OP(sch, set_weight))
SCX_CALL_OP_TASK(sch, SCX_KF_REST, set_weight, rq,
p, p->scx.weight);
}
scx_enable_task 依次做了三件事:
① 设置 p→scx.weight
② 调用 enable hook
BPF 回调
③ scx_set_task_state
READY → ENABLED
④ 调用 set_weight hook
BPF 回调
- 设置
p->scx.weight,然后调用 enable hook(此时 BPF 回调中可安全读取权重) - 将 task 状态设为 SCX_TASK_ENABLED
- 调用 set_weight hook
2.3 调度类切换的入口:switching_to_scx
task 从其他调度类切换到 ext_sched_class 时,check_class_changing 调用新调度类的 switching_to 钩子。switching_to_scx 在调用 scx_enable_task 之后,还会触发 set_cpumask hook:
c
// kernel/sched/ext.c
static void switching_to_scx(struct rq *rq, struct task_struct *p)
{
scx_enable_task(p);
/*
* set_cpus_allowed_scx() is not called while @p is associated with a
* different scheduler class. Keep the BPF scheduler up-to-date.
*/
if (SCX_HAS_OP(sch, set_cpumask))
SCX_CALL_OP_TASK(sch, SCX_KF_REST, set_cpumask, rq,
p, (struct cpumask *)p->cpus_ptr);
}
注意:switching_to_scx 只在调度类发生切换时被调用。fork 路径中 task 首次设置的调度类就是 ext_sched_class,不存在"切换",因此不经过此函数,也不触发 set_cpumask。
fork 路径
注册切换 / 系统调用路径
check_class_changing
switching_to_scx
scx_enable_task
enable hook
set_weight hook
set_cpumask hook
scx_post_fork
scx_enable_task
enable hook
set_weight hook
set_cpumask ✗ 不触发
三、注册调度器时执行enable(场景1)
sched_ext 调度器有两种接管模式。默认是全量接管 ------注册后系统内所有 task 都会被切换到 ext_sched_class。只有在 struct sched_ext_ops->flags 中设置了 SCX_OPS_SWITCH_PARTIAL 标志,才启用部分接管模式,此时只有调度类被显式设为 SCHED_EXT 的 task 才会被接管。
3.1 全量接管模式下的两种 task
task_should_scx 的判断逻辑决定了哪些 task 会被 sched_ext 接管:
- 全量接管 (
scx_switching_all = true,默认):所有非 DL、非 RT 的 task 都被设置为ext_sched_class - 部分接管 :只有
policy == SCHED_EXT的 task 才被设置为ext_sched_class,其余 task 仍由内核默认调度器管理
3.2 完整调用链
内核调度器 BPF子系统 用户态 内核调度器 BPF子系统 用户态 提交到 scx_enable_helper RT 内核线程执行 第一阶段:init_task (NONE → INIT → READY) 第二阶段:enable (READY → ENABLED) SCX_OPS_ATTACH(numa_aware_ops) bpf_map__attach_struct_ops bpf_link_create → bpf_sys_bpf __sys_bpf → link_create bpf_struct_ops_link_create bpf_scx_reg scx_enable(ops, link) scx_enable_workfn 遍历所有task → scx_init_task() scx_set_task_state(READY) scx_switching_all = true static_branch_enable(__scx_enabled) 遍历所有task → scx_setscheduler_class() p->>sched_class = ext_sched_class check_class_changing → switching_to_scx scx_enable_task → enable hook scx_set_task_state(ENABLED)
3.3 两个阶段的源码分析
第一篇文章 已经详细分析了第一阶段(init_task 循环)。这里重点看第二阶段(enable 循环):
c
// kernel/sched/ext.c scx_enable_workfn() 第二阶段(简化)
percpu_down_write(&scx_fork_rwsem);
scx_task_iter_start(&sti);
while ((p = scx_task_iter_next_locked(&sti))) {
const struct sched_class *old_class = p->sched_class;
const struct sched_class *new_class =
scx_setscheduler_class(p); // 内部调用 __setscheduler_class
struct sched_enq_and_set_ctx ctx;
if (!tryget_task_struct(p))
continue;
if (old_class != new_class && p->se.sched_delayed)
dequeue_task(task_rq(p), p, DEQUEUE_SLEEP | DEQUEUE_DELAYED);
sched_deq_and_put_task(p, DEQUEUE_SAVE | DEQUEUE_MOVE, &ctx);
p->scx.slice = SCX_SLICE_DFL;
p->sched_class = new_class;
check_class_changing(task_rq(p), p, old_class);
// → switching_to_scx → scx_enable_task → enable hook
sched_enq_and_set_task(&ctx);
check_class_changed(task_rq(p), p, old_class, p->prio);
put_task_struct(p);
}
scx_task_iter_stop(&sti);
percpu_up_write(&scx_fork_rwsem);
两个阶段之间设置了接管模式标志:
c
WRITE_ONCE(scx_switching_all, !(ops->flags & SCX_OPS_SWITCH_PARTIAL));
static_branch_enable(&__scx_enabled);
默认情况下 SCX_OPS_SWITCH_PARTIAL 未设置,scx_switching_all 被设为 true,即全量接管。
3.4 bypass 机制
scx_enable_workfn 一开始就进入 bypass 模式(scx_bypass(true)),直到两个阶段全部完成后才退出(scx_bypass(false))。
bypass 模式下,所有调度决策由内核默认逻辑处理,BPF 调度器的 ops 回调被跳过。这确保在 enable 过程中(部分 task 已切换、部分未切换),不会因 BPF 调度器处理不完整而挂死。
3.5 为什么使用专用 RT 内核线程
scx_enable() 并不在调用线程上直接执行,而是将工作提交到名为 scx_enable_helper 的 RT 内核线程。原因:在第二阶段的循环中,调用线程自身的 sched_class 也会从 fair 切换到 ext。由于 fair 优先级高于 ext,在 fair 类饱和时调用线程可能被无限饥饿,导致系统挂死。RT 内核线程不受此影响,可以保证前向推进。
c
// kernel/sched/ext.c 注释
/*
* 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.
*/
四、fork 新 task 时执行enable(场景2)
4.1 调用链
调度子系统 fork系统调用 用户态 调度子系统 fork系统调用 用户态 scx_init_task_enabled = true 时 条件1: scx_init_task_enabled = true 条件2: sched_class == ext_sched_class SYSCALL_DEFINE0(fork) kernel_clone copy_process() dup_task_struct → memcpy(继承父task) sched_fork() __setscheduler_class → ext_sched_class scx_pre_fork() sched_cgroup_fork() scx_fork() scx_init_task() → init_task hook sched_post_fork() scx_post_fork() scx_enable_task() → enable hook wake_up_new_task()
4.2 scx_post_fork 源码分析
c
// kernel/sched/ext.c
void scx_post_fork(struct task_struct *p)
{
if (scx_init_task_enabled) { // 条件1
scx_set_task_state(p, SCX_TASK_READY);
if (p->sched_class == &ext_sched_class) { // 条件2
struct rq_flags rf;
struct rq *rq;
rq = task_rq_lock(p, &rf);
scx_enable_task(p); // → enable hook
task_rq_unlock(rq, p, &rf);
}
}
raw_spin_lock_irq(&scx_tasks_lock);
list_add_tail(&p->scx.tasks_node, &scx_tasks);
raw_spin_unlock_irq(&scx_tasks_lock);
percpu_up_read(&scx_fork_rwsem);
}
fork 路径需要满足两个条件才会触发 enable:
条件 1:scx_init_task_enabled = true
scx_enable_workfn 在第一阶段开始前将其设为 true。如果 sched_ext 调度器未注册,此标志为 false,不会触发任何 sched_ext 回调。
条件 2:p->sched_class == &ext_sched_class
fork 系统调用不会为新 task 指定调度类,新 task 默认继承父 task 的 policy(如 SCHED_NORMAL)。新 task 的 sched_class 是如何变成 ext_sched_class 的?
答案在 sched_fork() 中:内核会为新 task 调用 __setscheduler_class(p->policy, p->prio) 来设置调度类。在全量接管模式下(scx_switching_all = true),task_should_scx(SCHED_NORMAL) 返回 true,因此 __setscheduler_class 返回 ext_sched_class。
scx_switching_all=true
scx未启用
fork 新 task
sched_fork
__setscheduler_class(policy=SCHED_NORMAL, prio)
task_should_scx(SCHED_NORMAL)?
返回 ext_sched_class
返回 fair_sched_class
scx_post_fork: 条件2满足
scx_enable_task → enable hook
scx_post_fork: 条件2不满足
enable hook 不触发
注意:fork 路径直接在
scx_post_fork中调用scx_enable_task,不经过switching_to_scx,因此不会触发set_cpumaskhook。
五、通过系统调执行enable(场景3)
5.1 调用链
用户态程序通过 sched_setscheduler、sched_setparam 或 sched_setattr 等系统调用,将 task 的调度类切换到 ext_sched_class 时,也会触发 enable。
调度子系统 系统调用 用户态 调度子系统 系统调用 用户态 switching_to_scx 1. 设置 p->>scx.weight 2. SCX_CALL_OP(enable) ← enable hook 3. scx_set_task_state(ENABLED) 4. SCX_CALL_OP(set_weight) 5. SCX_CALL_OP(set_cpumask) switched_to 回调 sched_setscheduler(pid, SCHED_EXT, ...) do_sched_setscheduler sched_setscheduler → _sched_setscheduler __sched_setscheduler __setscheduler_class → ext_sched_class p->>sched_class = ext_sched_class check_class_changing(rq, p, old_class) scx_enable_task(p) check_class_changed(rq, p, old_class)
5.2 与场景一的区别
场景一(注册时批量切换)和场景三(系统调用切换)的 enable 触发机制相同,都是通过 check_class_changing → switching_to_scx → scx_enable_task。区别在于:
- 场景一在
scx_enable_workfn的循环中批量处理所有 task - 场景三是针对单个 task 的按需切换
- 场景三也是部分接管模式(
SCX_OPS_SWITCH_PARTIAL)下的主要 enable 路径
六、enable 与其他 hook 的时序关系
6.1 完整时序
init_task → enable → set_weight → set_cpumask → switched_to → enqueue
| Hook | 触发位置 | 说明 |
|---|---|---|
init_task |
scx_init_task() |
task 初始化回调,仅调用一次 |
enable |
scx_enable_task() |
task 被 sched_ext 接管的核心回调 |
set_weight |
scx_enable_task() 中 enable 之后 |
通知 BPF 调度器 task 的权重 |
set_cpumask |
switching_to_scx() |
通知允许运行的 CPU 集合(仅调度类切换时触发,fork 路径不触发) |
switched_to |
check_class_changed() |
调度类切换完成后的通知 |
enqueue |
task 第一次被入队时 | task 进入可运行队列 |
6.2 三种场景的时序差异
系统调用切换
init_task
enable
set_weight
set_cpumask
switched_to
enqueue
fork 新 task
init_task
enable
set_weight
enqueue
注册时批量切换
init_task
enable
set_weight
set_cpumask
switched_to
enqueue
注意:
- fork 路径不经过
switching_to_scx,因此不触发set_cpumask - 系统调用路径在
__sched_setscheduler前面的代码路径中已完成init_task
七、实战:scx_simple 中的 enable 实现
scx_simple 是 Linux 内核自带的示例 sched_ext 调度器(tools/sched_ext/scx_simple.bpf.c),实现了一个全局加权 vtime 调度器。它的 enable 回调只有一行代码,却展示了一个关键的设计模式。
7.1 源码分析
c
// tools/sched_ext/scx_simple.bpf.c
static u64 vtime_now; // 全局 vtime 时钟
void BPF_STRUCT_OPS(simple_enable, struct task_struct *p)
{
p->scx.dsq_vtime = vtime_now;
}
7.2 设计要点
scx_simple 使用 vtime(虚拟时间)实现加权公平调度。每个 task 在 DSQ(调度队列)中的位置由 p->scx.dsq_vtime 决定,vtime 越小越先被调度。task 运行后根据权重消耗 vtime:
c
// stopping hook 中,根据权重消耗 vtime
void BPF_STRUCT_OPS(simple_stopping, struct task_struct *p, bool runnable)
{
if (fifo_sched)
return;
p->scx.dsq_vtime += (SCX_SLICE_DFL - p->scx.slice) * 100 / p->scx.weight;
}
enable 的作用就是为新接管的 task 初始化 vtime 起始位置。 p->scx.dsq_vtime = vtime_now 让新 task 的 vtime 与当前全局时钟对齐------既不会因为 vtime 太小而独占 CPU(饿死其他 task),也不会因为 vtime 太大而被其他 task 饿死。
这是一个值得借鉴的模式:在 enable 中为 task 设置合理的初始调度参数,避免新 task 加入时打破已有的调度公平性。
7.3 如果 enable 中不做初始化会怎样?
p->scx.dsq_vtime 的默认值是 0。如果 simple_enable 不做任何处理,新接管的 task 的 vtime 为 0,远小于已经在运行的 task(vtime 已推进到较大值)。这意味着新 task 会被当作"亏欠最多"的 task,在 enqueue 时获得最高优先级,直到其 vtime 追上其他 task。对于短暂接入的 task 这可能不是问题,但对于批量切换(场景一)中同时接入的大量 task,它们会集体抢占 CPU,造成延迟尖峰。
八、总结
8.1 enable 的三个核心触发场景
| 场景 | 触发条件 | 调用链 |
|---|---|---|
| 注册调度器 | 全量接管模式,已存在的 task | scx_enable_workfn → switching_to_scx → scx_enable_task |
| fork 新 task | scx_init_task_enabled && sched_class == ext_sched_class |
scx_post_fork → scx_enable_task |
| 系统调用 | 将 task 调度类切换到 ext_sched_class |
__sched_setscheduler → switching_to_scx → scx_enable_task |
8.2 enable 的关键约束
- enable 只触发一次:task 从 READY 变为 ENABLED 后,不会再触发 enable
- enable 运行在 rq lock 保护的上下文中:不能做阻塞操作
- enable 之前 scx.weight 已经设置好 :BPF 回调中可以安全读取
p->scx.weight - enable 之后紧跟 set_weight 和(可能)set_cpumask:在设计 BPF 调度器时需要注意这些 hook 之间的协调
8.3 设计建议
- enable 是初始化 task 调度参数的理想位置,如 vtime 起始值、优先级、亲和性等
- enable 之前内核已设置好
p->scx.weight,回调中可以安全使用 - enable 中避免耗时操作,必要的工作可以推迟到 tick 或 dispatch 中完成
参考资料
- Linux 6.18.26 内核源码
kernel/sched/ext.c - Linux 6.18.26 内核源码
kernel/sched/core.c scx_simple调度器源码tools/sched_ext/scx_simple.bpf.c