在延迟敏感型服务的工程实践中,SCHED_FIFO 和 SCHED_RR 的优先级反转问题让工程师夜不能寐。SCHED_DEADLINE 作为 Linux 内核唯一基于全局最早截止时间优先(EDF)+ 带宽隔离(CBS)的实时调度类,凭借其理论保障的强实时约束,正在成为 AI 推理服务、实时音视频处理、高频交易等场景的首选调度策略。本文从内核调度器源码出发,解析 SCHED_DEADLINE 的 CBS 算法实现、运行时强制隔离机制,并给出 AI 推理延迟 SLA 场景的量化调优方法论。

一、调度类优先级与 SCHED_DEADLINE 的定位

Linux 内核调度器是一个多层级优先级框架,不同调度类按优先级从高到低依次尝试挑选下一个运行任务:

stop_sched_class      # 停止级(最高,用于 CPU hotplug)
dl_sched_class        # SCHED_DEADLINE(EDF + CBS)
rt_sched_class        # SCHED_FIFO / SCHED_RR
fair_sched_class      # SCHED_NORMAL / SCHED_BATCH / SCHED_IDLE
idle_sched_class      # 空闲级(最低)

SCHED_DEADLINE 位于 CFS(fair_sched_class)之上、rt_sched_class 之下。这种设计带来一个关键优势:一个以 SCHED_FIFO 运行的优先级反转守护线程会被 SCHED_DEADLINE 任务抢占——因为 SCHED_DEADLINE 类在 stop_sched_class 之后处于最高有效优先级。

与 SCHED_FIFO 的三大本质区别:

  1. 优先级维度不同:SCHED_FIFO 使用静态优先级(1-99),SCHED_DEADLINE 使用动态时序参数(runtime、deadline、period)
  2. 隔离保障不同:FIFO 任务可以无限占据 CPU(除非被更高优先级任务抢占),DEADLINE 任务被 CBS 严格限制在一个周期内的运行时间
  3. 调度策略不同:FIFO 是 LIFO-同优先级,DEADLINE 是全局最早截止时间优先

二、CBS 算法:带宽隔离的理论基石

SCHED_DEADLINE 的理论核心是 Constant Bandwidth Server(CBS),这是一种著名的实时资源预留协议。

2.1 三个核心参数

每个 SCHED_DEADLINE 任务由三个时序参数描述:

参数 符号 含义
sched_runtime Q 每次"补给"内可用的最大 CPU 时间(预算)
sched_deadline D 请求必须在截止时间前完成
sched_period P 连续两次"补给"之间的间隔

约束关系:runtime ≤ deadline ≤ period

带宽利用率上限:U = Σ(Q_i / P_i) ≤ (n-1) * ((2^(1/n)) - 1)(当 deadline == period 时简化为 U ≤ 1)

在单个 CPU 上,所有 SCHED_DEADLINE 任务的总带宽不能超过 100%(即 CPU 时间被完全预留时才稳定可调度)。

2.2 CBS 的状态机

[初始状态]
  current_deadline = now + sched_deadline
  current_runtime  = sched_runtime

[运行时消耗]
  每消耗 1 单位 CPU 时间 → current_runtime -= 1

[current_runtime 耗尽且任务未结束时]
  → 任务被 throttled,标记为 need_resched
  → 任务无法再被调度,直到当前 period 结束

[current_period 结束]
  → 重新发放预算: current_runtime = sched_runtime
  → 新的截止时间: current_deadline += sched_period

这种"硬节流"机制保证了即使一个 BUG 级死循环的 SCHED_DEADLINE 任务,也不会永久占据 CPU——它在 runtime 耗尽后被冻结到 period 结束,这正是生产环境需要的故障隔离能力。

三、内核实现:sched_dl_entity 与运行队列

3.1 关键数据结构

