一文读懂 Linux 调度策略:7 种策略映射关系与适用场景全梳理

一、简介:调度策略为何是 Linux 性能的核心杠杆

Linux 内核调度器是操作系统最核心的子系统之一,直接决定进程何时获得 CPU 时间、获得多长时间。在多核处理器普及、异构计算兴起的今天,调度策略的选择已成为系统性能调优的首要杠杆:

  • 云计算场景:容器密度提升 30% 往往源于 CFS 参数的精细化调优

  • 实时控制场景:机械臂控制周期从 10ms 抖动降至 100μs 确定性,依赖 SCHED_FIFO 的正确配置

  • 桌面交互场景:Chrome 多标签流畅度与 CFS 的 vruntime 计算密切相关

然而,生产环境中策略滥用现象普遍:将普通计算任务绑定 SCHED_FIFO 导致系统 starvation,或对实时任务使用 CFS 造成不可接受的延迟抖动。本文从源码机制、实验验证、场景映射三个维度,建立完整的调度策略知识体系。


二、核心概念:调度类与策略的层次架构

2.1 调度类(Scheduling Class)——策略的容器

Linux 内核采用模块化调度类设计,通过 sched_class 结构体实现策略扩展:

调度类源码文件管理策略调度优先级
stop_sched_classsched.c停止任务(CPU 热插拔)最高,-1
dl_sched_classsched/deadline.cSCHED_DEADLINE0
rt_sched_classsched/rt.cSCHED_FIFO, SCHED_RR1-99
fair_sched_classsched/fair.cSCHED_NORMAL, SCHED_BATCH, SCHED_IDLE100-139
idle_sched_classsched/idle.c空闲任务最低

关键洞察:调度类优先级固定,高优先级类存在可运行任务时,低优先级类完全无法获得 CPU。

2.2 七种调度策略详解

// include/uapi/linux/sched.h
#define SCHED_NORMAL        0   /* CFS 默认策略 */
#define SCHED_FIFO          1   /* 实时 FIFO */
#define SCHED_RR            2   /* 实时轮询 */
#define SCHED_BATCH         3   /* CFS 批处理变体 */
#define SCHED_IDLE          5   /* CFS 空闲任务 */
#define SCHED_DEADLINE      6   /* 截止时间调度 */
#define SCHED_EXT           7   /* 可扩展调度(BPF,6.12+)*/
策略调度类优先级范围核心特性典型误用
SCHED_NORMALfairnice: -20~19公平分享、交互奖励实时任务误用
SCHED_FIFOrt1-99抢占式、无时间片长期占用 CPU
SCHED_RRrt1-99抢占式、RR 时间片同优先级任务过多
SCHED_BATCHfairnice: -20~19减少抢占、提高吞吐交互任务误用
SCHED_IDLEfair始终最低仅空闲时运行期望及时响应
SCHED_DEADLINEdl动态计算带宽隔离、截止时间保证参数配置错误
SCHED_EXTextBPF 程序定义用户态可编程调度内核版本不支持

2.3 关键术语表

术语定义源码关联
vruntime虚拟运行时间,CFS 公平度量struct sched_entity::vruntime
rt_runtime_us实时任务每周期最大运行时间/proc/sys/kernel/sched_rt_runtime_us
dl_bwDeadline 任务带宽占比struct dl_bw::bw
sched_domain调度域,多核负载均衡单元struct sched_domain
load_balance跨 CPU 负载均衡算法kernel/sched/fair.c: load_balance()

三、环境准备:可复现实验平台搭建

3.1 硬件环境

组件规格用途
CPUx86_64,≥4 核,支持 TSC多核调度实验
内存≥8 GB大负载测试
存储SSD快速编译内核

3.2 软件环境

组件版本安装命令
Ubuntu Server22.04 LTS基础系统
Linux 内核6.6 LTS(含 sched_ext)源码编译
perf6.6apt install linux-tools-common
bpftrace0.19+apt install bpftrace
stress-ng0.15+apt install stress-ng

3.3 内核编译与 sched_ext 启用

#!/bin/bash
# setup_kernel.sh - 一键搭建实验环境
set -e

KERNEL_VERSION=6.6.21
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-${KERNEL_VERSION}.tar.xz
tar -xf linux-${KERNEL_VERSION}.tar.xz
cd linux-${KERNEL_VERSION}

# 配置:启用 SCHED_DEADLINE 调试与 sched_ext
make defconfig
./scripts/config --enable CONFIG_SCHED_DEBUG
./scripts/config --enable CONFIG_SCHEDSTATS
./scripts/config --enable CONFIG_SCHED_CORE
./scripts/config --enable CONFIG_SCHED_CLASS_EXT
./scripts/config --enable CONFIG_DEBUG_PREEMPT

