Sched_ext 回调深度解析(一):sched_ext 框架总览——前言
基于 Linux 6.18.26,结合内核源码逐行分析
系列文章:
- Sched_ext 回调深度解析(一):sched_ext 框架总览——前言
- Sched_ext 回调深度解析(二):init_task —— 每个任务走进调度器的第一道门(6.18.26)
- Sched_ext 回调深度解析(三):enable —— 任务被调度器接管的关键时刻(6.18.26)
- Sched_ext 回调深度解析(四):select_cpu —— 任务唤醒时的选核决策(6.18.26)
- Sched_ext 回调深度解析(五):runnable —— 任务状态转换的哨兵(6.18.26)
- Sched_ext 回调深度解析(六):enqueue —— 任务入队,调度器的核心决策点(6.18.26)
- Sched_ext 回调深度解析(七):dispatch —— 从队列取出任务送到 CPU(6.18.26)
- Sched_ext 回调深度解析(八):running —— 任务开始执行(6.18.26)
1. sched_ext 是什么
sched_ext(简称 scx)是 Linux 内核的 eBPF 调度器框架,自 6.12 起合入主线。它允许开发者用 eBPF 程序在用户态编写自定义 CPU 调度策略,而无需修改内核代码或重启系统。
传统上,添加一个新的 Linux 调度器意味着编写一个完整的sched_class,直接操作内核内部数据结构——这需要内核开发 expertise,修改后必须重新编译、重启才能生效。sched_ext改变了这个局面:内核提供了一个标准的回调接口(struct sched_ext_ops),开发者只需用 BPF 程序实现这些回调,就能定义一个完整的调度策略。调度器程序可以热加载、热卸载,切换策略时系统中的任务会自动平滑迁移。
可以把sched_ext理解为一个"调度器插件系统"——内核在关键调度时机(任务唤醒、入队、出队、开始执行、让出 CPU 等)调用 BPF 回调,BPF 程序在回调中做出调度决策。
1.1 框架总览
从上图可以看出整个框架的运作方式:
- 开发者编写 BPF 调度器程序,实现
struct sched_ext_ops中定义的回调函数。 - 用户态工具将 BPF 程序加载到内核,内核注册这些回调。
- 内核核心调度器在调度事件发生时,调用
ext_sched_class中对应的方法。 ext_sched_class的方法触发 BPF 回调,BPF 程序在回调中做出调度决策(如选择 CPU、入队到哪个 DSQ、从哪个 DSQ 取出任务等)。- **DSQ(Dispatch Queue)**是 BPF 调度器和内核之间的任务交换枢纽——BPF 程序将任务放入 DSQ,内核从 DSQ 中取出任务送到 CPU。
2. 完整回调总览
2.1 回调一览表
struct sched_ext_ops代表一个 BPF 调度器,其中定义了多个回调函数。下表列出了所有核心回调:
| 钩子 | 触发次数 | 含义 |
|---|---|---|
init_task | 每个 task 仅一次 | “登记”——task 被 scx 框架认识 |
enable | 每个 task 仅一次 | “上岗”——task 被 scx 正式接管 |
select_cpu | 每次唤醒时 | “选座”——task 醒来时选择目标 CPU |
runnable | 每次变为可运行时 | “就位”——通知 task 变为 runnable |
enqueue | 每次入队时 | “排队”——task 被放入调度队列 |
dispatch | 每次 CPU 需要任务时 | “叫号”——从 DSQ 取出 task 送到 CPU |
running | 每次调度执行时 | “开工”——task 即将占用 CPU |
stopping | 每次调度结束时 | “收工”——task 让出 CPU |
quiescent | 每次变为不可运行时 | “离场”——通知 task 变为 quiescent |
set_weight | 权重变更时 | 通知 BPF 调度器 task 的权重 |
set_cpumask | CPU 亲和性变更时 | 通知允许运行的 CPU 集合 |
2.2 回调分类
一次性回调(每个 task 仅触发一次):
init_task— 登记,task 被 scx 框架认识enable— 上岗,task 被 scx 正式接管
状态通知回调(每次状态变化时触发):
runnable/quiescent— 可运行 / 不可运行running/stopping— 开始执行 / 停止执行set_weight/set_cpumask— 权重 / 亲和性变更
调度决策回调(核心调度逻辑):
select_cpu— 选核决策,选择目标 CPUenqueue— 入队决策,放入 DSQdispatch— 分发决策,从 DSQ 取出送到 CPU
3. task 生命周期中的回调时序
3.1 一次性回调 vs 重复回调
init_task和enable是"一次性门禁",只在 task 进入 scx 管理时各触发一次;而running和stopping是"旋转门",每次 task 被调度到 CPU 或让出 CPU 时都会触发,构成调度循环。
4. task 的四种状态
每个 task 在 scx 框架中都有一个状态,记录在task->scx.state中:
// include/linux/sched/ext.henumscx_task_state{SCX_TASK_NONE,// ops.init_task() 还没被调用SCX_TASK_INIT,// init_task 执行成功,但任务还没就绪SCX_TASK_READY,// 完全初始化,可以被 scx 调度SCX_TASK_ENABLED,// 已激活,正在被 scx 调度SCX_TASK_NR_STATES,};状态流转图:
| 状态 | 含义 |
|---|---|
SCX_TASK_NONE | 初始状态,还未被 scx 认识 |
SCX_TASK_INIT | 短暂中间态,init_task返回 0 后立即进入 |
SCX_TASK_READY | 完全初始化,可以被 scx 调度 |
SCX_TASK_ENABLED | 激活态,正在被 scx 调度,ops.enable()已调用 |
SCX_TASK_INIT是一个中间态,只在scx_init_task()执行成功后短暂存在,紧接着就会被推进到 READY。
5. ext_sched_class 全貌
sched_ext在内核中注册为ext_sched_class,它是完整的 Linux 调度类(sched_class)。内核核心调度器在调度事件发生时,统一通过sched_class的方法指针来调用 scx 的实现。为方便后续各篇分析时对照,这里给出ext_sched_class的完整定义(kernel/sched/ext.c:3324):
DEFINE_SCHED_CLASS(ext)={.enqueue_task=enqueue_task_scx,.dequeue_task=dequeue_task_scx,.yield_task=yield_task_scx,.yield_to_task=yield_to_scx,.wakeup_preempt=wakeup_preempt_scx,.balance=balance_scx,.pick_task=pick_task_scx,.put_prev_task=put_prev_task_scx,// ← stopping 在这里触发.set_next_task=set_next_task_scx,// ← running 在这里触发.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,#ifdefCONFIG_UCLAMP_TASK.uclamp_enabled=1,#endif};注意两个关键点:
ext_sched_class没有pick_next_task方法,只有pick_task。这意味着在__pick_next_task中,scx 走的是class->pick_task(rq)+put_prev_set_next_task()分支,而不是class->pick_next_task(rq, prev)分支。- running 和 stopping 分别挂接在
set_next_task和put_prev_task上。这两个方法在每次调度切换时成对调用:先put_prev_task(旧任务 stopping),再set_next_task(新任务 running)。
6. 小结与系列导航
本文从宏观角度描绘了sched_ext框架的全貌:
- 架构层面:sched_ext 是一个 eBPF 驱动的调度器插件系统,用户态 BPF 程序通过实现
struct sched_ext_ops中的回调来定义调度策略,内核在关键调度时机调用这些回调。 - 回调体系:11 个核心回调分为三类——一次性回调(
init_task、enable)、状态通知回调(runnable/quiescent、running/stopping等)和调度决策回调(select_cpu、enqueue、dispatch)。 - 状态机:task 在 scx 框架中经历
NONE → INIT → READY → ENABLED四个状态,其中INIT是短暂中间态,ENABLED是正常运行态。 - 调度类集成:
ext_sched_class作为标准的 Linuxsched_class注册到内核,核心调度器通过统一接口调用 scx 的实现。
理解了框架全貌之后,后续文章将逐一深入每个回调的内核实现细节。建议按以下顺序阅读:
- init_task—— 每个 task 走进调度器的第一道门,理解 task 如何被 scx 框架"登记"
- enable—— task 被 scx 正式接管的关键时刻,理解状态从 READY 到 ENABLED 的跃迁
- select_cpu—— 任务唤醒时的选核决策,理解 BPF 调度器如何影响 CPU 选择
- runnable—— 任务状态转换的哨兵,理解 runnable/quiescent 的对称设计
- enqueue—— 任务入队,理解 DSQ 机制和调度决策的核心逻辑
- dispatch—— 从 DSQ 取出任务送到 CPU,理解分发机制的完整流程
- running—— 任务开始执行,理解 running/stopping 的成对设计