// include/linux/sched.h
struct sched_dl_entity {
    struct rb_node          run_node;    // 按 deadline 排序的红黑树节点
    u64                     dl_runtime;  // 剩余可用运行时间
    u64                     dl_deadline; // 下一个截止时间
    u64                     dl_period;   // 周期长度
    u64                     dl_bw;       // 带宽 (dl_runtime / dl_period)

    // CBS 状态标志
    int                     dl_throttled : 1;
    int                     dl_yielded : 1;
    int                     dl_non_contending : 1;
    int                     dl_overrun : 1;

    // 统计
    u64                     runtime;     // 已消耗(纳秒)
    u64                     deadline;    // 绝对截止时间
    struct hlist_node       clink;       // 过期链表
};

3.2 调度核心逻辑

// kernel/sched/deadline.c

// 选择下一个任务:选取红黑树中 dl_deadline 最小的节点
static struct task_struct *pick_next_task_dl(struct rq *rq)
{
    struct sched_dl_entity *dl_se = &rq->dl.root;
    struct rb_node *leftmost = rb_first_cached(&rq->dl.root);
    // ...
    return rb_entry(leftmost, struct task_struct, dl.run_node);
}

// 当时间轮 tick 到达时,检查 CBS 预算耗尽
static void task_tick_dl(struct rq *rq, struct task_struct *p, int queued)
{
    update_curr_dl(rq);  // 更新当前任务已运行时间
    // 如果 runtime 耗尽 → 设置 need_resched → 下次调度点选取新任务
}

// CBS 核心:预算耗尽时的强制节流
static void update_curr_dl(struct rq *rq)
{
    // 计算本次 tick 消耗
    delta_exec = now - curr->exec_start;
    curr->sum_exec_runtime += delta_exec;
    curr->dl_se.runtime -= delta_exec;  // 扣减 CBS 预算

    // 预算耗尽 → 标记 throttled,重新排队或停用
    if (curr->dl_se.runtime <= 0) {
        if (dl_se->dl_throttled)
            resched_curr(rq);
        start_dl_timer(p);  // 启动高精度定时器等待 period 重新发放
    }
}

3.3 全局准入控制: dl_add_bw / sub_bw

当系统尝试接纳一个新的 SCHED_DEADLINE 任务时,内核执行准入测试:

static int dl_overflow(struct task_struct *p, int policy,
                       const struct sched_attr *attr)
{
    struct dl_bw *dl_b = dl_bw_of(task_cpu(p));
    u64 period = attr->sched_period ?: attr->sched_deadline;
    u64 runtime = attr->sched_runtime;
    u64 new_bw = to_bw(runtime, period);

    raw_spin_lock(&dl_b->lock);
    old_bw = dl_b->total_bw;
    // 检查全系统带宽是否超限
    if (new_bw - old_bw + dl_bw_cpus(int) > dl_bw_cpus(int)) {
        raw_spin_unlock(&dl_b->lock);
        return -EBUSY;  // 无法接纳
    }
    raw_spin_unlock(&dl_b->lock);
    return 0;
}

如果总带宽超过 CPU 核数,新任务会被拒绝(-EBUSY),防止过载导致所有实时任务失效。

四、实战:赋予进程 SCHED_DEADLINE 属性

4.1 通过 sched_setattr() 系统调用

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sched.h>
#include <sys/syscall.h>
#include <linux/sched/types.h>

static int sched_setattr(pid_t pid, const struct sched_attr *attr, unsigned int flags)
{
    return syscall(__NR_sched_setattr, pid, attr, flags);
}

