Linux 6.12 引入的 sched_ext(Scheduler Extensibility)是内核调度架构的一次范式突破。它允许用户态通过 BPF 程序定义完整的调度策略,动态加载、替换甚至停用默认的 CFS/EEVDF 调度器,无需重新编译内核,无需修改一行内核代码。本文从调度器框架演进出发,深入剖析 sched_ext 的核心机制——SCX_OPS 生命周期、CPU Dispatch 队列、DSQ(Dispatch Task Queue)优先级调度、_timer 机制与 watchdog——并通过一个完整的生产级 BPF 调度器源码(scx_example_scheduler)演示如何从零搭建。最后给出 Meta、Google 在数据中心场景的部署数据,以及与 CFS、sched_setattr、cgroup 的交互陷阱。

1. 为什么需要 BPF 调度器?

Linux CFS(Completely Fair Scheduler)自 2.6.23(2007 年)以来统治了主流 Linux 的 CPU 调度格局。其红黑树虚拟时间(vruntime)模型在通用场景下表现优异,但面对以下需求时无能为力:

  • 异构任务优先级:AI 训练集群中的 GPU 计算任务与 CPU 预处理任务的协同调度,需要在亚毫秒级抢占
  • NUMA 感知亲和:单节点 192 核的 ARM 服务器上,跨 die 内存访问延迟差异达 30%,CFS 的 load_balance 无法快速响应
  • 实时性保证:5G UPF(User Plane Function)数据面需要确定性延迟 <50μs,CFS 无法满足
  • 能耗调度:移动设备上需要根据电池状态动态调整迁移阈值

传统的解决方案是修改内核代码、提交 patch、等待合并、升级内核——周期长达 6-18 个月。sched_ext 将这个闭环缩短到几分钟:写一个 BPF 程序,scx_loader 加载,立刻生效。

2. Sched-Ext 架构总览

Sched-Ext 的设计哲学受到 eBPF 成功的启发:提供一个安全的沙箱执行环境,让调度策略在用户态以 BPF 字节码形式运行,内核负责执行 BPF hook 并提供基础设施。

核心组件关系如下:

┌──────────────────────────────────────────────────┐
│                 用户态                            │
│  scx_loader    ←── BPF .o 文件                   │
│  scx_rustisco  ←── 用 Rust 编写的调度器          │
│  scx_layerwise←── 分组分层调度策略                │
└─────────────┬────────────────────────────────────┘
              │ bpf(BPF_PROG_LOAD) + sys_sched_ext
              ▼
┌──────────────────────────────────────────────────┐
│              内核空间 sched_ext core              │
│  ┌─────────┐  ┌──────────┐  ┌───────────────┐   │
│  │SCX_OPS  │  │ p->scx   │  │ scx_bpf API   │   │
│  │回调注册 │  │ 每任务   │  │ 辅助函数      │   │
│  └────┬────┘  └────┬─────┘  └───────┬───────┘   │
│       │            │                │            │
│       ▼            ▼                ▼            │
│  ┌─────────────────────────────────────────┐     │
│  │          全局 DSQ + 每 CPU local DSQ     │     │
│  │    (scx_bpf_dispatch / consume)         │     │
│  └─────────────────────────────────────────┘     │
└──────────────────────────────────────────────────┘

2.1 SCX_OPS 生命周期

每个 Sched-Ext 调度器必须注册一组 SCX_OPS 回调:

// include/linux/sched/ext.h
struct scx_ops {
    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 (*running)(struct task_struct *p);
    void (*stopping)(struct task_struct *p, bool runnable);
    void (*enable)(struct task_struct *p);
    void (*disable)(struct task_struct *p);
    void (*tick)(struct task_struct *p);
    bool (*yield)(struct task_struct *p);
    bool (*preempt)(struct task_struct *p);
    // ... 20+ 个可选 ops
};

内核在以下时刻触发这些回调:

  • enqueue:任务变为 runnable 状态(wakeup、fork、迁移)
  • dequeue:任务主动放弃 CPU 或 sleep
  • dispatch:CPU 空闲或需要选择下一个任务时
  • running / stopping:任务开始/停止执行
  • tick:调度器 tick(通常为 1ms 可配置)

2.2 DSQ:Dispatch Task Queue

DSQ 是 Sched-Ext 的核心数据结构,分为两类:

类型 生命周期 特点
SCX_DSQ_GLOBAL 调度器全局 FIFO,无优先级
SCX_DSQ_LOCAL 每 CPU 高优先级于全局
用户自定义 DSQ 用户创建 可自由组合优先级

