1. CFS 算法概述:完全公平调度器设计哲学、红黑树数据结构基础、虚拟运行时间概念
1.1 完全公平调度器(CFS)的设计哲学
CFS(Completely Fair Scheduler)是 Linux 内核 2.6.23 版本引入的默认进程调度器,由 Ingo Molnár 设计。其核心设计哲学可概括为:“模拟理想多任务处理器上的完美公平调度”。
在理想多任务处理器中,假设有 N 个可运行进程,每个进程应获得 1/N 的 CPU 时间。CFS 不采用传统的时间片轮转(Round-Robin)或优先级队列,而是通过虚拟运行时间(vruntime)来度量每个进程已获得的 CPU 时间,并始终选择 vruntime 最小的进程来运行。
设计目标:
- 公平性:确保每个进程获得与其权重成比例的 CPU 时间。
- 低延迟:交互式进程能快速响应。
- 可扩展性:支持大量进程(数千个)时仍保持高效。
- 无饥饿:所有进程最终都能获得 CPU 时间。
与传统 O(1) 调度器的区别:O(1) 调度器依赖固定时间片和优先级数组,而 CFS 使用红黑树动态维护进程顺序,时间分配由权重和系统负载动态决定,无需固定时间片。
1.2 红黑树数据结构基础
CFS 使用红黑树(Red-Black Tree)作为核心数据结构来组织可运行进程。红黑树是一种自平衡二叉查找树,具有以下关键特性:
- 平衡性:任何路径上的黑色节点数相同,确保树的高度不超过 2*log₂(n+1)。
- 查找效率:查找、插入、删除操作的时间复杂度均为 O(log n)。
- 有序性:中序遍历可得到按 vruntime 排序的进程序列。
在 CFS 中,红黑树的键值是进程的 vruntime。树的最左节点(vruntime 最小)即为下一个要运行的进程。
1.3 虚拟运行时间(vruntime)概念
虚拟运行时间(vruntime)是 CFS 实现公平调度的核心度量。它表示进程在理想多任务处理器上应获得的 CPU 时间,而非实际物理时间。
vruntime 的计算公式如下:
vruntime += delta_exec * (NICE_0_LOAD / se->load.weight)
其中:
- delta_exec:进程实际运行的物理时间。
- NICE_0_LOAD:nice 值为 0 时的默认权重(1024)。
- se->load.weight:当前进程的调度实体权重,由 nice 值决定。
关键理解:权重越高的进程(nice 值越低),其 vruntime 增长越慢,因此更容易被调度器选中,获得更多 CPU 时间。权重越低的进程(nice 值越高),vruntime 增长越快,CPU 时间分配减少。
| nice 值 | 权重(weight) | vruntime 增长速率(相对 nice=0) |
|---|---|---|
| -20 | 88761 | 极慢(约 1.15%) |
| -10 | 11051 | 慢(约 9.27%) |
| 0 | 1024 | 基准(100%) |
| 10 | 335 | 快(约 305%) |
| 19 | 15 | 极快(约 6827%) |
1.4 CFS 调度流程与 vruntime 更新
CFS 的调度循环可概括为以下步骤:
- 选择下一个进程:从红黑树中取出最左节点(vruntime 最小)。
- 执行进程:将 CPU 分配给该进程运行。
- 更新 vruntime:在进程运行期间(通过 tick 中断)或进程主动让出 CPU 时,更新其 vruntime。
- 重新插入红黑树:如果进程未退出,将其重新插入红黑树(或更新其键值)。
- 重复步骤 1。
注意:CFS 并不依赖固定的时间片长度。进程可以运行任意短的时间,只要其 vruntime 仍然是最小值。当有更高优先级(权重)的进程就绪时,当前进程可能被抢占。这种设计使得交互式进程能获得极低的调度延迟。
1.5 关键数据结构与内核代码片段
在 Linux 内核中,CFS 的核心数据结构定义在 kernel/sched/fair.c 和 include/linux/sched.h 中。以下为关键结构:
// 调度实体(sched_entity),每个进程对应一个
struct sched_entity {
struct load_weight load; // 进程权重
struct rb_node run_node; // 红黑树节点
struct list_head group_node; // 组调度链表节点
unsigned int on_rq; // 是否在就绪队列中
u64 vruntime; // 虚拟运行时间
u64 prev_sum_exec_runtime; // 上次调度时的总执行时间
// ... 其他字段
};
// CFS 就绪队列(cfs_rq)
struct cfs_rq {
struct load_weight load; // 就绪队列总权重
unsigned int nr_running; // 可运行进程数
u64 min_vruntime; // 最小虚拟运行时间
struct rb_root_cached tasks_timeline; // 红黑树根节点(含缓存)
struct sched_entity *curr; // 当前正在运行的进程
struct sched_entity *next; // 下一个要运行的进程
// ... 其他字段
};
min_vruntime 的作用:CFS 维护一个全局的 min_vruntime 值,用于新进程或休眠后唤醒的进程初始化 vruntime,防止它们因 vruntime 过小(或过大)而长时间占用(或无法获得)CPU。
CFS 选择下一个进程的核心函数是 pick_next_entity(),其逻辑可简化为:
static struct sched_entity *pick_next_entity(struct cfs_rq *cfs_rq)
{
struct rb_node *left = rb_first_cached(&cfs_rq->tasks_timeline);
if (!left)
return NULL;
return rb_entry(left, struct sched_entity, run_node);
}
该函数通过 rb_first_cached() 直接获取红黑树最左节点(缓存了 leftmost 指针),时间复杂度为 O(1)。
1.6 本章小结
| 概念 | 说明 | 关键作用 |
|---|---|---|
| 设计哲学 | 模拟理想多任务处理器,按权重公平分配 CPU | 消除固定时间片,实现动态公平 |
| 红黑树 | 自平衡二叉查找树,按 vruntime 排序 | O(log n) 插入/删除,O(1) 获取最左节点 |
| vruntime | 虚拟运行时间,度量进程已获得的 CPU 份额 | 调度决策的唯一依据 |
| 权重 | 由 nice 值决定,影响 vruntime 增长速率 | 实现优先级差异化 |