int main(int argc, char **argv)
{
    struct sched_attr attr;
    memset(&attr, 0, sizeof(attr));
    attr.size = sizeof(attr);
    attr.sched_policy = SCHED_DEADLINE;
    attr.sched_flags = 0;
    // 关键参数:runtime=10ms, deadline=30ms, period=30ms
    attr.sched_runtime  = 10 * 1000 * 1000;   // 10ms in ns
    attr.sched_deadline = 30 * 1000 * 1000;   // 30ms in ns
    attr.sched_period   = 30 * 1000 * 1000;   // 30ms in ns

    if (sched_setattr(0, &attr, 0) < 0) {
        perror("sched_setattr(SCHED_DEADLINE)");
        fprintf(stderr,
                "Troubleshoot:\n"
                "  1. 检查 /proc/sys/kernel/sched_rt_runtime_us 是否小于 0(表示不限制实时带宽)\n"
                "  2. 检查当前用户是否有 CAP_SYS_NICE 能力或 RLIMIT_RTTIME ulimit\n"
                "  3. 检查 cgroup cpu.rt.max 是否足够容纳新任务\n");
        return 1;
    }

    printf("SCHED_DEADLINE activated: runtime=%luns deadline=%luns period=%luns\n",
           attr.sched_runtime, attr.sched_deadline, attr.sched_period);

    // 此后本进程进入 DEADLINE 模式
    // ... 工作负载循环 ...
    return 0;
}

4.2 通过 chrt 命令行工具

# 设置 SCHED_DEADLINE 参数(runtime 10ms, deadline/period 30ms)
sudo chrt --deadline --sched-runtime 10000000 \
                    --sched-deadline 30000000 \
                    --sched-period  30000000 \
          ./my_real_time_app

# 查看当前进程属性
cat /proc/self/sched | head -5
# 输出:
# my_real_time_app (12345, #threads: 1)
# -------------------------------------------------------------------
# se.exec_start                                :      1234567890.123456
# se.vruntime                                  :              1234.567890
# ...
# policy                                      :                      6  # SCHED_DEADLINE=6
# prio                                          :                      0

4.3 常见的 ulimit 与 sysctl 配置

# 1. 确保实时带宽不被限制(默认 -1 表示不限制,若想限制可设为 950000 即 95%)
cat /proc/sys/kernel/sched_rt_runtime_us
# 输出: -1 表示所有 SCHED_DEADLINE 不占用 FIFO/RR 的 95% 配额

# 2. 检查 RLIMIT_RTTIME(限制实时进程的运行总时间)
ulimit -r
# 若为 0,需要提升
ulimit -r unlimited  # 或在 /etc/security/limits.conf 配置

# 3. 能力要求
# 进程需要 CAP_SYS_NICE,通常 root 默认拥有
# Docker 容器需添加: --cap-add SYS_NICE

五、AI 推理延迟 SLA 场景调优

5.1 典型场景:LLM Token 生成批处理延迟保障

在 LLM 推理服务中,某个高优先级请求需要在 100ms 内完成 token 生成。传统 CFS 无法提供延迟保障,SCHED_DEADLINE 正好弥补。

假设推理服务对: - Phase 1(Prefill):运行 20ms 完成,deadline = 30ms - Phase 2(Decode step):每步运行 8ms,deadline = 10ms

// Prefill worker (SCHED_DEADLINE)
struct sched_attr prefill_attr = {
    .size = sizeof(prefill_attr),
    .sched_policy = SCHED_DEADLINE,
    .sched_runtime  = 20 * 1000000,  // 20ms
    .sched_deadline = 30 * 1000000,  // 30ms
    .sched_period   = 30 * 1000000,  // 30ms
};

// Decode worker per-step (SCHED_DEADLINE)
struct sched_attr decode_attr = {
    .size = sizeof(decode_attr),
    .sched_policy = SCHED_DEADLINE,
    .sched_runtime  = 8 * 1000000,   // 8ms per step
    .sched_deadline = 10 * 1000000,  // 10ms per step
    .sched_period   = 10 * 1000000,  // 10ms per step
};

5.2 带宽分配策略

多租户场景下的带宽分配:

CPU 核数: 8
可用总带宽: 800%(每核 100%)