make -j$(nproc)
sudo make modules_install
sudo make install

echo "内核安装完成,重启后选择新内核"

3.4 验证环境

# 验证调度类存在
cat /proc/sys/kernel/sched_schedstats  # 应输出 1

# 验证 sched_ext 可用(6.12+)
ls /sys/kernel/sched_ext/  # 应存在

# 安装测试工具
sudo apt install rt-tests sysstat

四、应用场景:云原生数据库的混合负载调度

云原生分布式数据库(如 TiDB、CockroachDB)的生产环境中,典型负载构成:

  • OLTP 事务处理:短查询,延迟敏感,需 SCHED_NORMAL + 高 nice 优先级

  • OLAP 分析查询:长扫描,吞吐优先,适用 SCHED_BATCH 减少缓存污染

  • Raft 共识协议:心跳超时严格,关键路径使用 SCHED_FIFO 保证 10ms 内响应

  • compaction 后台任务:磁盘 IO 密集型,SCHED_IDLE 避免影响前台

调度策略误用案例:某金融客户将 compaction 设为 SCHED_FIFO 优先级 50,导致 Raft 心跳任务(同策略优先级 60)在 compaction 运行时被饿死,引发频繁 leader 选举,P99 延迟从 5ms 恶化至 2s。修复方案:compaction 降级为 SCHED_IDLE,Raft 保持 SCHED_FIFO 优先级 80。


五、实际案例与步骤:七策略全量实验

5.1 实验一:CFS 三策略对比(NORMAL/BATCH/IDLE)

目标

量化 SCHED_BATCH 的吞吐优势与 SCHED_IDLE 的延迟代价。

代码:策略设置与负载生成
// cfs_compare.c
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/resource.h>

// 设置 CFS 策略与 nice 值
int set_cfs_policy(int policy, int nice_val) {
    struct sched_param param = {0};
    
    // 验证策略有效性
    if (policy != SCHED_NORMAL && policy != SCHED_BATCH && policy != SCHED_IDLE) {
        fprintf(stderr, "Invalid CFS policy: %d\n", policy);
        return -1;
    }
    
    // 设置调度策略
    if (sched_setscheduler(0, policy, &param) == -1) {
        perror("sched_setscheduler");
        return -1;
    }
    
    // 设置 nice 值(对 SCHED_IDLE 无效)
    if (policy != SCHED_IDLE && setpriority(PRIO_PROCESS, 0, nice_val) == -1) {
        perror("setpriority");
        return -1;
    }
    
    printf("Set policy=%d, nice=%d (pid=%d)\n", policy, nice_val, getpid());
    return 0;
}

// CPU 密集型任务:计算素数
void cpu_intensive_task(int iterations) {
    struct timeval start, end;
    gettimeofday(&start, NULL);
    
    long long count = 0;
    for (int i = 2; i < iterations; i++) {
        int is_prime = 1;
        for (int j = 2; j * j <= i; j++) {
            if (i % j == 0) { is_prime = 0; break; }
        }
        if (is_prime) count++;
    }
    
    gettimeofday(&end, NULL);
    double elapsed = (end.tv_sec - start.tv_sec) * 1000.0 + 
                     (end.tv_usec - start.tv_usec) / 1000.0;
    printf("Completed: %lld primes in %.2f ms\n", count, elapsed);
}

int main(int argc, char *argv[]) {
    if (argc != 4) {
        fprintf(stderr, "Usage: %s <policy> <nice> <iterations>\n", argv[0]);
        fprintf(stderr, "Policy: 0=NORMAL, 3=BATCH, 5=IDLE\n");
        return 1;
    }
    
    int policy = atoi(argv[1]);
    int nice_val = atoi(argv[2]);
    int iterations = atoi(argv[3]);
    
    if (set_cfs_policy(policy, nice_val) != 0) return 1;
    cpu_intensive_task(iterations);
    return 0;
}
编译与运行
gcc -O2 -o cfs_compare cfs_compare.c

# 三策略并行对比(iterations=500000)
echo "=== SCHED_NORMAL, nice=0 ==="
taskset -c 0 ./cfs_compare 0 0 500000 &

echo "=== SCHED_BATCH, nice=0 ==="  
taskset -c 0 ./cfs_compare 3 0 500000 &

echo "=== SCHED_IDLE ==="
taskset -c 0 ./cfs_compare 5 0 500000 &