任务入队时通过 scx_bpf_dispatch(p, dsq_id, slice, enq_flags) 选择目标 DSQ 并指定时间片长度。这给了调度器极大的灵活性:你可以为不同任务类型创建不同 DSQ,同时为交互式任务保留较小的时间片来更频繁地重新调度。

3. 编写一个 BPF 调度器

以下是一个简化但有教学意义的 BPF 调度器——scx_lodestar,实现了"交互式优先 + CFS-like 公平"的两级调度策略:

// scx_lodestar.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

#define TASK_INTERACTIVE_WEIGHT  100
#define TASK_BATCH_WEIGHT        1

char _license[] SEC("license") = "GPL";

// 自定义 DSQ 分配
#define DSQ_INTERACTIVE    0
#define DSQ_BACKGROUND     1
#define NR_DSQ_TYPES       2

// 判断任务是否为"交互型"——基于最近运行时间估计
static inline bool is_interactive(struct task_struct *p) {
    u64 avg_slice = p->se.sum_exec_runtime / (p->se.nr_migrations + 1);
    // 平均运行时间 < 5ms 视为交互型
    return avg_slice < 5000000ULL; // ns
}

SEC("tp_btf/sched_wakeup")
int BPF_PROG(handle_wakeup, struct task_struct *p) {
    u64 now = bpf_ktime_get_ns();
    u64 slice = is_interactive(p) ? 4000000 : 2000000; // 4ms / 2ms
    u64 dsq_id = is_interactive(p) ? DSQ_INTERACTIVE : DSQ_BACKGROUND;

    scx_bpf_dispatch(p, dsq_id, slice, SCX_ENQ_WAKEUP);
    return 0;
}

SEC("tp_btf/sched_wakeup_new")
int BPF_PROG(handle_wakeup_new, struct task_struct *p) {
    // 新任务默认放到 background DSQ
    scx_bpf_dispatch(p, DSQ_BACKGROUND, 2000000, SCX_ENQ_WAKEUP);
    return 0;
}

SEC("tp_btf/sched_switch")
int BPF_PROG(handle_switch, bool preempt,
             struct task_struct *prev, struct task_struct *next) {
    // 记录运行时间,后续 tick 中判断交互性
    u64 now = bpf_ktime_get_ns();
    u64 delta = now - prev->scx.dsq_stamp;
    bpf_printk("scx_lodestar: prev=%s ran %llu ns\n", prev->comm, delta);
    return 0;
}

3.1 编译与加载

# 需要 clang 17+ 和 bpftool
clang -O2 -g -target bpf -c scx_lodestar.bpf.c -o scx_lodestar.bpf.o

# 通过 scx_loader 加载
scx_loader scx_lodestar.bpf.o --(verbose)

# 或手动加载
bpftool prog load scx_lodestar.bpf.o /sys/fs/bpf/scx_lodestar \
    type tracing
bpftool prog attach pinned /sys/fs/bpf/scx_lodestar \
    /sys/kernel/tracing/events/sched/sched_wakeup

3.2 关键陷阱:Watchdog 与退出条件

Sched-Ext 调度器有一个内置 watchdog:如果 BPF 程序在某个 SCX_OPS 回调中耗时超过一定阈值(默认 5ms),内核会认为调度器 bug 导致系统不稳定,立即停用调度器并回退到 CFS。

// 错误示例:在 enqueue 中做复杂计算
SEC("tp_btf/sched_wakeup")
int BPF_PROG(bad_enqueue, struct task_struct *p) {
    // BPF verifier 不会报错,但 watchdog 会触发!
    for (int i = 0; i < 1000; i++) { ... } // 危险
    return 0;
}

所以 BPF 调度器必须保证所有回调 O(1) 复杂度,复杂数据结构预分配,用 BPF_MAP_TYPE_PERCPU_ARRAY 而不是 iterate。

4. 生产级调度器最佳实践

Meta 的 Greg Gershony 在 LPC 2024 和 LPC 2025 上分享了 sched_ext 在数据中心的部署经验。以下是从公开演讲中提炼的关键数据:

4.1 Meta 的 Rust 调度器 scx_rustland

Meta 开发了一个混合调度器 scx_rustland,用 Rust+BPF 实现。核心思路:BPF 侧只做快速入队(enqueue),用户态 Rust 侧做复杂调度决策。

性能数据(截至 2025 Q3):

指标 CFS scx_rustland 提升
尾部延迟 P99(Cache workload) 12ms 7ms -42%
吞吐量(Web 服务) 100% 108% +8%
CPU 空闲比例 15% 8% 节省 7%
调度器开销 <1% 0.3% -70%

4.2 Google 的 AI 训练场景