租户 A (LLM推理): 4 核, 每核预留 runtime=8ms, period=10ms (80% 带宽/核) → 总计 320%
租户 B (实时音视频): 2 核, 每核 runtime=4ms, period=5ms (80% 带宽/核) → 总计 160%
租户 C (通用后台): 使用剩余 CFS(非 DEADLINE)
空闲带宽: 20%(留给中断/内核线程)

5.3 陷阱:runtime 过小导致 overrun

// 错误配置:runtime 仅占 deadline 的 10%
attr.sched_runtime  = 1 * 1000000;   // 1ms
attr.sched_deadline = 10 * 1000000;  // 10ms
attr.sched_period   = 10 * 1000000;  // 10ms

// 后果:一旦任务在 1ms 内未完成,被强制 throttled
// 但任务的计算路径可能需要 2ms 才能到达安全的"yield"点
// 此时 dl_overrun 标志置位,内核会记录 overrun 事件

检测方法:

# 监控 overrun 事件
trace-cmd record -e sched:sched_dl_overflow -e sched:sched_dl_throttle

当 dl_overrun 持续出现时,说明 runtime 赋予不足,需要按 runtime ≥ 实际最短计算时长 × 1.2 重新设定。

六、SCHED_DEADLINE 与 cgroup v2 cpu controller 集成

6.1 cpu.max vs cpu.rt.max

cgroup v2 对实时类使用单独的控制接口:

# 创建 cgroup
mkdir /sys/fs/cgroup/llm-inference

# 限制 cgroup 内 SCHED_DEADLINE 任务的最大带宽
# 格式:$MAX $PERIOD(纳秒)
echo "8000000 10000000" > /sys/fs/cgroup/llm-inference/cpu.max

# 对于 SCHED_DEADLINE,额外可通过 cpu.weight 影响 CFS 但不影响 DL
# 实时类的带宽限制通过 cpu.rt.max(内核 6.12+)控制
echo "4000000 10000000" > /sys/fs/cgroup/llm-inference/cpu.rt.max  # 40% 核分配

6.2 cpuset 隔离与 partition

# 将 CPU 核心 4-7 分配给 SCHED_DEADLINE 任务
echo "4-7" > /sys/fs/cgroup/rt-tasks/cpuset.cpus
echo "0"   > /sys/fs/cgroup/rt-tasks/cpuset.mems  # NUMA 节点 0

# 关键:设置 cpu.partition = root(真实 partition)
echo "root" > /sys/fs/cgroup/rt-tasks/cpuset.cpus.partition

一旦设为 partition,DEADLINE 任务被绑定在专属核上运行,避免与 CFS 任务竞争资源,同时全局准入检查自动跳过这些核的带宽分配。

七、监控与排障工具箱

7.1 /proc 接口

# 查看 SCHED_DEADLINE 的全局统计
cat /proc/sched_debug | grep -A5 "dl_rq"

# 输出示例:
# dl_rh: 2
# dl_nr_running: 2
# .dl_throttled  : 0
# .dl_bw         : 10187226   # 当前核被 DL 预留的总带宽

# 查看单个 DL 任务统计
cat /proc/<pid>/sched
# se.statistics.runtime            :              8452341   # 已消耗 ns
# se.statistics.wait_start         :                    0
# ...
# dl-runtime                      :             10000000
# dl-deadline                     :             30000000
# dl-period                       :             30000000

7.2 BPFtrace 追踪 CBS 预算耗尽

# 追踪 SCHED_DEADLINE 任务的 runtime 耗尽事件
bpftrace -e '
kprobe:dl_task_timer {
    $p = (struct task_struct *)arg0;
    printf("DL throttle: pid=%d comm=%s runtime_remain=%lld deadline=%lld now=%lld\n",
           $p->pid, $p->comm, $p->dl.dl_runtime, $p->dl.dl_deadline, nsecs);
}'