wait
预期结果与分析
策略典型完成时间原因
SCHED_NORMAL基准标准公平调度
SCHED_BATCH-10%~15%减少抢占,提高缓存局部性
SCHED_IDLE5x~10x仅空闲时运行,被严重饿死
监控命令
# 实时观察调度延迟
watch -n 0.5 'cat /proc/$(pgrep cfs_compare)/sched | grep -E "se.vruntime|nr_migrations"'

# 统计上下文切换
perf stat -e context-switches,cs -p $(pgrep cfs_compare) sleep 5

5.2 实验二:实时策略优先级反转观测

目标

复现优先级反转现象,验证 SCHED_FIFO 的抢占行为。

代码:三任务优先级模型
// priority_inversion.c
#define _GNU_SOURCE
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sched.h>
#include <time.h>

#define HIGH_PRIO   80
#define MID_PRIO    50
#define LOW_PRIO    20

static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
static volatile int shared_resource = 0;

// 设置实时优先级
void set_rt_priority(int prio) {
    struct sched_param param = {.sched_priority = prio};
    if (pthread_setschedparam(pthread_self(), SCHED_FIFO, &param)) {
        perror("pthread_setschedparam");
        exit(1);
    }
}

// 高优先级任务:尝试获取被低优先级持有的锁
void* high_task(void* arg) {
    set_rt_priority(HIGH_PRIO);
    printf("[H] Started, priority=%d\n", HIGH_PRIO);
    
    struct timespec start, end;
    clock_gettime(CLOCK_MONOTONIC, &start);
    
    pthread_mutex_lock(&lock);  // 此处阻塞,等待 L 释放
    clock_gettime(CLOCK_MONOTONIC, &end);
    
    double wait_ms = (end.tv_sec - start.tv_sec) * 1000.0 +
                     (end.tv_nsec - start.tv_nsec) / 1e6;
    printf("[H] Got lock after %.3f ms\n", wait_ms);
    
    shared_resource++;
    pthread_mutex_unlock(&lock);
    return NULL;
}

// 中优先级任务:纯计算,会抢占低优先级
void* medium_task(void* arg) {
    set_rt_priority(MID_PRIO);
    printf("[M] Started, priority=%d, spinning...\n", MID_PRIO);
    
    // 模拟 500ms 计算,抢占 L
    volatile unsigned long long spin = 0;
    struct timespec deadline;
    clock_gettime(CLOCK_MONOTONIC, &deadline);
    deadline.tv_nsec += 500 * 1000000;
    
    while (clock_gettime(CLOCK_MONOTONIC, &(struct timespec){0}) || 
           (deadline.tv_sec > 0 || deadline.tv_nsec > 0)) {
        spin++;
    }
    
    printf("[M] Done spinning\n");
    return NULL;
}

// 低优先级任务:持有锁,被中优先级抢占
void* low_task(void* arg) {
    set_rt_priority(LOW_PRIO);
    printf("[L] Started, priority=%d, taking lock...\n", LOW_PRIO);
    
    pthread_mutex_lock(&lock);
    printf("[L] Got lock, entering critical section (will be preempted)\n");
    
    // 500ms 临界区,期间会被 M 抢占
    usleep(500000);
    
    printf("[L] Releasing lock\n");
    pthread_mutex_unlock(&lock);
    return NULL;
}

int main() {
    pthread_t t_low, t_mid, t_high;
    
    printf("=== Priority Inversion Demo ===\n");
    printf("Expected: H waits ~500ms (L's CS) + M's spin time\n");
    printf("With PI:  H waits ~500ms only\n\n");
    
    // 创建线程(注意启动顺序确保竞争条件)
    pthread_create(&t_low, NULL, low_task, NULL);
    usleep(10000);  // 确保 L 先拿到锁
    
    pthread_create(&t_mid, NULL, medium_task, NULL);
    usleep(10000);  // 确保 M 开始运行
    
    pthread_create(&t_high, NULL, high_task, NULL);
    
    pthread_join(t_low, NULL);
    pthread_join(t_mid, NULL);
    pthread_join(t_high, NULL);
    
    return 0;
}
编译与运行
gcc -O2 -o priority_inversion priority_inversion.c -pthread

# 需要 root 设置实时优先级
sudo ./priority_inversion
启用优先级继承对比
// 在 main() 中创建锁前添加:
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
pthread_mutex_init(&lock, &attr);
结果对比
配置H 等待时间说明
无 PI~1000msL 被 M 抢占,H 等待 L+M
有 PI~500msL 继承 H 的优先级,M 无法抢占

5.3 实验三:SCHED_DEADLINE 带宽隔离验证

目标

验证 DL 调度器的带宽隔离特性,防止任务超支。

代码:周期任务与过载测试
// deadline_test.c
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/sched.h>

