CFS完全公平调度器:Linux内核进程调度深度解析
引言
进程调度是操作系统最核心的功能之一,它决定了哪个进程在何时获得CPU时间。从早期的O(n)调度器,到O(1)调度器,再到如今广泛使用的CFS(Completely Fair Scheduler,完全公平调度器),Linux内核的调度器经历了巨大的演进。本文将深入解析CFS的设计思想、核心数据结构与实现原理。
一、进程调度的基本概念
进程调度器的目标是在多个可运行进程之间公平地分配CPU时间。一个理想的调度器应该满足以下条件:
- 公平性:每个进程应该获得与其权重成比例的CPU时间
- 低延迟:进程切换应尽可能快,减少上下文切换开销
- 高吞吐:系统整体吞吐量最大化
- 响应性:交互式进程应获得快速响应
二、历史调度器的问题
在CFS之前,Linux使用了两种主要的调度器:
O(n)调度器(2.4内核):每次选择进程时需要遍历所有可运行进程,时间复杂度为O(n)。当进程数量增加时,调度开销线性增长,不适合高负载场景。
O(1)调度器(2.6.0 - 2.6.22):引入了运行队列和优先级数组,将时间复杂度降低到O(1)。但其复杂的交互检测算法和基于时间片的分配机制,在处理大量交互式进程时仍然存在问题。
三、CFS的设计哲学
CFS由Ingo Molnar在2007年提出(2.6.23内核引入),其核心设计理念是:模拟理想多任务处理器。
在理想多任务处理器上,每个进程都能获得1/N的CPU时间(N为可运行进程数)。CFS的目标不是直接分配时间片,而是追踪每个进程的虚拟运行时间(vruntime),总是选择vruntime最小的进程运行。
关键特性:
- 不再使用时间片概念,而是基于虚拟运行时间
- 使用红黑树(RB-Tree)组织可运行进程,O(log n)查找
- 支持基于权重的公平分配
- 天然支持NUMA负载均衡
四、核心数据结构
4.1 调度实体(sched_entity)
每个进程或调度组都有一个调度实体,其中关键字段包括:
struct sched_entity {
struct load_weight load; // 权重
struct rb_node run_node; // 红黑树节点
u64 vruntime; // 虚拟运行时间
u64 exec_start; // 本次执行开始时间
u64 sum_exec_runtime; // 总实际运行时间
};
4.2 运行队列(cfs_rq)
struct cfs_rq {
struct load_weight load; // 总权重
unsigned int nr_running; // 可运行进程数
u64 min_vruntime; // 最小虚拟运行时间(基准值)
struct rb_root tasks_timeline; // 红黑树根节点
struct rb_node *rb_leftmost; // 最左节点(vruntime最小的进程)
};
五、虚拟运行时间的计算
vruntime是CFS的核心概念,其计算公式为:
vruntime += delta_exec * (NICE_0_LOAD / weight)
其中:
- delta_exec:进程实际运行的物理时间
- NICE_0_LOAD:nice值0对应的权重(1024)
- weight:进程的权重(由nice值决定)
重要推论:
- nice值越低(优先级越高)的进程,权重越大,vruntime增长越慢
- nice值越高(优先级越低)的进程,权重越小,vruntime增长越快
- 当高优先级进程的vruntime逐渐接近低优先级进程时,CFS会自然进行抢占
六、调度流程详解
6.1 进程入队(enqueue_entity)
- 将进程的sched_entity插入红黑树
- 更新cfs_rq的nr_running计数
- 如果新进程的vruntime小于当前最左节点,更新rb_leftmost指针
- 触发检查是否需要抢占当前进程
6.2 进程出队(dequeue_entity)
- 从红黑树中移除进程
- 如果移除的是最左节点,重新查找新的最左节点
- 更新cfs_rq统计信息
6.3 选择下一个进程(pick_next_entity)
CFS总是选择红黑树最左节点(vruntime最小的进程),时间复杂度O(1)。如果红黑树为空,返回NULL。
6.4 检查抢占(check_preempt_tick)
当前进程运行超过__sched_period(调度周期)除以可运行进程数的时间后,会被抢占。这确保了每个进程在一个周期内至少运行一次。
七、调度周期与最小粒度
CFS定义了两个重要的可调整参数:
- sched_latency:调度周期(默认6ms),目标是在这个周期内让所有进程都运行一次
- min_granularity:最小时间片(默认0.75ms),防止进程切换过于频繁
计算公式:
sched_period = max(sched_latency, nr_running * min_granularity)
当进程数很少时,使用sched_latency;当进程数很多时,保证每个进程至少获得min_granularity时间片。
八、组调度(Group Scheduling)
CFS支持组调度,允许对一组进程进行整体带宽控制。这在多用户CPU配额控制中非常有用:
- 每个用户有自己的调度组
- 组内进程公平分配组获得的CPU份额
- 通过cgroup的cpu子系统可以限制组的CPU使用率
九、NUMA感知调度
现代多核系统通常具有NUMA(非统一内存访问)架构。CFS的NUMA感知特性包括:
- 进程迁移时考虑内存本地性
- 负载均衡时优先迁移到本地NUMA节点
- 通过sched_numa_balancing参数控制
十、实际调优案例
10.1 调整进程优先级
# 设置进程的nice值(-20到19,-20最高)
nice -n -10 ./my_program
# 动态调整运行中进程的优先级
renice -n 5 -p 12345
10.2 使用cgroups限制CPU
# 创建cgroup并限制CPU配额
mkdir /sys/fs/cgroup/cpu/myapp
echo 50000 > /sys/fs/cgroup/cpu/myapp/cpu.cfs_quota_us
echo 100000 > /sys/fs/cgroup/cpu/myapp/cpu.cfs_period_us
echo $$ > /sys/fs/cgroup/cpu/myapp/cgroup.procs
10.3 内核参数调优
# 查看当前调度器参数
cat /proc/sys/kernel/sched_latency_ns
cat /proc/sys/kernel/sched_min_granularity_ns
# 调整调度延迟(低延迟桌面环境)
sysctl kernel.sched_latency_ns=4000000
十一、CFS的演进
从2.6.23引入至今,CFS经历了多次重要改进:
- 2.6.24:引入组调度支持
- 2.6.38:改进autorun机制,减少唤醒延迟
- 3.14:NUMA-balancing自动负载均衡
- 5.x:EEVDF调度器准备(Earliest Eligible Virtual Deadline First)
十二、总结
CFS通过引入虚拟运行时间的概念,以简洁优雅的方式解决了多进程公平调度的问题。其核心优势包括:
- 算法简洁,代码量相比O(1)调度器减少数百行
- 公平性保障,不会出现进程饥饿
- 支持多种调度策略(SCHED_NORMAL/SCHED_BATCH/SCHED_IDLE)
- 天然支持组调度和带宽控制
- NUMA感知,适配现代硬件架构
理解CFS对于系统管理员、内核开发者和性能优化工程师都是非常有价值的。
参考资料
- Linux内核源码:kernel/sched/fair.c
- Ingo Molnar, "CFS: Complete Fair Scheduler," Linux Kernel Mailing List, 2007
- Robert Love, "Linux Kernel Development," 3rd Edition
- 内核文档:Documentation/scheduler/sched-design-CFS.txt

发表评论 取消回复