引言
调度器是操作系统的核心组件,负责在多个可执行实体之间分配CPU时间。Linux内核从O(1)调度器演进到CFS(Completely Fair Scheduler),再到Deadline调度策略,不断在高吞吐与低延迟之间寻求平衡。本文以Linux 6.x内核源码为基础,深入解析CFS的核心数据结构与算法,并介绍实时调度策略的实战应用。
1. CFS完全公平调度器核心原理
1.1 完全公平的理想模型
CFS的核心思想是追求"完全公平"——如果系统中有N个可运行任务,每个任务应当获得1/N的CPU时间。理想情况下,CPU时间应当被无限细分,每个任务轮流执行极短的时间片。但这会带来巨大的上下文切换开销,因此CFS做了现实折中。
1.2 调度实体(sched_entity)与虚拟运行时间(vruntime)
CFS使用struct cfs_rq(CFS运行队列)管理所有普通任务。每个任务通过struct sched_entity加入这个队列,该结构嵌入在task_struct.se中。
struct sched_entity {
struct load_weight load; // 任务权重(由nice值决定)
struct rb_node run_node; // 红黑树节点
unsigned int on_rq; // 是否在运行队列中
u64 exec_start; // 本次开始执行时间
u64 sum_exec_runtime; // 累计执行时间
u64 vruntime; // 虚拟运行时间 ★核心字段
u64 prev_sum_exec_runtime; // 上次执行前累计
};vruntime(虚拟运行时间)是CFS的核心概念,它表示任务"理应消耗"的加权CPU时间。vruntime增长越慢,说明任务优先级越高(权重越大),下次被调度到的机会越大。
1.3 vruntime计算公式
// kernel/sched/fair.c: update_curr()
delta_exec = now - curr->exec_start; // 实际执行时间(纳秒)
delta_exec_weighted = calc_delta_fair(delta_exec, curr); // 加权时间
curr->vruntime += delta_exec_weighted;关键转换函数:
static inline u64 calc_delta_fair(u64 delta, struct sched_entity *se) {
// NICE_0_LOAD = 1024 (nice 0的权重)
if (unlikely(se->load.weight != NICE_0_LOAD))
delta = __calc_delta(delta, NICE_0_LOAD, &se->load);
return delta;
// 等价于: delta * NICE_0_LOAD / se->load.weight
}推导:nice值每降低1级(优先级提高),CPU份额增加约10%。权重与CPU份额呈反比,因此低nice值任务增长vruntime较慢,更容易获得调度机会。
2. 红黑树调度数据结构
2.1 为什么选择红黑树
CFS使用红黑树(rbtree)组织所有可运行任务,以vruntime为key。这种选择而非传统runqueue数组的优势在于:
- O(log n) 插入/删除/查找最左节点:每次pick_next_task只需取最左节点(最小vruntime)
- 缓存友好性:
rb_leftmost指针缓存了最左节点,实际调度是O(1) - 动态适应性:任务数量可动态伸缩,无需预分配
2.2 pick_next_task_fair 核心流程
// kernel/sched/fair.c: pick_next_task_fair()
static struct task_struct *pick_next_task_fair(struct rq *rq) {
struct cfs_rq *cfs_rq = &rq->cfs;
struct sched_entity *se;
// 1. 从红黑树取最左节点(最小vruntime)
se = pick_next_entity(cfs_rq);
// 2. 设置当前任务的exec_start
set_next_entity(cfs_rq, se);
// 3. 更新组调度层级
do {
cfs_rq = group_cfs_rq(se);
} while (cfs_rq);
return task_of(se);
}2.3 新任务vruntime初始化
新创建的任务不能从vruntime=0开始,否则会长期霸占CPU。CFS使用place_entity()将新任务vruntime设为运行队列的最左节点值(cfs_rq->min_vruntime),让其"追上"已有任务。
// 新任务首次入队(fork后)
static void place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se,
int initial) {
u64 vruntime = cfs_rq->min_vruntime;
if (initial) vruntime += sched_vslice(cfs_rq, se); // 补偿一个时间片
}3. 调度粒度与最小时间片控制
CFS通过sched_latency和min_granularity两个参数控制调度粒度:
/proc/sys/kernel/sched_latency_ns = 24_000_000 // 24ms(默认调度周期)
/proc/sys/kernel/sched_min_granularity_ns = 3_000_000 // 3ms(最小时间片)
/proc/sys/kernel/sched_wakeup_granularity_ns = 4_000_000 // 4ms(唤醒抢占粒度)当可运行任务数量超过 sched_latency / min_granularity 时,每个任务的时间片会被扩大,避免过度上下文切换。
// 计算每个任务的时间片
static u64 sched_period(unsigned long nr_running) {
if (nr_running > sched_latency / min_granularity)
return nr_running * min_granularity;
return sched_latency;
}3.1 唤醒抢占(wake preemption)
当唤醒一个新任务时,检查其vruntime是否比当前任务小超过阈值:
// check_preempt_wakeup()
if (delta < 0 || (delta > 0 && delta < sysctl_sched_wakeup_granularity))
resched_curr(rq); // 抢占当前任务4. NUMA感知与负载均衡
4.1 NUMA复亲和调度
CFS在NUMA系统中加入了NUMA拓扑感知:
- select_task_rq_fair:选择最优运行队列(首选相同NUMA节点)
- NOCSD(No-Cache Sharing Domains):检测共享LLC的CPU组
- sched_numa_balancing:周期性扫描进程地址空间,将进程迁移到靠近其内存的NUMA节点
4.2 负载均衡算法
多核系统中的负载均衡分为:
- PMC级(每个tick):检查是否需要迁移任务
- SD级(调度域):从MC→DIE→NUMA逐级拉伸不平衡
- IDLE级:空闲CPU主动拉任务
// 负载均衡核心入口
static int load_balance(int this_cpu, struct rq *this_rq,
struct sched_domain *sd, enum cpu_idle_type idle) {
// 1. 找到最繁忙的组
// 2. 从最繁忙的CPU拉取任务
// 3. 执行负载转移
}5. 组调度(Group Scheduling)
5.1 CFSBandwidth控制
CFS组调度带宽控制允许为每个cgroup设置CPU使用上限:
// 创建cgroup并设置CPU带宽
mkdir /sys/fs/cgroup/cpu/myapp
echo 100000 > /sys/fs/cgroup/cpu/myapp/cpu.cfs_period_us // 100ms周期
echo 50000 > /sys/fs/cgroup/cpu/myapp/cpu.cfs_quota_us // 50ms配额 = 0.5核
echo $PID > /sys/fs/cgroup/cpu/myapp/cgroup.procs5.2 带宽机制实现
struct cfs_bandwidth {
ktime_t period; // 周期
u64 quota; // 配额
u64 runtime; // 剩余运行时间
struct hrtimer period_timer; // 周期定时器
};
static int do_sched_cfs_period_timer(struct cfs_bandwidth *cfs_b) {
// 周期结束时补充quota
cfs_b->runtime = cfs_b->quota;
// 唤醒所有throttle的任务
unthrottle_cfs_rq(cfs_rq);
}当cgroup中的任务用完配额时,throttle_cfs_rq()将任务移出红黑树,直到下个周期补充。
6. 实时调度策略
6.1 SCHED_FIFO — 先进先出实时策略
SCHED_FIFO是抢占式的FIFO策略,用于硬实时任务:
- 优先级1-99(99最高)
- 一旦获得CPU,一直运行直到:主动yield、阻塞、或被更高优先级抢占
- 同优先级之间FIFO轮转
- 无时间片限制——低优先级任务可能被饿死
// 设置SCHED_FIFO优先级50
struct sched_param param = { .sched_priority = 50 };
sched_setscheduler(pid, SCHED_FIFO, ¶m);6.2 SCHED_RR — 轮询实时策略
SCHED_RR在SCHED_FIFO基础上增加了时间片:
- 同优先级任务按时间片RR轮转
- 时间片默认为100ms(
RR_TIMESLICE) - 适合需要公平共享实时优先级的场景
sysctl -w kernel.sched_rr_timeslice_ms=1006.3 SCHED_DEADLINE — 全局最早截止时间优先(EDF)
SCHED_DEADLINE是Linux 3.14+引入的基于EDF的实时调度策略,适合有明确截止时间要求的实时任务。
// SCHED_DEADLINE参数
struct sched_attr {
.size = sizeof(struct sched_attr),
.sched_policy = SCHED_DEADLINE,
.sched_runtime = 10_000_000, // 10ms(每个周期内运行时间)
.sched_deadline = 20_000_000, // 20ms(相对截止时间)
.sched_period = 20_000_000, // 20ms(周期)
};6.4 EDF调度可行性判定
SCHED_DEADLINE在任务准入时使用Constant Bandwidth Server (CBS)算法验证:
// 可行性检查条件
Σ (runtime_i / deadline_i) <= 1.0
// 示例:3个DL任务
// Task A: runtime=3ms, deadline=10ms → 0.3
// Task B: runtime=4ms, deadline=20ms → 0.2
// Task C: runtime=2ms, deadline=15ms ≈ 0.133
// 总和 = 0.633 < 1.0 ✓ 可行当CPU利用率超过100%时,sched_setattr()返回-EBUSY。
7. 调度器性能调优
7.1 减少上下文切换
对于延迟敏感型应用:
# 提高sched_min_granularity_ns减少抢占
echo 10000000 > /proc/sys/kernel/sched_min_granularity_ns # 10ms
# 降低wakeup_granularity减少唤醒抢占
echo 10000000 > /proc/sys/kernel/sched_wakeup_granularity_ns7.2 CPU亲和性绑定
// 使用taskset或sched_setaffinity绑定到特定CPU
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(2, &cpuset);
CPU_SET(3, &cpuset);
sched_setaffinity(pid, sizeof(cpuset), &cpuset);7.3 禁用NUMA自动均衡
对于单节点内存密集型应用:
echo 0 > /proc/sys/kernel/numa_balancing
# 或使用sysctl
sysctl kernel.numa_balancing=07.4 实时隔离
使用isolcpus内核参数隔离CPU给实时任务:
# GRUB配置
isolcpus=4-7 nohz_full=4-7 rcu_nocbs=4-7
# 将实时任务绑到隔离CPU
chrt -f 99 taskset -c 4 ./realtime_task7.5 监控与诊断
# 查看调度器统计
cat /proc/${PID}/sched | head -30
# 使用perf分析调度延迟
perf sched record -a sleep 10
perf sched latency
# 使用ftrace跟踪调度事件
echo 1 > /sys/kernel/debug/tracing/events/sched/enable
cat /sys/kernel/debug/tracing/trace_pipe | head -508. 调度策略选择指南
| 场景 | 推荐策略 | 理由 |
|---|---|---|
| 桌面交互应用 | CFS + nice调整 | 公平共享,避免饿死 |
| Web服务器 | CFS默认 | 高吞吐,自动均衡 |
| 音视频编码 | SCHED_RR | 实时性+公平性 |
| 工业控制(硬实时) | SCHED_FIFO高优先级 | 确定性延迟,不超调 |
| 自动驾驶/机器人 | SCHED_DEADLINE | 可证明的截止时间保证 |
| 后台批处理 | SCHED_BATCH | 低优先级,防止干扰交互 |
结论
Linux内核CFS调度器通过虚拟运行时间和红黑树的巧妙组合,在公平性和效率之间取得了出色的平衡。理解CFS的vruntime计算机制、红黑树数据结构、粒度控制、NUMA感知和组调度,是进行系统级性能优化的基石。对于实时场景,SCHED_FIFO提供硬实时抢占能力,SCHED_DEADLINE则通过EDF算法提供可证明的截止时间保证。在实际部署时,需要结合具体应用特征,合理选择调度策略并进行系统级调优。
- 源码参考:
kernel/sched/fair.c、kernel/sched/rt.c、kernel/sched/deadline.c - 配置文档:
Documentation/scheduler/ - 调试工具:
perf sched、ftrace sched_*、/proc/<pid>/sched

发表评论 取消回复