Sched_ext 回调深度解析(二):enable —— 任务被调度器接管的关键时刻(6.18.26)

基于 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 管理,后续的 enqueuedispatchrunningstopping 等 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 回调

  1. 设置 p->scx.weight,然后调用 enable hook(此时 BPF 回调中可安全读取权重)
  2. 将 task 状态设为 SCX_TASK_ENABLED
  3. 调用 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_cpumask hook。


五、通过系统调执行enable(场景3)

5.1 调用链

用户态程序通过 sched_setschedulersched_setparamsched_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_workfnswitching_to_scxscx_enable_task
fork 新 task scx_init_task_enabled && sched_class == ext_sched_class scx_post_forkscx_enable_task
系统调用 将 task 调度类切换到 ext_sched_class __sched_setschedulerswitching_to_scxscx_enable_task

8.2 enable 的关键约束

  1. enable 只触发一次:task 从 READY 变为 ENABLED 后,不会再触发 enable
  2. enable 运行在 rq lock 保护的上下文中:不能做阻塞操作
  3. enable 之前 scx.weight 已经设置好 :BPF 回调中可以安全读取 p->scx.weight
  4. 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
相关推荐
geshifei4 天前
Sched_ext 回调深度解析(一):init_task —— 每个任务走进调度器的第一道门(6.18.26)
linux·ebpf
mounter62510 天前
比 veth 更强、为 eBPF 而生:深度解析 Linux netkit 虚拟网卡驱动
linux·ebpf·kernel·netkit
张璐月1 个月前
[eCapture] OpenSSL 文件 Hook 机制
网络·ebpf·ecapture
key_3_feng1 个月前
生成式AI+eBPF:智能运维新范式的技术实现与深度解析
aigc·ebpf
key_3_feng1 个月前
eBPF驱动的企业可观测性革命:从内核层重构运维新范式
ebpf
mounter6251 个月前
深度拦截:Linux 内核引入 Firmware LSM 挂钩,eBPF 再下一城!
linux·服务器·ebpf·kernel·firmware
张璐月1 个月前
[ecapture] gotls:三种模式实现说明与上层应用职责
网络·ebpf·gotls·ecapture
张璐月1 个月前
[ecapture] eBPF hook gotls 收包乱序根因分析
ebpf·gotls·ecapture
张璐月1 个月前
[eCapture] GoTLS Perf 事件有序下发
ebpf·gotls·ecapture