一、简介:调度策略为何是 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_class | sched.c | 停止任务(CPU 热插拔) | 最高,-1 |
dl_sched_class | sched/deadline.c | SCHED_DEADLINE | 0 |
rt_sched_class | sched/rt.c | SCHED_FIFO, SCHED_RR | 1-99 |
fair_sched_class | sched/fair.c | SCHED_NORMAL, SCHED_BATCH, SCHED_IDLE | 100-139 |
idle_sched_class | sched/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_NORMAL | fair | nice: -20~19 | 公平分享、交互奖励 | 实时任务误用 |
| SCHED_FIFO | rt | 1-99 | 抢占式、无时间片 | 长期占用 CPU |
| SCHED_RR | rt | 1-99 | 抢占式、RR 时间片 | 同优先级任务过多 |
| SCHED_BATCH | fair | nice: -20~19 | 减少抢占、提高吞吐 | 交互任务误用 |
| SCHED_IDLE | fair | 始终最低 | 仅空闲时运行 | 期望及时响应 |
| SCHED_DEADLINE | dl | 动态计算 | 带宽隔离、截止时间保证 | 参数配置错误 |
| SCHED_EXT | ext | BPF 程序定义 | 用户态可编程调度 | 内核版本不支持 |
2.3 关键术语表
| 术语 | 定义 | 源码关联 |
|---|---|---|
| vruntime | 虚拟运行时间,CFS 公平度量 | struct sched_entity::vruntime |
| rt_runtime_us | 实时任务每周期最大运行时间 | /proc/sys/kernel/sched_rt_runtime_us |
| dl_bw | Deadline 任务带宽占比 | struct dl_bw::bw |
| sched_domain | 调度域,多核负载均衡单元 | struct sched_domain |
| load_balance | 跨 CPU 负载均衡算法 | kernel/sched/fair.c: load_balance() |
三、环境准备:可复现实验平台搭建
3.1 硬件环境
| 组件 | 规格 | 用途 |
|---|---|---|
| CPU | x86_64,≥4 核,支持 TSC | 多核调度实验 |
| 内存 | ≥8 GB | 大负载测试 |
| 存储 | SSD | 快速编译内核 |
3.2 软件环境
| 组件 | 版本 | 安装命令 |
|---|---|---|
| Ubuntu Server | 22.04 LTS | 基础系统 |
| Linux 内核 | 6.6 LTS(含 sched_ext) | 源码编译 |
| perf | 6.6 | apt install linux-tools-common |
| bpftrace | 0.19+ | apt install bpftrace |
| stress-ng | 0.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, ¶m) == -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_IDLE | 5x~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, ¶m)) {
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 | ~1000ms | L 被 M 抢占,H 等待 L+M |
| 有 PI | ~500ms | L 继承 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 编程,最终形成完整的调度子系统知识体系。
参考文献
-
Linux Kernel Documentation:
Documentation/scheduler/ -
Bovet, D. P., & Cesati, M. (2005). Understanding the Linux Kernel
-
Li, T., et al. (2019). "Linux Kernel Scheduler: A Survey". ACM Computing Surveys
-
sched_ext: https://github.com/sched-ext/scx

790

被折叠的 条评论
为什么被折叠?