// 内核未导出,需自行定义
struct sched_attr {
    __u32 size;
    __u32 sched_policy;
    __u64 sched_flags;
    __s32 sched_nice;
    __u32 sched_priority;
    __u64 sched_runtime;
    __u64 sched_deadline;
    __u64 sched_period;
};

#ifndef SCHED_DEADLINE
#define SCHED_DEADLINE  6
#endif

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[]) {
    if (argc != 4) {
        fprintf(stderr, "Usage: %s <runtime_us> <deadline_us> <period_us>\n", argv[0]);
        fprintf(stderr, "Example: %s 10000 20000 20000 (50% bandwidth)\n", argv[0]);
        return 1;
    }
    
    __u64 runtime = atoll(argv[1]) * 1000;   // 转 ns
    __u64 deadline = atoll(argv[2]) * 1000;
    __u64 period = atoll(argv[3]) * 1000;
    
    struct sched_attr attr = {
        .size = sizeof(struct sched_attr),
        .sched_policy = SCHED_DEADLINE,
        .sched_runtime = runtime,
        .sched_deadline = deadline,
        .sched_period = period,
    };
    
    if (sched_setattr(0, &attr, 0) == -1) {
        perror("sched_setattr");
        return 1;
    }
    
    printf("SCHED_DEADLINE set: runtime=%.2fms, deadline=%.2fms, period=%.2fms\n",
           runtime/1e6, deadline/1e6, period/1e6);
    
    // 模拟周期任务:运行 runtime,然后睡眠到下一周期
    while (1) {
        struct timespec start, now;
        clock_gettime(CLOCK_MONOTONIC, &start);
        
        // 模拟计算负载
        volatile unsigned long long spin = 0;
        while (spin < runtime / 10) spin++;  // 简化模拟
        
        clock_gettime(CLOCK_MONOTONIC, &now);
        double exec_ms = (now.tv_sec - start.tv_sec) * 1000.0 +
                         (now.tv_nsec - start.tv_nsec) / 1e6;
        
        printf("Job executed in %.3f ms\n", exec_ms);
        
        // 使用 sched_yield 让调度器处理截止时间
        sched_yield();
    }
    
    return 0;
}
运行与监控
gcc -O2 -o deadline_test deadline_test.c

# 创建 2 个 DL 任务,各申请 60% 带宽(总和 120%,第二个将失败)
sudo ./deadline_test 12000 20000 20000 &  # 60%
sudo ./deadline_test 12000 20000 20000     # 60%,预期失败

# 监控 DL 运行队列
watch -n 0.5 'cat /proc/sched_debug | grep -A5 dl_rq'
预期结果
# 第一个任务成功
SCHED_DEADLINE set: runtime=12.00ms, deadline=20.00ms, period=20.00ms

# 第二个任务失败(带宽超限)
sched_setattr: No space left on device

5.4 实验四:sched_ext BPF 自定义调度器(内核 6.12+)

目标

体验用户态可编程调度,实现简单的 FIFO 调度器。

BPF 程序代码
// minimal.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

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

// 自定义调度:简单 FIFO,选择最早入队任务
SEC("struct_ops/minimal_select_cpu")
s32 BPF_PROG(minimal_select_cpu, struct task_struct *p, s32 prev_cpu, u64 wake_flags)
{
    // 优先使用之前运行的 CPU(缓存友好)
    if (bpf_cpumask_test_cpu(prev_cpu, p->cpus_ptr))
        return prev_cpu;
    return bpf_cpumask_any_distribute(p->cpus_ptr);
}

SEC("struct_ops/minimal_enqueue")
void BPF_PROG(minimal_enqueue, struct task_struct *p, u64 enq_flags)
{
    // 直接入队,由核心调度器处理
    scx_bpf_dispatch(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, enq_flags);
}

SEC("struct_ops/minimal_dispatch")
void BPF_PROG(minimal_dispatch, s32 cpu, struct task_struct *prev)
{
    // 从全局队列取任务
    scx_bpf_consume(SCX_DSQ_GLOBAL);
}

// 注册调度类操作
SEC(".struct_ops")
struct sched_ext_ops minimal_ops = {
    .select_cpu = (void *)minimal_select_cpu,
    .enqueue = (void *)minimal_enqueue,
    .dispatch = (void *)minimal_dispatch,
    .name = "minimal",
};
编译与加载
# 依赖 libbpf 和 clang
sudo apt install libbpf-dev clang llvm

# 编译
clang -O2 -target bpf -c minimal.bpf.c -o minimal.bpf.o