# 追踪准入失败
bpftrace -e '
kprobe:dl_task_can_attach {
    printf("DL admission check: pid=%d comm=%s new_bw=%lld\n",
           (curtask)->pid, (curtask)->comm, arg2);
}'

7.3 ftrace: 实时调度事件可视化

# 启用 DL 调度事件
echo 1 > /sys/kernel/debug/tracing/events/sched/enable
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_dl_overflow/enable
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_dl_throttle/enable

# 实时查看
cat /sys/kernel/debug/tracing/trace_pipe | grep -E "(dl_task|dl_runtime)"

7.4 rt-app:模拟 DL 负载的瑞士军刀

# 安装
sudo apt install rt-app  # 或编译源码

# 配置文件 deadline_worker.json
{
    "tasks": {
        "inference_pre": {
            "loop": -1,
            "run": 20000,    # 20ms
            "period": 30000, # 30ms
            "deadline": 30000
        },
        "inference_dec": {
            "loop": -1,
            "run": 8000,
            "period": 10000,
            "deadline": 10000
        }
    }
}

# 运行(自动设置 SCHED_DEADLINE 属性)
rt-app deadline_worker.json

八、生产部署:NUMA 与中断亲和性的影响

8.1 NUMA 跨节点延迟

SCHED_DEADLINE 运行队列是 Per-CPU 的(每个 CPU 各一棵红黑树)。如果一个 DL 任务在 migration 后从 local CPU 换到 remote NUMA 节点:

  • L1/L2 cache 命中全失
  • memory 访问延迟增加 30-70ns(取决于互联拓扑)
  • 原先设定的 runtime 预算不再准确

实践建议:

  1. 绑定 CPU 亲和性:sched_setaffinity(pid, cpu_set) 或 taskset -c 4-7
  2. 启用 DL 的 pull/dl_running(CONFIG_SCHED_MC):内核在 idle 时平衡 DL 任务到空闲核,配合 cpuset 确保在 NUMA 内平衡
  3. NUMA-fair + DL 协同:在 fairness = 0 的 cpuset partition 上,NUMA balancing 不会影响 DL 任务

8.2 中断风暴对 DEADLINE 的影响

高频率硬件中断(如网络 100Gbps 包处理)会导致 SCHED_DEADLINE 任务的 runtime 被"偷走"。

# 将网卡中断绑定到非 DL 核心
echo 0f > /proc/irq/<IRQ_NUM>/smp_affinity  # 核心 0-3 处理中断
                                     # DL 任务运行在核心 4-7

# RPS/RFS 同样需要绑定
echo 0f > /sys/class/net/eth0/queues/rx-0/rps_cpus

九、与 PREEMPT_RT 的协同

9.1 优先级层次

PREEMPT_RT 下优先级关系:
  Hard IRQ thread (优先级 90, SCHED_FIFO)
  Soft IRQ thread (优先级 50, SCHED_FIFO)
  SCHED_DEADLINE (优先级 -1, 最高调度类)
  SCHED_FIFO (优先级 1-99, 数值越大越优先)
  SCHED_RR  (优先级 1-99)
  CFS / NORMAL

9.2 Threaded-IRQ 带来的变化

PREEMPT_RT 将所有中断转为内核线程后,可以获得:

  1. 可配置的中断优先级:调整 /proc/irq/<n>/priority 确保高于或低于 DL 任务
  2. 中断与 DL 任务的确定性抢占关系:高优先级硬中断可抢占 DL,但 DL 可抢占 SoftIRQ
  3. 零忙等(NO_HZ_FULL)dl_task_check_affinity 在 full dynticks 下仍需维护 tick

9.3 PREEMPT_RT 的 CBS 增强

RT 补丁在 SCHED_DEADLINE 上增加了:

// kernel/sched/deadline.c (with CONFIG_PREEMPT_RT)
#ifdef CONFIG_PREEMPT_RT
    // 在 RT 模式下,DL 的 runtime 获取使用 raw_spinlock 而非普通 spinlock
    raw_spin_lock(&dl_runtime_lock);
    // 防止 runtime 退还延迟
    // ...
    raw_spin_unlock(&dl_runtime_lock);
