本篇只钉一件事:
CPU 决定「下一个跑谁」时,内核会按固定顺序询问若干调度模块(sched_class)。
这个顺序从哪来?运行时怎么用?sched_ext(BPF 调度)怎样插进来?
行号基于 linux-source-7.0.0(Makefile 为 7.0.12)。
官方源码:
wget https://cdn.kernel.org/pub/linux/kernel/v7.x/linux-7.0.12.tar.xz
tar -xf linux-7.0.12.tar.xz后面会跟源码,但读之前先把「本篇认重要」的几条摆出来(这一节主要围着它们转):
next 是任务(task_struct *),不是调度器。决定它时不是把本 CPU 所有可跑任务摊平打分,而是 按固定顺序问各个调度类(stop → dl → rt → fair → ext → idle)——前面的类有可跑任务就返回,后面的类不再问。SCHED_DATA),不是运行时注册表;地址更小的类更先被问到。SCHED_EXT 才进 BPF。调用入口记住一条链即可:
schedule → __schedule → __pick_next_task(按类询问,交出 next)下面从入口跟起,把结论逐条对上源码。
本 CPU 上正在跑的进程,过一会儿可能要换:睡了、时间片到了、被更高优先级任务打断……
内核最终会进 schedule():找出 下一个要跑的任务,再做上下文切换。
这里的 next 是任务(类型是 struct task_struct *),不是调度器。
当前任务常叫 prev,即将上场的叫 next;各调度类(fair / rt / ext …)只是被依次询问「你这边有没有可跑任务可交出来当 next」。
本文不关心「怎么切寄存器」,只关心调用一路跟到哪里、这个 next 任务是在哪一层被决定的。
一次普通调度,大致经过四层包装,一层比一层更靠近「决定 next」:
schedule()(core.c:6975)schedule() 时从这里进。__schedule_loop()(core.c:6966)__schedule(里面会挑 next、可能切换);need_resched()——若仍标着「该调度」,再挑一轮。__schedule()(core.c:6748)rq、当前任务叫 prev;prev 要睡,先把它从可跑队列里弄下去;next,最后做上下文切换。pick_next_task() → __pick_next_task()(调用点 core.c:6836,实现 core.c:5910)串起来就是:
schedule() // 入口:我要调度了
→ __schedule_loop() // 关抢占,必要时多调几次
→ __schedule() // 处理 prev,准备切换
└─ pick_next_task() // 请给出 next
└─ __pick_next_task() // 真正决定 next(按类询问)下面按调用顺序看。主线用完整语句标出;大约三成旁支用注释说明「它是什么 / 为什么在这」,不必深追。
schedule:入口/* core.c:6975 */
asmlinkage __visible void __sched schedule(void)
{
struct task_struct *tsk = current;
/*
* current:本 CPU 当前正在跑的那个任务(宏,不是普通局部变量)。
* task_struct:描述一个任务/线程的内核对象(pid、状态、sched_class…)。
*/
/* ... 调试断言等,可忽略 ... */
/*
* task_is_running:状态是否还是 TASK_RUNNING(可跑)。
* 若已经不是(例如准备睡眠),先做 workqueue / block 层收尾,
* 再进真正的调度。细节与「谁当下一个任务」无关。
*/
if (!task_is_running(tsk))
sched_submit_work(tsk);
/*
* SM_NONE:sched_mode 之一,表示「普通主动 schedule」。
* 另有 SM_PREEMPT(抢占进来)、SM_IDLE(idle 路径)等,
* 主要影响 __schedule 里对「是否算抢占 / 是否阻塞 prev」的判断。
*/
__schedule_loop(SM_NONE);
sched_update_worker(tsk); /* worker 线程记账,可忽略 */
}__schedule_loop:可能多轮「挑 next」/* core.c:6966 */
static __always_inline void __schedule_loop(int sched_mode)
{
do {
/*
* preempt_disable:禁止内核抢占。
* 调度过程中不能再被别的路径半路打断另开一次 schedule。
*/
preempt_disable();
__schedule(sched_mode); /* 内部:挑 next,必要时 context_switch */
/*
* 开回抢占计数,但「先不要因为 need_resched 立刻再抢进 schedule」:
* 交给下面 while 统一决定要不要再来一轮。
*/
sched_preempt_enable_no_resched();
} while (need_resched());
/*
* need_resched():当前任务(current)是否还挂着 TIF_NEED_RESCHED。
* __schedule 开头会清掉 prev 的该标记;若循环条件仍为真,
* 说明「这一轮结束之后、或结束过程中」又被标了一次「该调度」,
* 必须再进 __schedule 重新挑 next——不是 while 闲的。
*/
}为何已经挑过 next,还可能再进一轮?
__schedule 整段里抢占是关着的;中断仍可能进,但不能在中途再开一次完整 schedule。
常见做法是:中断 / 唤醒路径只打上 TIF_NEED_RESCHED,等 __schedule 返回、本循环看到 need_resched() 再选。典型情况包括:
try_to_wake_up → 若应抢占当前任务,就 resched_curr 置位。__schedule 可能已挑完甚至切走过;回到 loop 时标记还在 → 再挑。scheduler_tick 发现时间片/延迟等需要换人,置 need_resched。__schedule / 刚返回」的窗口,本轮 while 会再进一次。need_resched。prev == next(没人可换),或刚清完标记又在关抢占窗口里被再次置位;sched_preempt_enable_no_resched 不会自动再进 schedule,靠 while 补上,避免漏调度。直观记:
__schedule 挑好 next(甚至切走再切回)
→ 回到 loop
→ need_resched 仍真? → 局面可能已变 → 再 __schedule 重挑
→ 否则才真正离开 schedule()主线仍是「一次 __schedule 里如何按类交出 next」;loop 只是保证 标记不会白打。
__schedule:处理 prev,再请出 next函数从 core.c:6748 起很长。按阶段读:
/* core.c:6748 起,结构示意 + 关键原句 */
static void __schedule(int sched_mode)
{
struct task_struct *prev, *next;
struct rq_flags rf;
struct rq *rq;
int cpu;
/* ... 局部变量:preempt、prev_state 等 ... */
cpu = smp_processor_id(); /* 当前 CPU 编号 */
rq = cpu_rq(cpu); /* 该 CPU 的运行队列 runqueue */
prev = rq->curr; /* 当前正在跑的任务 → 后面叫 prev */
/* ... schedule_debug / hrtick / livepatch 等旁路,可跳过 ... */
local_irq_disable(); /* 关本 CPU 中断,避免选 next 时被打断 */
/* ... RCU / migrate_disable 记账 ... */
rq_lock(rq, &rf); /* 拿住本 rq 的锁;调度核心数据结构要串行改 */
/* ... update_rq_clock:更新 rq 时钟,供 fair 等算时间 ... */
prev_state = READ_ONCE(prev->__state);
/*
* __state:任务状态。TASK_RUNNING=可跑;非 0 常表示睡眠/停止等。
* 主动 schedule 且 prev 已非 RUNNING → 通常要把 prev 移出可跑队列。
*/
if (sched_mode == SM_IDLE) {
/* idle 专用捷径:队列空则继续跑自己,不必 pick */
/* ... */
} else if (!preempt && prev_state) {
try_to_block_task(rq, prev, &prev_state, /* ... */);
/* 把要睡的 prev 从 rq 可跑集合里弄下去(出队) */
}
pick_again:
/*
* 本文主线:请本 CPU 的调度逻辑给出下一个任务。
* 返回值 next:struct task_struct *,即将上场的任务。
* rq->donor:与代理执行相关,初读可当成「从谁的视角挑」;
* 多数路径下等价于从当前 rq 挑。
*/
next = pick_next_task(rq, rq->donor, &rf);
rq_set_donor(rq, next);
rq->next_class = next->sched_class; /* 记下 next 属于哪个调度类 */
/* ... 若 next 被 mutex 挡住,可能 find_proxy_task 再挑;初读可跳过 ... */
picked:
clear_tsk_need_resched(prev); /* 清「该调度」标记 */
/* ... */
if (likely(prev != next)) {
/*
* 真发生切换:rq->curr = next,再 context_switch(prev, next)
* 保存 prev 寄存器 / 恢复 next 寄存器。细节不在本文范围。
*/
/* ... context_switch ... */
}
/* ... 放锁、开中断、可能返回到 next 的执行流 ... */
}要点:__schedule 负责「场面」(锁、阻塞 prev、切换);谁当 next 交给 pick_next_task。
pick_next_task:多数配置下只是转一手/* core.c:6465(无 CONFIG_SCHED_CORE 时) */
static struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
/*
* 有 SCHED_CORE(SMT 同核协同)时,这里会更复杂,见第 2 步旁支。
* 未开时就是直接进 __pick_next_task。
*/
return __pick_next_task(rq, prev, rf);
}__pick_next_task(core.c:5910)才是「按调度类挨个问」的地方,第 2 步展开。
到这里记住:
schedule→__schedule→__pick_next_task:next(下一个任务)在最后一层被决定。
容易以为:本 CPU 上所有可跑任务排成一张大表,__pick_next_task 从头扫到尾,挑一个「最该跑」的。
实际不是这样。 任务按策略分在不同调度类里(rt 管实时、fair 管普通进程、ext 管 BPF……),各自有自己的队列和算法。__pick_next_task 做的是:按固定顺序问各个调度类——「你这类有没有可跑任务?」——而不是把全机任务摊平扫一遍。
问 stop → 有可跑的?有就返回;没有再问
问 dl → 有可跑的?有就返回;没有再问
问 rt → …
问 fair → …
问 ext → …
问 idle → 总能交出 idle(兜底)例如有 SCHED_FIFO 可跑时,根本不会去 fair 里挑普通进程。
stop_task.c:99 | ||
SCHED_DEADLINE | deadline.c:3427 | |
SCHED_FIFOSCHED_RR | rt.c:2590 | |
fair.c:13953 | ||
ext.c:3521 | ||
idle.c:569 |
全文主线因此变成:
先问哪一个调度类?这个顺序从哪来?
下一步打开 __pick_next_task,看它怎么按类循环。
__pick_next_task 里看见「按类循环」上一层只负责请出 next;真正决定「哪个任务」在这里(core.c:5910)。
读法同第 1 步:主线看语句,旁支看注释。
/* core.c:5909–5965 */
/*
* Pick up the highest-prio task:
* 在本 CPU 的 rq 上,挑出优先级最高的那一类里的下一个任务。
* 「类间谁更高」= 后面 for_each_active_class 的遍历顺序(第 3–4 步)。
*/
static inlinestruct task_struct *
__pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
__must_hold(__rq_lockp(rq)) /* 调用方必须已持有 rq 锁 */
{
conststruct sched_class *class; /* 当前问到的调度类 */
struct task_struct *p; /* 某一类交出来的候选任务 */
rq->dl_server = NULL; /* deadline server 相关,初读可忽略 */
/*
* scx_enabled():是否已加载并启用了 BPF 调度器(sched_ext)。
* 为真时关掉下面的 fair 捷径,一律走 restart 按类扫描。
* 原因:BPF 可能接管了大量原 fair 任务,捷径假设不再成立。
*/
if (scx_enabled())
goto restart;
/*
* ========== fair 捷径(优化路径,不是主线语义)==========
* 若本 rq 上可跑任务「几乎全是 fair」,且 prev 也不比 fair 更高类,
* 就直接调 fair,免去 for 循环问 stop/dl/rt/…。
*
* sched_class_above(a, b):a 是否比 b「更高类」(地址更小更优先)。
* nr_running:本 rq 可跑任务总数。
* cfs.h_nr_queued:fair/cfs 侧排队任务数。
* 两者相等 ≈ 没有 rt/dl 等其它类的可跑任务。
*/
if (likely(!sched_class_above(prev->sched_class, &fair_sched_class) &&
rq->nr_running == rq->cfs.h_nr_queued)) {
p = pick_next_task_fair(rq, prev, rf);
/*
* RETRY_TASK:特殊哨兵值,表示「现在定不了,请重来」
* (例如 balance 后队列变了)。不是真正的 task_struct。
*/
if (unlikely(p == RETRY_TASK))
goto restart;
/*
* fair 也没人可跑 → 通常只剩 idle。
* put_prev_set_next_task:从 prev 切到 p 时,通知两类
* 「你不再是 curr / 你是 curr」的成对回调,初读知职责即可。
*/
if (!p) {
p = pick_task_idle(rq, rf);
put_prev_set_next_task(rq, prev, p);
}
return p; /* 捷径命中:next 已定,函数返回 */
}
restart:
/*
* ========== 慢路径 / 主线:按调度类挨个问 ==========
* prev_balance:挑 next 前,必要时做负载均衡(拉任务过来等)。
* 与「类间顺序」无关,失败/变化可能配合 RETRY_TASK 重来。
*/
prev_balance(rq, prev, rf);
/*
* for_each_active_class:从最高类扫到最低类(宏,第 3 步展开)。
* 顺序大致:stop → dl → rt → fair → ext → idle。
* 「前面的类只要交出非空任务,后面的类不会被问到」。
*/
for_each_active_class(class) {
if (class->pick_next_task) {
/*
* 目前主要是 fair 填了 .pick_next_task。
* 接口更「完整」:知道 prev,便于 fair 做特殊优化。
*/
p = class->pick_next_task(rq, prev, rf);
if (unlikely(p == RETRY_TASK))
goto restart;
if (p)
return p; /* 该类有可跑任务 → 就是 next */
} else {
/*
* stop / dl / rt / ext / idle 等多走 .pick_task:
* 「你这类里挑一个可跑的出来;没有就返回 NULL」。
*/
p = class->pick_task(rq, rf);
if (unlikely(p == RETRY_TASK))
goto restart;
if (p) {
put_prev_set_next_task(rq, prev, p);
return p; /* 同上:有货就返回,不再问后面 */
}
}
/* p == NULL:该类没人,循环继续问下一类 */
}
/*
* 理论上走不到:idle 类在链接顺序最后,总能交出 per-CPU idle 任务。
* 若仍为空,说明调度类链接/实现坏了。
*/
BUG();
}这一步只要带走三句话:
for_each_active_class:按类顺序问,先交出任务的类赢得 next。CONFIG_SCHED_CORE 是什么(旁支)Kconfig:kernel/Kconfig.preempt:151,全称 Core Scheduling for SMT。
prctl(PR_SCHED_CORE) 把任务划进同一 core group;选 next 时尽量让 siblings 跑同组任务,对不上就让一侧 idle。SCHED_CORE,类间顺序仍是那一套。对调用的影响:
pick_next_task__schedule 调用的那个) | |
|---|---|
SCHED_CORE | core.c:6465return __pick_next_task(...) |
SCHED_CORE | core.c:6011 |
开了之后,同文件还有一份更简单的按类扫描,只走 .pick_task(给 core 路径用):
/* core.c:5990–6003 */
static inline struct task_struct *pick_task(struct rq *rq, struct rq_flags *rf)
{
conststruct sched_class *class;
struct task_struct *p;
rq->dl_server = NULL;
for_each_active_class(class) {
p = class->pick_task(rq, rf);
if (p)
return p;
}
BUG(); /* The idle class should always have a runnable task. */
}初读知「同样按类问」即可;SMT cookie / forced-idle 细节可后看。
下一步:这个 for 的头尾指针、以及「下一类」怎么跳,在 sched.h 里。
for_each_active_class,看见头尾指针第 2 步的 for_each_active_class(class) 不是普通 class++。
它从链接排出的「最高类」走到「最低类」,每一步用 next_active_class 前进——多数时候加一格,有 scx 时可能再跳过一格空类。
/* sched.h:2714–2716:头尾是链接脚本里的地址标号(第 4 步) */
externstruct sched_class __sched_class_highest[];
externstruct sched_class __sched_class_lowest[];
/* sched.h:2746–2750 */
#define for_active_class_range(class, _from, _to) \
for (class = (_from); class != (_to); class = next_active_class(class))
#define for_each_active_class(class) \
for_active_class_range(class, __sched_class_highest, __sched_class_lowest)
/*
* 展开后等价于:
* for (class = 最高类;
* class != 最低类标号; // 不含终点
* class = next_active_class(class))
* 注意:这里用 != 和 next_active_class,不是简单的 class++。
* (另有 for_each_class 用 class++ 扫「全部类」,含未激活的;选 next 用 active 版。)
*/
/* sched.h:2752 */
#define sched_class_above(_a, _b) ((_a) < (_b))
/* 地址更小 → 类更高 → 更先被 for 问到 */next_active_class:重点看 scx 那两段内存里类对象大致排成一排(第 4 步钉死):
stop → dl → rt → fair → ext → idleclass++ 就是顺着这排往后移一格。有 CONFIG_SCHED_CLASS_EXT 时,移完还可能再移一格,用来跳过当前没必要问的类:
/* sched.h:2724–2737 */
/*
* Iterate only active classes. SCX can take over all fair tasks or be
* completely disabled. If the former, skip fair. If the latter, skip SCX.
*
* 官方注释已经点明:全开 scx → 跳过 fair;没启用 scx → 跳过 ext。
*/
static inline const struct sched_class *
next_active_class(const struct sched_class *class)
{
class++; /* 先正常走到「下一格」 */
#ifdef CONFIG_SCHED_CLASS_EXT
/*
* ---------- 情况 A:跳过 fair ----------
* scx_switched_all():BPF 已「全量接管」——原来挂在 fair 上的
* 普通任务大都迁到 ext 了(开关何时置位见第 7 步)。
*
* 此时 fair 队列几乎没人,每次 schedule 仍问 fair 是浪费。
* 若本次 class++ 刚好落到 &fair_sched_class,再 ++ 一次,直接去 ext。
*
* 注意比较时机:是「++ 之后」的指针和 &fair 比。
* 例如当前停在 rt,next 时:
* class++ → fair → 条件成立再 ++ → ext
* 于是扫描序列变成:… → rt →(跳过 fair)→ ext → …
*
* 链接顺序仍是 fair 在 ext 前;这里只是迭代时少问空类,
* 并没有把 ext「提升」到 fair 上面。
*/
if (scx_switched_all() && class == &fair_sched_class)
class++;
/*
* ---------- 情况 B:跳过 ext ----------
* scx_enabled():是否已加载并启用某个 BPF 调度器。
*
* 内核可能编译了 ext_sched_class(开了 CONFIG_SCHED_CLASS_EXT),
* 但运行时还没 load 任何 BPF。这时问 ext 没有意义。
* 若 class++ 落到 &ext_sched_class,再 ++,直接去 idle。
*
* 例如当前停在 fair,且没启用 scx:
* class++ → ext → 条件成立再 ++ → idle
* 扫描序列:… → fair →(跳过 ext)→ idle
*/
if (!scx_enabled() && class == &ext_sched_class)
class++;
#endif
return class;
}两个开关本身(sched.h:1830–1834):
DECLARE_STATIC_KEY_FALSE(__scx_enabled); /* 已 load BPF 调度器 */
DECLARE_STATIC_KEY_FALSE(__scx_switched_all); /* fair 类任务已迁到 SCX */
#define scx_enabled() static_branch_unlikely(&__scx_enabled)
#define scx_switched_all() static_branch_unlikely(&__scx_switched_all)
/*
* static_branch:运行时可开关的「极便宜」分支(内核常用技巧)。
* 默认都是 false:没 load BPF 时,上面两段 if 都不进,等价于纯 class++。
*/对照三种常见扫描结果(链接顺序不变,变的是「跳谁」):
没 load BPF: stop → dl → rt → fair → (跳过 ext) → idle
BPF + 部分接管: stop → dl → rt → fair → ext → idle // 两段 if 都不跳
BPF 全开: stop → dl → rt → (跳过 fair) → ext → idle没开 CONFIG_SCHED_CLASS_EXT 时:没有 ext 对象,也没有这两段 if;scx_enabled() 等编译成恒 false。
for_each_active_class = 从 __sched_class_highest 走到 lowest,步进函数是 next_active_class。next_active_class 先 class++;有 scx 配置时,可能再跳过 空的 fair 或 未启用的 ext。运行时「怎么扫」说到这。还缺的几块,后面按这个顺序补:
第 4 步 这一排 stop→…→idle 谁写死的? → 链接脚本 SCHED_DATA
第 5 步 每个 sched_class 对象从哪来? → DEFINE_SCHED_CLASS + 编译
第 6 步 ext 何时出现在这一排里? → CONFIG_SCHED_CLASS_EXT
第 7 步 第 3 步两个开关谁、何时置位? → 加载 BPF / PARTIAL
第 8 步 任务怎么挂到某一类上? → policy → __setscheduler_class第 3 步里 class++ 能成立,是因为各 sched_class 在内存里已经排成连续一排。__sched_class_highest / lowest不是 C 里手写的数组,而是链接脚本里的地址标号。
/* include/asm-generic/vmlinux.lds.h:153–162 */
#define SCHED_DATA \
STRUCT_ALIGN(); \
__sched_class_highest = .; \
*(__stop_sched_class) \
*(__dl_sched_class) \
*(__rt_sched_class) \
*(__fair_sched_class) \
*(__ext_sched_class) \
*(__idle_sched_class) \
__sched_class_lowest = .;
/*
* 链接器按上面顺序,把名为 __xxx_sched_class 的 section
* 拼进 vmlinux。拼完后:
* highest 标在第一格之前,lowest 标在最后一格之后。
* for 从 highest 扫到 lowest,class++ 就是沿这排往后走。
*/因此固定顺序是:
低地址 ← 先被 for 问到
stop → dl → rt → fair → ext → idle
高地址 ← 最后才问到「谁更高」用指针比大小(第 3 步已出现):
/* sched.h:2752 */
#define sched_class_above(_a, _b) ((_a) < (_b)) /* 地址更小更优先 */钉死:fair 在 ext 前面 → fair 先于 ext 被询问。
第 3 步的「跳过」只是少问空格,不能把 ext 插到 fair 前面。
下一问:链接脚本收集的是 section 名;那每个 xxx_sched_class 对象是谁放进这些 section 的?
答:各调度实现文件里用宏定义全局表,编译进具名 section,再被第 4 步的 SCHED_DATA 收进去。
没有运行时 register_sched_class()。
/* sched.h:2709–2712 */
#define DEFINE_SCHED_CLASS(name) \
const struct sched_class name##_sched_class \
__aligned(__alignof__(struct sched_class)) \
__section("__" #name "_sched_class")
/*
* 例:DEFINE_SCHED_CLASS(fair) → 符号 fair_sched_class,
* section 名 "__fair_sched_class",正好对上 SCHED_DATA 里的
* *(__fair_sched_class)。
*/三类示例(注意 fair 有 .pick_next_task,ext 通常只有 .pick_task——对应第 2 步分支):
/* stop_task.c:99 起 */
DEFINE_SCHED_CLASS(stop) = {
.pick_task = pick_task_stop,
/* ... enqueue/dequeue/... */
};
/* fair.c:13953 起 */
DEFINE_SCHED_CLASS(fair) = {
.pick_task = pick_task_fair,
.pick_next_task = pick_next_task_fair, /* 多数类没有 */
/* ... */
};
/* ext.c:3521 起(需编进内核,见第 6 步) */
DEFINE_SCHED_CLASS(ext) = {
.pick_task = pick_task_scx,
/* 没有 .pick_next_task */
/* ... */
};stop_task.c:99 | stop_sched_class | __stop_sched_class |
deadline.c:3427 | dl_sched_class | __dl_sched_class |
rt.c:2590 | rt_sched_class | __rt_sched_class |
fair.c:13953 | fair_sched_class | __fair_sched_class |
ext.c:3521 | ext_sched_class | __ext_sched_class |
idle.c:569 | idle_sched_class | __idle_sched_class |
这些 .c 怎么进镜像(仍不是运行时注册):
# kernel/sched/Makefile:39–42
obj-y += core.o fair.o build_policy.o build_utility.o/* build_utility.c:84 */
#include "stop_task.c"
/* build_policy.c:50–64 */
#include "idle.c"
#include "rt.c"
#include "deadline.c"
#ifdef CONFIG_SCHED_CLASS_EXT
# include "ext.c" /* 没开配置就没有 ext 对象 → 第 6 步 */
#endif串起来:
DEFINE_SCHED_CLASS → 各自 section → SCHED_DATA 排序 → for_each_active_class 挨个问第 5 步末尾的 #ifdef 说明:ext 不是永远在镜像里。
# kernel/Kconfig.preempt:169
config SCHED_CLASS_EXT
bool "Extensible Scheduling Class"
depends on BPF_SYSCALL && BPF_JIT && DEBUG_INFO_BTF
/* 打开后才有 sched_ext 框架,允许 BPF 实现调度策略 */build_policy.c 编入 ext.c → 链接段里有 ext_sched_class(夹在 fair 与 idle 之间)。scx_enabled() / scx_switched_all() 在头文件里变成恒 false(sched.h:1851),第 3 步那两段额外 class++ 也不编译。/* sched.h:1851–1853 */
#else /* !CONFIG_SCHED_CLASS_EXT */
#define scx_enabled() false
#define scx_switched_all() false注意:编进内核 ≠ 正在用 BPF。只表示框架在;scx_enabled 要等加载 BPF 后才为真(第 7 步)。
开机时 sched_init 用 BUG_ON 验收第 4 步的链接顺序没排反(core.c:8581):
BUG_ON(!sched_class_above(&stop_sched_class, &dl_sched_class));
BUG_ON(!sched_class_above(&dl_sched_class, &rt_sched_class));
BUG_ON(!sched_class_above(&rt_sched_class, &fair_sched_class));
BUG_ON(!sched_class_above(&fair_sched_class, &idle_sched_class));
#ifdef CONFIG_SCHED_CLASS_EXT
BUG_ON(!sched_class_above(&fair_sched_class, &ext_sched_class));
BUG_ON(!sched_class_above(&ext_sched_class, &idle_sched_class));
#endif再次确认:fair < ext(地址)→ fair 先问。
到这里,「类排成一排、for 怎么走、ext 在不在镜像」都齐了。
第 3 步里两个运行时开关,还没说谁在何时打开——那是加载 BPF 的事。
第 3 步已经说明:扫描时可能跳过 fair / ext。
这里补完整故事——很多读者没用过 sched_ext,先把「谁在用户态加载、用了哪些 API」说清楚,再落到内核里如何点亮那两个 static key。
内核开了 CONFIG_SCHED_CLASS_EXT 之后,多了一个 ext 调度类(空壳框架)。
真正的策略写在 BPF 程序里:实现一张回调表 struct sched_ext_ops(选 CPU、入队、dispatch……),加载进内核后,ext 类挑任务时就会调这些回调。
可以把它想成:
用户写一份「调度策略」.bpf.c
→ 用户态程序用 libbpf 加载并 attach
→ 内核启用 scx:打开 scx_enabled(以及可能的 switched_all)
→ 之后 schedule 按第 3 步的规则扫类;挂在 ext 上的任务由 BPF 策略安排这和「写一个内核模块改 fair.c」不同:策略在 BPF 里,可热加载 / 卸载,不必重编内核。
公开示例在内核树(不依赖任何内部仓库):
tools/sched_ext/scx_simple.c + scx_simple.bpf.cmake -C tools/sched_ext,跑 tools/sched_ext/build/bin/scx_simple用户态用 libbpf(加载 BPF、创建 map、attach)。sched_ext 示例再包一层宏,但底下就是 libbpf。
典型三步(摘自 tools/sched_ext/scx_simple.c):
/* 1) 打开 skeleton(bpftool 生成的 *.skel.h,少手写 fd) */
skel = SCX_OPS_OPEN(simple_ops, scx_simple);
/* 2) 加载进内核:验证器检查 BPF 字节码 */
SCX_OPS_LOAD(skel, simple_ops, scx_simple, uei);
/* 3) attach struct_ops → 内核走 scx enable,点亮开关 */
link = SCX_OPS_ATTACH(skel, simple_ops, scx_simple);
/* 进程在前台活着,调度器就保持启用;退出时: */
bpf_link__destroy(link); /* libbpf:拆掉 link,卸载该 BPF 调度器 */
scx_simple__destroy(skel);若不用 scx 宏,对应的 libbpf 概念是:
bpf_object__open* / skeleton open
→ bpf_object__load(或 skel load)
→ bpf_map__attach_struct_ops / bpf_link 挂上 sched_ext_ops
→ bpf_link__destroy 卸载libbpf | libbpf-dev 等) | .bpf.o、map、link |
*.bpf.skel.h | bpftool gen skeleton | |
SCX_OPS_OPEN/LOAD/ATTACH | tools/sched_ext 辅助宏 | |
bpf_link__destroy |
BPF 侧把回调填进 sched_ext_ops(tools/sched_ext/scx_simple.bpf.c):
SCX_OPS_DEFINE(simple_ops,
.select_cpu = (void *)simple_select_cpu,
.enqueue = (void *)simple_enqueue,
.dispatch = (void *)simple_dispatch,
/* ... */
.name = "simple");内核结构体:struct sched_ext_ops(ext_internal.h:272)——函数指针表 + flags + name。
ops->flags/* ext_internal.h:137–140 */
/*
* If set, only tasks with policy set to SCHED_EXT are attached.
* If clear, SCHED_NORMAL tasks are also included.
*/
SCX_OPS_SWITCH_PARTIAL = 1LLU << 3,SCX_OPS_SWITCH_PARTIAL | ||
|---|---|---|
SCHED_NORMAL 等也会被迁过去 | 只有policy == SCHED_EXT | |
scx_switched_all | ||
scx_simple) |
用户态在 ATTACH 之前改 flags 即可,例如(tools/sched_ext/scx_qmap.c 的 -p):
skel->struct_ops.qmap_ops->flags |= SCX_OPS_SWITCH_PARTIAL;PARTIAL 下还要把进程改成 SCHED_EXT(标准 POSIX / Linux API):
struct sched_param param = { .sched_priority = 0 };
sched_setscheduler(pid, SCHED_EXT, ¶m);
/* 或 sched_setattr();也可用 syscall(__NR_sched_setscheduler, ...) */含义:改 task_struct 的 policy → 第 8 步 __setscheduler_class / task_should_scx 才会把任务挂到 ext_sched_class。
SCX_OPS_ATTACH 成功后,内核进入 enable(ext.c:5257 一带)。与本文相关的几句:
/* 根据 BPF 是否带 PARTIAL,记下「要不要全开」 */
WRITE_ONCE(scx_switching_all, !(ops->flags & SCX_OPS_SWITCH_PARTIAL));
static_branch_enable(&__scx_enabled);
/* → 此后 scx_enabled() == true:第 3 步不再跳过 ext */
/* ... 遍历任务,把合格的迁到 ext 类(换 p->sched_class)... */
if (!(ops->flags & SCX_OPS_SWITCH_PARTIAL))
static_branch_enable(&__scx_switched_all);
/* → 全开时 scx_switched_all() == true:第 3 步跳过 fair */对应关系(和第 3 步对上):
「某个 policy 该不该进 scx」:
/* ext.c:3852 */
bool task_should_scx(int policy)
{
if (!scx_enabled() || /* 正在关闭 */ ...)
return false;
if (READ_ONCE(scx_switching_all))
return true; /* 全开:普通策略也进 */
return policy == SCHED_EXT; /* PARTIAL:仅 SCHED_EXT */
}不必读内核变量,看 sysfs(实现见 ext.c:3700 一带):
# 是否启用 / 状态字符串
cat /sys/kernel/sched_ext/state
# 1 ≈ 全开(switching_all);0 ≈ PARTIAL 或未全开
cat /sys/kernel/sched_ext/switch_all
# 当前 BPF 调度器名字(ops.name,如 "simple")
cat /sys/kernel/sched_ext/root/ops
# 某进程是否在 ext 上
grep ext.enabled /proc/<pid>/schedtools/sched_ext/scx_simple):OPEN → LOAD → ATTACH;ATTACH 触发内核 enable。scx_enabled 几乎总开;scx_switched_all 仅全开时开。SCX_OPS_SWITCH_PARTIAL;进程侧再 sched_setscheduler(..., SCHED_EXT, ...)。到此为止讲的都是:问类的顺序(以及何时跳过空类)。
另一半:某个进程的 p->sched_class 指向哪一类,由 policy 等决定。
只有任务挂在某类的队列上,该类被问到时才可能交出 next。
用户可见 policy:
/* include/uapi/linux/sched.h:114–121 */
#define SCHED_NORMAL 0
#define SCHED_FIFO 1
#define SCHED_RR 2
#define SCHED_BATCH 3
#define SCHED_IDLE 5
#define SCHED_DEADLINE 6
#define SCHED_EXT 7policy → 调度类(接上第 7 步的 task_should_scx):
/* core.c:7231–7244 */
conststruct sched_class *__setscheduler_class(int policy, int prio)
{
if (dl_prio(prio))
return &dl_sched_class;
if (rt_prio(prio))
return &rt_sched_class;
#ifdef CONFIG_SCHED_CLASS_EXT
if (task_should_scx(policy))
return &ext_sched_class;
#endif
return &fair_sched_class;
}读法:
task_should_scx(policy) → ext(全开时 NORMAL 也会;PARTIAL 时需 SCHED_EXT)。PARTIAL 下想让某进程走 BPF:先加载带 SCX_OPS_SWITCH_PARTIAL 的调度器,再用 sched_setscheduler(pid, SCHED_EXT, …) / sched_setattr() 改 policy(见第 7.3 节)。
cat /sys/kernel/sched_ext/state
cat /sys/kernel/sched_ext/switch_all
cat /sys/kernel/sched_ext/root/ops
grep ext.enabled /proc/<pid>/sched两条线合在一起:
链接顺序决定「先问谁」;
policy(+ scx 是否全开)决定「任务站在哪一类的队列里」。
BPF 接管 = 换任务所属 class + 扫描时跳过空类,不是改链接顺序。整条逻辑链:
schedule → __pick_next_task
→ for_each_active_class(可跳过空 fair / 未启用 ext)
→ 顺序来自 SCHED_DATA(fair 在 ext 前)
→ 对象来自 DEFINE_SCHED_CLASS(ext 受 Kconfig 约束)
→ 跳过开关在加载 BPF 时置位(PARTIAL 与否)
→ 任务进哪一类看 policy(+ task_should_scx)__pick_next_task(core.c:5910)按类询问 next。SCHED_DATA(vmlinux.lds.h:153):stop → dl → rt → fair → ext → idle。DEFINE_SCHED_CLASS(sched.h:2709)进 section,无运行时注册表。sched.h:2728),不是把 ext 排到 fair 前。ops->flags(ext_internal.h:140,启用时 ext.c:5261 / 5298)决定。类间谁先被问到,看链接顺序;任务进哪一类,看 policy(以及 scx 是否全开)。