# 加载(使用 bpftool 或 sched_ext 加载器)
sudo bpftool struct_ops register minimal.bpf.o

# 验证加载
cat /sys/kernel/sched_ext/root/ops  # 应显示 minimal

六、常见问题与解答

问题现象诊断与解决
sched_setscheduler: Operation not permitted非 root 设置实时优先级需 CAP_SYS_NICE 能力,或修改 /etc/security/limits.conf@realtime - rtprio 99
SCHED_DEADLINE 返回 ENODEV内核未启用 CONFIG_SCHED_DEADLINE检查 grep SCHED_DEADLINE /boot/config-$(uname -r),重新编译内核
实时任务导致系统无响应SCHED_FIFO 任务死循环无 yield添加 watchdog,或使用 SCHED_RR 强制时间片
sched_ext BPF 加载失败invalid func unknown#195内核版本 < 6.12,或 libbpf 版本不匹配
CFS 任务 nice -20 仍不够快需要硬实时保证升级到 SCHED_FIFO/SCHED_DEADLINE,nice 仅影响权重而非抢占

七、实践建议与最佳实践

7.1 策略选择决策树

开始
  │
  ├─ 是否需要硬实时保证(截止时间 < 1ms)?
  │    ├─ 是 → 任务数量少?→ SCHED_FIFO
  │    │              └─ 多 → SCHED_DEADLINE
  │    └─ 否 → 继续
  │
  ├─ 是否为后台批处理(大计算量,无交互)?
  │    └─ 是 → SCHED_BATCH
  │
  ├─ 是否为最低优先级兜底任务?
  │    └─ 是 → SCHED_IDLE
  │
  └─ 默认 → SCHED_NORMAL,按需调整 nice

7.2 生产环境检查清单

  • [ ] sched_rt_runtime_us 预留非实时任务带宽(建议 5%-10%)

  • [ ] 实时任务绑定隔离 CPU(isolcpus + taskset

  • [ ] 启用 SCHED_DEADLINE 时验证总带宽 ≤ 100%

  • [ ] 使用 cyclictest 验证调度延迟:sudo cyclictest -p99 -i100 -d600

  • [ ] 监控 /proc/schedstat 检测异常迁移

7.3 学术研究数据收集

#!/bin/bash
# collect_sched_data.sh - 论文数据收集脚本

OUTPUT="sched_data_$(date +%Y%m%d_%H%M%S)"
mkdir -p $OUTPUT

# 1. 调度器统计
cat /proc/schedstat > $OUTPUT/schedstat_start.txt

# 2. 运行负载
stress-ng --cpu 4 --io 2 --vm 2 --timeout 300 &
STRESS_PID=$!

# 3. 周期性采样
for i in {1..30}; do
    echo "=== Sample $i ===" >> $OUTPUT/proc_sched_debug.log
    cat /proc/sched_debug >> $OUTPUT/proc_sched_debug.log
    
    cat /proc/loadavg >> $OUTPUT/loadavg.log
    
    sleep 10
done

kill $STRESS_PID

# 4. 后处理
cat /proc/schedstat > $OUTPUT/schedstat_end.txt

echo "数据收集完成:$OUTPUT/"

八、总结与应用场景

策略核心适用场景关键配置参数
SCHED_NORMAL通用服务、Web 后端、数据库连接池nice, sched_latency_ns
SCHED_BATCH数据 ETL、编译构建、科学计算同 NORMAL,减少抢占
SCHED_IDLE日志压缩、备份任务、索引重建无参数,自动最低
SCHED_FIFO工业控制、音频处理、硬实时中断priority 1-99, CPU 隔离
SCHED_RR同优先级多实时任务、教学演示priority, sched_rr_timeslice_ms
SCHED_DEADLINE视频编解码、自动驾驶感知、机器人控制runtime, deadline, period
SCHED_EXT研究实验、自定义调度策略(如 ML 推理调度)BPF 程序

掌握 Linux 七种调度策略的底层机制与适用边界,是构建高性能、可预测系统的基石。无论是撰写学术论文中的实验设计,还是优化生产环境的延迟表现,本文提供的源码、实验与数据收集方法均可直接复用。建议读者从 SCHED_NORMAL/BATCH 对比实验入手,逐步深入到实时策略与 sched_ext 编程,最终形成完整的调度子系统知识体系。


参考文献

  1. Linux Kernel Documentation: Documentation/scheduler/

  2. Bovet, D. P., & Cesati, M. (2005). Understanding the Linux Kernel

  3. Li, T., et al. (2019). "Linux Kernel Scheduler: A Survey". ACM Computing Surveys

  4. sched_ext: https://github.com/sched-ext/scx

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值