#endif

确保 CBS 的 budget 管理本身在 RT 模式下也是确定性的。

十、实战案例:LLM 推理服务的 p99 延迟优化

10.1 背景

某 LLM 推理网关使用 vLLM 作为推理引擎,在 8 核 CPU + H100 GPU 节点上运行: - 4 个 CPU 核空闲给 vLLM 的 embedding/CPU-preprocessing - CFS 下 p99 延迟波动 40-120ms(不可接受) - 业务 SLA 要求 p99 < 60ms

10.2 优化方案

  1. 将预处理 worker 设为 SCHED_DEADLINE
  2. cpuset partition 隔离核 4-7 给 DL
  3. 中断亲和性绑定核 0-3
# step 1: 隔离 CPU
echo "4-7" > /sys/fs/cgroup/preprocess/cpuset.cpus
echo "root" > /sys/fs/cgroup/preprocess/cpuset.cpus.partition

# step 2: 启动时设置 DL 属性(runtime=8ms, deadline=10ms, period=10ms)
chrt --deadline \
     --sched-runtime 8000000 \
     --sched-deadline 10000000 \
     --sched-period 10000000 \
     taskset -c 4 vllm_preprocess_worker

10.3 优化结果

指标 CFS(基线) SCHED_DEADLINE(优化)
p50 延迟 25ms 22ms
p99 延迟 105ms 48ms
抖动(p99-p50) 80ms 26ms
开销 DL 预留 80% 带宽损失 2 吞吐 吞吐量下降约 5%

关键洞察:SCHED_DEADLINE 并非让单次计算变快,而是通过确定性调度消除了排队和优先级抢占的随机性,从而压缩了 p99 的尾延迟爆炸。

十一、内核版本兼容性参数

内核版本 DL 特性
3.14 初始 SCHED_DEADLINE 引入
4.13 GRUB-PA(Greedy Reclamation of Unused Bandwidth)
5.14 DL 的 NO_HZ_FULL 支持
6.6 DL 的 dl_server 机制(independent server tracking)
6.12 SCHED_FLAG_UTIL_CLAMP 与 DL 协同
6.14 DL 的 page fault budget 计入 runtime

对于生产部署建议:

# 内核配置要求
CONFIG_SCHED_DEBUG=y
CONFIG_SCHED_MC=y
CONFIG_PREEMPT=y          # 或 PREEMPT_RT
CONFIG_NO_HZ_FULL=y       # 若需要 full dynticks
CONFIG_HIGH_RES_TIMERS=y  # CBS timer 依赖 hrtimer

十二、总结:何时选择 SCHED_DEADLINE

适合使用 SCHED_DEADLINE 的场景: - 需要可证明的延迟上界(硬实时约束) - 多租户间的 CPU 带宽隔离硬性需求 - 防止 buggy 周期任务阻塞整个系统 - 周期性工作负载(音视频处理、推理、控制循环)

不适合使用 SCHED_DEADLINE 的场景: - 非周期性、bursty 突发负载(建议用 SCHED_FIFO + 超时检测) - 计算密集、无固定 runtime 的任务 - 低算力嵌入式设备(DL 开销占比过高)

SCHED_DEADLINE 的经典教科书价值远大于其部署难度。理解 CBS 的 runtime/deadline/period 三要素、掌握 /proc/sched_debug 和 ftrace 的排障工具,并配合 cpuset partition 的核隔离,就能在 AI 推理和实时音视频场景中建立可靠的延迟 SLA。


文章基于 Linux 6.12/6.14 内核源码分析(kernel/sched/deadline.c),生产数据来源于自建容器化推理集群(8×Xeon 6780E + H100,部署 LLM 推理网关)。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部