引言
在现代计算系统中,进程调度器是操作系统的核心组件之一,它决定了哪个进程在何时获得 CPU 时间片。Linux 内核自 2.6.23 版本起引入的 CFS(Completely Fair Scheduler,完全公平调度器)彻底改变了 Linux 的调度设计理念。CFS 不再基于传统的时间片轮转算法,而是基于一个简洁而优雅的概念:虚拟运行时间(vruntime),通过红黑树数据结构实现 O(log n) 复杂度的高效调度决策。
1. CFS 的核心设计理念
CFS 的核心思想可以用一句话概括:让所有可运行进程的虚拟运行时间尽可能相等。这个"完全公平"的理念基于以下几个关键原则:
1.1 虚拟运行时间(vruntime)
每个进程维护一个 vruntime(virtual runtime)字段,表示该进程经过权重归一化后的实际运行时间。计算公式如下:
delta_vruntime = (delta_exec * NICE_0_LOAD) / weight
其中:
delta_exec:进程实际执行的物理时间NICE_0_LOAD:nice 值为 0 时的权重基准(1024)weight:进程的调度权重,由 nice 值决定
优先级高的进程(nice 值更低)拥有更大的权重,因此 vruntime 增长更慢,从而获得更多的 CPU 时间比例。
1.2 红黑树作为调度队列
CFS 使用红黑树(Red-Black Tree)来组织所有可运行进程,以 vruntime 作为排序键。最左侧节点即为 vruntime 最小的进程——也就是最"亏待"的进程,它将被选为下一个运行进程。这种设计带来了以下优势:
- O(log n) 时间复杂度的进程插入和删除
- O(1) 时间复杂度获取下一个待调度进程(最左节点缓存在
rb_leftmost) - 天然支持范围查询和高效更新
2. 权重与优先级体系
CFS 通过 nice 值到权重的映射来实现优先级控制。Linux 内核使用 sched_prio_to_weight 数组定义了 40 个优先级等级(nice 值 -20 到 19)对应的权重:
static const int prio_to_weight[40] = {
/* -20 */ 88761, 71755, 56483, 46273, 36291,
/* -15 */ 29154, 23254, 18705, 14949, 11916,
/* -10 */ 9548, 7620, 6100, 4904, 3906,
/* -5 */ 3121, 2501, 1991, 1586, 1277,
/* 0 */ 1024, 820, 655, 526, 423,
/* 5 */ 335, 272, 215, 172, 137,
/* 10 */ 110, 87, 70, 56, 45,
/* 15 */ 36, 29, 23, 18, 15,
};
相邻等级间的权重比例约为 1.25 倍,这意味着每降低一个 nice 值,进程多获得约 25% 的 CPU 时间。
2.1 调度权重计算示例
假设系统中有两个进程:
- 进程 A:nice = 0,weight = 1024
- 进程 B:nice = 5,weight = 335
CPU 时间分配比例约为:1024 : 335 ≈ 3.06 : 1,即进程 A 获得约 75% 的 CPU 时间,进程 B 获得约 25%。这与传统固定时间片轮转相比更加灵活和精确。
3. 调度延迟与粒度控制
CFS 通过多个 sysctl 参数精细控制调度行为:
3.1 关键调度参数
sched_latency(默认 6ms):目标调度延迟,即所有可运行进程轮流运行一遍的目标时间min_granularity(默认 0.75ms):最小粒度,防止进程切换过于频繁sched_min_granularity_ns:进程获得的最小连续执行时间sched_wakeup_granularity_ns:唤醒抢占粒度,控制新唤醒进程是否立即抢占当前进程
3.2 时间片计算
当可运行进程数为 n 时,每个进程的理论时间片为:
time_slice = max(sched_latency / n, min_granularity)
当进程数量很多时(n > sched_latency/min_granularity = 8),每个进程获得 min_granularity 而非按比例分配,避免切换开销过大。
4. 组调度(CGroup 支持)
CFS 天然支持组调度(Group Scheduling),通过 CPU 控制组(cpu cgroup)实现层次化的带宽分配:
4.1 两层调度架构
CFS 实现了两层调度结构:
- 用户组间:按照 cgroup 分配的 CPU 带宽比例进行调度
- 用户组内:按照进程的 nice 值进行比例公平调度
4.2 CFS 带宽控制
通过 cpu.cfs_quota_us 和 cpu.cfs_period_us 参数实现硬性的 CPU 带宽限制:
# 限制容器最多使用 1 个 CPU 核心的 50%
echo 50000 > /sys/fs/cgroup/cpu/mycontainer/cpu.cfs_quota_us
echo 100000 > /sys/fs/cgroup/cpu/mycontainer/cpu.cfs_period_us
当 cgroup 内的进程在 period 内累计使用 CPU 达到 quota 后,throttle 机制会被触发,该组所有进程将被暂停直到下一个 period 开始。
5. NUMA 感知调度
在现代多核 NUMA 架构中,CFS 与 NUMA 平衡机制协同工作:
5.1 NUMA 节点亲和性
进程的内存访问模式会影响调度决策:
- 进程倾向于在其内存所在 NUMA 节点上运行
- 内核的 AutoNUMA 平衡机制会扫描进程内存页访问频率
- 当远端访问开销过大时,内核会将进程迁移到其内存所在节点
5.2 调度域(Sched Domain)
Linux 调度域按硬件拓扑分层组织:
DIE → MC → SMT
SMT 层级处理超线程 sibling,MC 层级处理同一物理核心,DIE 层级处理不同 NUMA 节点。更高层级的负载均衡迁移成本更高,因此优先在低层级平衡。
6. 负载跟踪与带宽估计
CFS 使用 PELT(Per-Entity Load Tracking)算法来跟踪每个调度实体的负载:
6.1 PELT 衰减模型
每个调度实体维护一个负载值,按指数衰减规律更新:
y^n = y^(n-1) * (1 - 1/2^(n/decay)) + active * (1 - y^(n-1)/y^n)
32ms 的半衰期窗口意味着:32ms 前的负载贡献为当前的 50%,64ms 前为 25%。这种指数衰减模型能快速响应负载变化,同时提供稳定的历史负载估计。
6.2 利用率传播
调度实体的负载不仅影响自身,还会向上传播到其所属的 cgroup 和 CPU 运行队列:
- 进程负载 → CPU 运行队列负载 → 调度域负载
- 层级的负载用于负载均衡决策:从高负载域向低负载域迁移进程
7. 内核实现:关键数据结构
7.1 运行队列(cfs_rq)
struct cfs_rq {
struct load_weight load; // 运行队列总权重
unsigned long runnable_weight;
unsigned int nr_running; // 可运行进程数
unsigned int h_nr_running; // 包含组调度的总运行数
u64 exec_clock; // 执行时钟
u64 min_vruntime; // 最小 vruntime 基准
struct rb_root_cached runned_tasks_timeline; // 红黑树根
struct sched_entity *curr; // 当前运行实体
struct sched_entity *next; // 下一个运行实体(用于抢占后恢复)
struct sched_entity *last; // 上一个运行实体(用于上下文切换)
};
7.2 调度实体(sched_entity)
struct sched_entity {
struct load_weight load; // 实体权重
struct rb_node run_node; // 红黑树节点
unsigned int on_rq; // 是否在运行队列上
u64 exec_start; // 开始执行时间
u64 sum_exec_runtime; // 累计实际运行时间
u64 vruntime; // 虚拟运行时间
u64 prev_sum_exec_runtime; // 上次累计执行时间
u64 nr_migrations; // 迁移次数
// ...
};
8. 实践调优与性能优化
8.1 参数调优建议
# 低延迟桌面环境
sysctl kernel.sched_min_granularity_ns=1000000
sysctl kernel.sched_wakeup_granularity_ns=1500000
# 高吞吐量服务器
sysctl kernel.sched_min_granularity_ns=10000000
sysctl kernel.sched_wakeup_granularity_ns=15000000
# HPC / 实时计算
sysctl kernel.sched_min_granularity_ns=100000000
sysctl kernel.sched_latency_ns=500000000
8.2 观测与调试
理解 CFS 行为的最佳途径是通过 procfs 和 debugfs 获取运行时数据:
# 查看进程调度统计
cat /proc/<PID>/sched
# 查看 CFS 运行队列状态
cat /sys/kernel/debug/sched/debug
# 监控调度延迟
perf sched record -- sleep 1
perf sched latency
# 使用 schedtool 查看和设置调度策略
schedtool -v <PID>
8.3 sched_features 控制
可以通过 /proc/sys/kernel/sched_features 文件系统开关调整特定行为:
NO_GENTLE_FAIR_SLEEPERS:禁用对睡眠进程的 vruntime 补偿NO_NEXT_BUDDING:禁用新唤醒进程优先选择逻辑NO_WAKEUP_PREEMPTION:禁用唤醒立即抢占,改为时间片耗尽才进行抢占
9. 实时调度类与 CFS 的协作
Linux 调度器是一个模块化框架,按优先级从高到低依次为:
SCHED_DEADLINE (SCHED_DL) → SCHED_FIFO/SCHED_RR → SCHED_NORMAL (CFS) → SCHED_IDLE
CFS 实际管理 SCHED_NORMAL、SCHED_BATCH 和 SCHED_IDLE 三类进程。SCHED_IDLE 进程的 nice 值被固定映射到极低的权重(3),确保只在 CPU 完全空闲时才运行。
9.1 SCHED_DEADLINE 与 CFS 的交互
SCHED_DEADLINE 基于 EDF(Earliest Deadline First)算法,采用 Constant Bandwidth Server(CBS)机制。当 DEADLINE 任务处于可运行时,CFS 会主动避让。CBS 规则确保 DEADLINE 任务不会超过其预留带宽,同时不影响 CFS 进程的正常执行。
10. 前沿发展与未来方向
10.1 内核 5.x/6.x 的演进
近年来 Linux 调度器的持续改进包括:
- Core Scheduling:缓解 MDS/L1TF 等侧信道攻击,限制同一核心上 SMT sibling 运行不可信进程
- NUMA 平衡改进:AutoNUMA v2 引入周期性内存扫描和智能进程迁移
- 热插拔优化:CPU 热插拔时的负载均衡策略优化,减少服务中断
- EAS(Energy Aware Scheduling):在 ARM big.LITTLE 架构上结合调度与能耗管理
10.2 云原生时代的调度需求
容器密度和微服务架构对调度器提出了新挑战:
- 延迟敏感型 workload:需要在容器级别保证 P99 尾延迟
- CPU 配额与 CFS 带宽的交互:高并发场景下 quota 耗尽导致的不公平问题
- 大规模节点上的调度域优化:减少不必要的跨 NUMA 负载均衡开销
总结
Linux CFS 通过虚拟运行时间和红黑树的优雅组合,在简洁的设计中实现了高效、公平的进程调度。其基于权重的比例公平分配、对 NUMA 拓扑的感知能力、以及与 cgroup 的无缝整合,使其从 2007 年诞生至今仍然是 Linux 内核调度的核心。理解 CFS 的内部机制,能够帮助系统管理员做出更精准的调优决策,也能让开发者编写出更加符合调度器友好的应用程序。
随着异构计算(GPU、DPU、NPU)的普及和调度需求的日益复杂,CFS 的架构仍将持续演进,在"完全公平"的核心理念之上,支撑起下一代计算平台的需求。

发表评论 取消回复