Linux 内核进程调度器深度实战:从 CFS 红黑树到 EEVDF、实时调度类、cgroups v2 CPU 控制器与生产调优的完全工程指南
操作系统调度器是内核中最核心的组件之一——它决定了哪个线程在何时、哪个 CPU 核上执行。从早期的 O(n) 调度器到 O(1) 调度器,再到完全公平调度器 CFS,直至 Linux 6.6 引入的 EEVDF(Earliest Eligible Virtual Deadline First),Linux 调度器经历了四次重大架构变革。本文将从调度器基本原理出发,深入剖析 CFS 的 vruntime 机制与红黑树实现、EEVDD 算法的革命性改进、实时调度类(SCHED_FIFO/RR/DEADLINE)、cgroups v2 CPU 控制器、NUMA 负载均衡、内核抢占模型、上下文切换开销优化,并给出生产环境下的调优策略与监控方案。
1. 调度器演进简史
Linux 调度器的演进史就是一部"追求公平与效率平衡"的历史:
- O(n) 调度器(Linux 2.4):遍历所有可运行进程选择时间片最大的进程执行,时间复杂度 O(n),在进程数增长时性能急剧下降,SMP 支持原始。
- O(1) 调度器(Linux 2.6.0 ~ 2.6.22):引入 active/expired 两个优先级数组和 140 级优先级位图,选择进程时间复杂度 O(1),但交互进程识别启发式算法复杂且易被"欺骗"。
- CFS(Completely Fair Scheduler,Linux 2.6.23 ~ 6.5):由 Ingo Molnár 提出,核心思想是维护每个进程的 vruntime(虚拟运行时间),使用红黑树选择 vruntime 最小的进程运行,实现数学意义上的"完全公平"。
- EEVDF(Earliest Eligible Virtual Deadline First,Linux 6.6+ 默认):Mel Gorman 等人推动的替代方案,保留 vruntime 的一致性保证,通过引入 deadline 概念在 O(1) 时间内完成调度决策,同时解决了 CFS 在负载变化时的延迟波动问题。
CFS 的核心哲学极其优雅:不使用传统时间片,而是追踪每个进程已获得的 CPU 时间,每次选择累计运行时间最少的进程。这等价于让所有可运行进程"赛跑",落后的进程被优先调度,最终所有进程趋于相同的 vruntime。
2. CFS 核心原理:vruntime 与红黑树
2.1 vruntime 的权重计算
vruntime(虚拟运行时间)是 CFS 的命脉。每个进程的 vruntime 递增速度与其实际运行时间的关系由进程权重决定:
// 内核源码:kernel/sched/fair.c
static void update_curr(struct cfs_rq *cfs_rq)
{
struct sched_entity *curr = cfs_rq->curr;
u64 now = rq_clock_task(rq_of(cfs_rq));
u64 delta_exec;
delta_exec = now - curr->exec_start;
curr->exec_start = now;
curr->sum_exec_runtime += delta_exec;
// 关键公式:vruntime += delta_exec * (NICE_0_LOAD / curr->weight)
curr->vruntime += calc_delta_fair(delta_exec, curr);
update_min_vruntime(cfs_rq);
}
其中 calc_delta_fair 的实现为:vruntime += delta_exec * NICE_0_LOAD / weight。这意味着:
- 高权重(高优先级)进程 vruntime 增长慢 → 更长时间才被追赶上 → 获得更多 CPU 时间。
- 低权重(低优先级)进程 vruntime 增长快 → 很快被其他进程超过 → 让出 CPU。
Linux 使用 40 级优先级(-20 到 19),相邻级权重比约为 1.25:1(即每降低一个 nice 级,多获得约 25% CPU 时间)。具体权重表在 kernel/sched/core.c 中定义为 sched_prio_to_weight[40],其中 nice=0 对应权重 1024(即 NICE_0_LOAD)。
2.2 红黑树数据结构
CFS 使用红黑树(rb-tree)组织可运行进程,键值为 vruntime。红黑树提供了以下操作复杂度:
- 插入:O(log n)
- 删除:O(log n)
- 选择最小值(最左侧节点):O(log n) 实际 O(1) 因为有缓存
每 CPU 运行队列 cfs_rq 维护自己的红黑树:
struct cfs_rq {
struct load_weight load; // 树上所有进程的总权重
unsigned int nr_running; // 可运行进程数
u64 min_vruntime; // 树中最小 vruntime(用于新进程初始化)
struct rb_root_cached tasks_timeline; // 红黑树根节点(缓存最左节点)
struct sched_entity *curr; // 当前运行的进程
// ...
};
调度时的选择操作极为高效:
// pick_next_task_fair() 中最关键一步
static struct sched_entity *__pick_first_entity(struct cfs_rq *cfs_rq)
{
struct rb_node *left = cfs_rq->tasks_timeline.rb_leftmost;
return rb_entry(left, struct sched_entity, run_node);
}
由于 rb_root_cached 缓存了最左端节点,__pick_first_entity 实际为 O(1) 操作。新进程加入时通过 __enqueue_entity 插入树中,时钟中断后若 vruntime 变化则可能需要 __dequeue_entity 和重新插入。
2.3 新进程的 vruntime 初始化
fork 出的新进程 vruntime 初始化为当前 cfs_rq 的 min_vruntime,这防止了"新进程饿死老进程"的问题(如果从 0 开始,新进程会迅速抢占 CPU)。同时也防止了通过网络 fork(如某些迁移场景)把远处进程的过高 vruntime 带来,导致长期不公平。
3. 调度类体系:多级队列与优先级
Linux 调度器不是单一算法,而是通过调度类(sched_class)实现的多级优先级结构。调度类按优先级从高到低排列:
// 内核源码:include/linux/sched.h
extern const struct sched_class stop_sched_class; // 最高优先级(停机)
extern const struct sched_class dl_sched_class; // SCHED_DEADLINE
extern const struct sched_class rt_sched_class; // SCHED_FIFO / SCHED_RR
extern const struct sched_class fair_sched_class; // SCHED_NORMAL / SCHED_BATCH / SCHED_IDLE
extern const struct sched_class idle_sched_class; // 最低优先级(空闲)
调度时从 stop → dl → rt → fair → idle 逐级查找,第一个非空的调度类胜出。这意味着:
- stop 类用于 CPU 热插拔、IPI 等不可被抢占的操作。
- dl 类的 SCHED_DEADLINE 使用全局 EDF 算法,保证
(runtime, deadline, period)三元组的硬实时约束。 - rt 类的 SCHED_FIFO(无时间片,直到主动让出)和 SCHED_RR(带时间片轮转)用于软实时任务。
- fair 类涵盖普通进程(SCHED_NORMAL)、批处理进程(SCHED_BATCH)和空闲进程(SCHED_IDLE)。
- idle 类只在完全没有其他可运行进程时执行 idle 线程。
3.1 SCHED_DEADLINE——硬实时保障
SCHED_DEADLINE 基于 Constant Bandwidth Server(CBS)算法,使用 EDF + 带宽隔离。用户通过 sched_setattr() 设置 sched_runtime、sched_deadline 和 sched_period:
// 设置一个 SCHED_DEADLINE 任务的 C 示例
#define _GNU_SOURCE
#include
#include
struct sched_attr attr = {
.size = sizeof(attr),
.sched_policy = SCHED_DEADLINE,
.sched_runtime = 30 * 1000 * 1000, // 30ms runtime
.sched_deadline = 50 * 1000 * 1000, // 50ms deadline
.sched_period = 50 * 1000 * 1000, // 50ms period
};
sched_setattr(0, &attr, 0);
内核的准入条件为:Σ(runtime_i / period_i) ≤ 1(多核上为 ≤ 核数 × capacity)。这意味着在保证 100% 单核带宽的前提下,最多只能容纳一个 (30ms/50ms) = 60% 带宽的任务,剩余 40% 可供 CFS 进程使用。
3.2 实时优先级范围
SCHED_FIFO 和 SCHED_RR 的优先级范围是 1-99(1 最低,99 最高)。需要注意的是,RT 进程可以无条件抢占 CFS 进程,甚至 root 权限的 SCHED_FIFO 优先级 99 进程可能会导致系统被锁死(因为连 shell 都无法运行)。因此在生产环境中必须配合 /etc/security/limits.conf 中的 rtprio 限制使用。
4. EEVDF:CFS 的革命性替代
4.1 CFS 的局限性
尽管 CFS 在大多数场景下表现优秀,但存在几个根本性问题:
延迟波动:当高权重进程从睡眠中醒来(wakeup),可能因为 vruntime 显著小于 min_vruntime 而长时间抢占低权重进程,导致延迟敏感应用的抖动。虽然 CFS 引入了 wakeup_granularity 等参数缓解,但根本问题在于 vruntime 与时间缺乏显式映射。
唤醒抢占限制:sched_wakeup_preempt 逻辑中,只有当新唤醒进程的 vruntime 减去当前进程的 vruntime 大于 sysctl_sched_wakeup_granularity(默认 1ms)时才允许抢占。这在高负载时会导致微小的"调度时延"。

发表评论 取消回复