Google 在 Borg 集群中使用 sched_ext 实现了"gang scheduling":一组 8-GPU 的训练任务要么全部调度成功,要么全部不跑,避免了等待"第 9 个卡"死锁。

// Gang scheduling 核心简化逻辑
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 1024);
    __type(key, u32);   // job_id
    __type(value, struct gang_group);
} gang_groups SEC(".maps");

SEC("tp_btf/sched_switch")
int gang_dispatch(...) {
    struct gang_group *grp = bpf_map_lookup_elem(&gang_groups, &job_id);
    if (!grp) return 0;
    
    // 只有同一个 gang 内所有任务都满足时才 dispatch
    if (grp->nr_ready >= grp->nr_total) {
        // 一次性出队所有任务
        for (int i = 0; i < grp->nr_total; i++) {
            scx_bpf_dispatch(grp->tasks[i], SCX_DSQ_GLOBAL, 
                             SCX_SLICE_DFL, 0);
        }
    }
}

5. 与现有子系统的交互

5.1 Sched-Ext 与 cgroup v2

从 Linux 6.13 开始,sched_ext 原生支持 cpu 控制器:

# 启用特定 cgroup 的 Sched-Ext 调度
echo "scx_lodestar" > /sys/fs/cgroup/mygroup/cpu.sched_ext.name

# 同时设置 CPU 配额——与 CFS 带宽限制协同
echo "80000 100000" > /sys/fs/cgroup/mygroup/cpu.max

注意:Sched-Ext 调度器接管 CPU 时,cgroup v2 的 CPU bandwidth control 仍然生效。两者不冲突:sched_ext 决定"哪个任务先跑",bwctl 决定"跑多久"。

5.2 Sched-Ext 与 Real-Time

Sched-Ext 调度器不能抢占 SCHED_FIFO/SCHED_RR 实时任务。实时任务在 sched_setscheduler() 后自动进入 RT-class(rt_rq),sched_ext 的 BPF 调度器只对 SCHED_NORMAL、SCHED_BATCH、SCHED_IDLE 任务生效。

# 实时任务不受 sched_ext 影响
chrt -f 50 ./my_rt_app   # SCHED_FIFO 50——走 rt_rq
./my_normal_app           # SCHED_NORMAL——走 sched_ext

5.3 与 NUMA 亲和

sched_ext 提供了 scx_bpf_dispatch_from_dsq_set_*() 系列 API 实现 NUMA-aware 调度。但在实际部署中,过度的 NUMA 亲和会导致负载不均。Meta 的经验是:只在 DSQ 级别做粗粒度 NUMA 隔离(不同 NUMA node 用不同 DSQ),而不是 per-task 内存绑定。

6. 当前局限与未来方向

6.1 已知限制

• 没有 per-cgroup 优先级继承:不同于 CFS 的 nice 值传播,sched_ext 需要自己在 BPF 中实现优先级继承,复杂度高

• 热迁移开销:sched_ext 的 ops.migrate 回调在热迁移路径上,错误实现会导致 lock contention

• 工具链不成熟:bpftool prog profile 对 sched_ext 仍支持有限,debug 主要靠 bpf_printk

• LSM hook 不安全:BPF 调度器可以读取别的进程的 task_info,这引发安全考虑。上游正在讨论 BPF token 机制来限制可见性

6.2 上游开发进度(2025 Q3)

  • SLEEPER_TASK(v6.14):区分短期 sleep(spinlock)和长期 sleep(I/O),让调度器针对短期 sleep 使用 spin-wait 策略
  • 层次化 DSQ(v6.15 规划中):DSQ 内部再分组,支持更多优先级层次
  • CGroup delegation(v6.16 规划中):容器可以加载自己的 sched_ext 调度器而无需宿主机 root

7. 总结

Sched-Ext 不仅仅是一个新特性——它代表了一种思考操作系统内核的新范式:内核作为平台,策略上移。这在历史上经历了三次浪潮:

时代 技术 调度策略
1990s 单队列 Round Robin / Schedular Class
2000s CFS + cgroup per-cgroup bandwidth control
2020s Sched-Ext 用户态 BPF 程序完全自定义

与 eBPF 在可观测性和安全领域的成功类似,sched_ext 正在将"修改内核=修改策略"变为历史。对于开发者来说,你需要掌握的核心知识是:BPF 编程基础、scx_bpf_dispatch() API 的行为语义、时间片管理与交互/批处理任务的区分。在 2026 年的当下,任何对延迟敏感的负载都值得尝试 sched_ext 部署——从嵌入式到超大规模数据中心。

文档权威参考:Documentation/scheduler/sched-ext.rst(内核源码树),以及 [sched-ext/scx GitHub 仓库](https://github.com/sched-ext/scx)。
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部