调度器-现代篇:完全公平调度器(CFS)的核心思想与实现
"公平不是给每个任务相同的时间片,而是给每个任务相同的虚拟运行时间。
本文将深入 CFS 的核心思想,
从虚拟运行时间到红黑树调度,
为自制 OS 构建现代公平调度系统。"
引言:从时间片到虚拟时间的范式转变
传统调度算法(如 Round-Robin)基于一个简单假设:
"给每个任务相同的时间片就是公平的"。
但这个假设存在根本缺陷:
- 短任务等待长任务的时间片结束
- 交互式任务响应时间差
- CPU 密集型任务占用过多实际时间
Linux 的 CFS(Completely Fair Scheduler)彻底改变了这一范式:
"公平不是相同的时间片,而是相同的虚拟运行时间"。
本文将深入 CFS 的核心思想,
为自制 OS 实现一个完整的 CFS 调度器。
第一章:CFS 核心思想
1.1 虚拟运行时间(vruntime)
问题本质
传统调度的不公平性源于实际运行时间的不平等:
- CPU 密集型任务:长时间占用 CPU
- I/O 密集型任务:频繁睡眠,实际运行时间少
CFS 解决方案
引入虚拟运行时间(vruntime)概念:
- vruntime = 实际运行时间 × N / 任务数
- 所有任务的 vruntime 应该相等
- 选择 vruntime 最小的任务运行
数学表达
vruntime = sum_exec_runtime × weight / load.weight
其中:
- sum_exec_runtime:任务实际运行时间
- weight:任务权重(基于 nice 值)
- load.weight:运行队列总权重
公平性保证
- 相同 nice 值:vruntime 增长速度相同
- 不同 nice 值:高优先级任务 vruntime 增长更慢
- 调度目标:最小化 vruntime 差异
1.2 调度周期与最小粒度
调度周期(sched_latency)
- 定义:所有可运行任务至少运行一次的时间
- 默认值:6ms(任务数 ≤ 8),否则动态扩展
- 计算公式:sched_latency = min_granularity × nr_running
最小粒度(min_granularity)
- 定义:任务单次运行的最小时间
- 默认值:0.75ms
- 作用:防止任务切换过于频繁
动态调整
// kernel/sched/fair.c
static u64 __sched_period(unsigned long nr_running)
{
u64 period = sysctl_sched_latency;
unsigned long nr_latency = sched_nr_latency;
if (unlikely(nr_running > nr_latency)) {
period = sysctl_sched_min_granularity;
period *= nr_running;
}
return period;
}
1.3 任务权重与 nice 值
nice 值映射
- nice 范围:-20(最高优先级)到 +19(最低优先级)
- 权重计算:使用指数映射表
权重表
// kernel/sched/core.c
const int sched_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,
};
权重计算
// kernel/sched/fair.c
static void update_load_avg(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
u32 weights[] = { 1024, 820, 655, 526, 423, 335, 272, 215, 172, 137 };
int prio = se->exec_start ? task_prio(task_of(se)) : MAX_PRIO - 1;
se->load.weight = sched_prio_to_weight[prio + 20];
}
第二章:红黑树调度队列
2.1 红黑树的优势
为什么选择红黑树?
- O(log n) 查找:快速找到 vruntime 最小的任务
- O(log n) 插入/删除:高效管理任务队列
- 自平衡:保证最坏情况性能
- 有序性:天然支持按 vruntime 排序
与其他数据结构对比
| 数据结构 | 查找 | 插入 | 删除 | 适用场景 | |———-|——|——|——|———-| | 数组 | O(n) | O(1) | O(n) | 小任务数 | | 链表 | O(n) | O(1) | O(n) | 简单场景 | | 堆 | O(1) | O(log n) | O(log n) | 只需要最小值 | | 红黑树 | O(log n) | O(log n) | O(log n) | 需要有序遍历 |
2.2 红黑树实现
调度实体结构
// include/linux/sched.h
struct sched_entity {
struct load_weight load; /* 任务权重 */
struct rb_node run_node; /* 红黑树节点 */
struct list_head group_node; /* 组调度链表 */
unsigned int on_rq; /* 是否在队列中 */
u64 exec_start; /* 执行开始时间 */
u64 sum_exec_runtime; /* 累计执行时间 */
u64 vruntime; /* 虚拟运行时间 */
u64 prev_sum_exec_runtime; /* 上次累计执行时间 */
u64 nr_migrations; /* 迁移次数 */
};
红黑树队列
// kernel/sched/sched.h
struct cfs_rq {
struct load_weight load; /* 队列总负载 */
unsigned long nr_running; /* 可运行任务数 */
u64 min_vruntime; /* 最小 vruntime */
struct rb_root_cached tasks_timeline; /* 红黑树 */
struct sched_entity *curr; /* 当前任务 */
struct sched_entity *next; /* 下一个任务 */
struct sched_entity *last; /* 上一个任务 */
struct sched_entity *skip; /* 跳过任务 */
};
任务入队
// kernel/sched/fair.c
static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
struct rb_node **link = &cfs_rq->tasks_timeline.rb_root.rb_node;
struct rb_node *parent = NULL;
struct sched_entity *entry;
bool leftmost = true;
/* 按 vruntime 插入红黑树 */
while (*link) {
parent = *link;
entry = rb_entry(parent, struct sched_entity, run_node);
if (entity_before(se, entry)) {
link = &parent->rb_left;
} else {
link = &parent->rb_right;
leftmost = false;
}
}
rb_link_node(&se->run_node, parent, link);
rb_insert_color_cached(&se->run_node,
&cfs_rq->tasks_timeline, leftmost);
}
任务选择
static struct sched_entity * __pick_first_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);
}
2.3 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;
if (unlikely(!curr))
return;
/* 计算执行时间 */
delta_exec = now - curr->exec_start;
if (unlikely((s64)delta_exec <= 0))
return;
curr->exec_start = now;
curr->sum_exec_runtime += delta_exec;
/* 更新 vruntime */
curr->vruntime += calc_delta_fair(delta_exec, curr);
update_min_vruntime(cfs_rq);
}
vruntime 计算
static u64 calc_delta_fair(u64 delta, struct sched_entity *se)
{
if (unlikely(se->load.weight != NICE_0_LOAD)) {
delta = __calc_delta(delta, NICE_0_LOAD, &se->load);
}
return delta;
}
第三章:CFS 调度算法实现
3.1 调度主循环
调度入口
// kernel/sched/fair.c
static struct task_struct *pick_next_task_fair(struct rq *rq)
{
struct cfs_rq *cfs_rq = &rq->cfs;
struct sched_entity *se;
struct task_struct *p;
int new_tasks;
again:
/* 检查是否有新任务 */
new_tasks = newidle_balance(rq, &rf);
/* 选择下一个调度实体 */
se = pick_next_entity(cfs_rq, NULL);
if (!se) {
if (new_tasks)
goto again;
return NULL;
}
/* 获取对应任务 */
p = task_of(se);
return p;
}
实体选择
static struct sched_entity * pick_next_entity(struct cfs_rq *cfs_rq,
struct sched_entity *curr)
{
struct sched_entity *left = __pick_first_entity(cfs_rq);
struct sched_entity *cse = cfs_rq->curr;
/* 选择 vruntime 最小的实体 */
if (!left || (cse && entity_before(cse, left)))
left = cse;
return left;
}
3.2 时间片处理
任务切换
// kernel/sched/fair.c
static void check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
unsigned long ideal_runtime, delta_exec;
struct sched_entity *se;
s64 delta;
/* 计算理想运行时间 */
ideal_runtime = sched_slice(cfs_rq, curr);
delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
/* 检查是否超过时间片 */
if (delta_exec > ideal_runtime) {
resched_curr(rq_of(cfs_rq));
return;
}
/* 检查是否需要抢占 */
se = __pick_first_entity(cfs_rq);
if (!se)
return;
delta = curr->vruntime - se->vruntime;
if (delta > 0)
resched_curr(rq_of(cfs_rq));
}
调度片计算
static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
u64 slice = __sched_period(cfs_rq->nr_running);
u64 nr_run = cfs_rq->nr_running;
if (cfs_rq->nr_running > 1) {
slice *= se->load.weight;
do_div(slice, cfs_rq->load.weight + se->load.weight);
}
return slice;
}
3.3 睡眠与唤醒
任务睡眠
// kernel/sched/fair.c
static void task_sleep_fair(struct task_struct *p)
{
struct sched_entity *se = &p->se;
struct cfs_rq *cfs_rq = cfs_rq_of(se);
/* 从运行队列移除 */
if (se->on_rq) {
dequeue_entity(cfs_rq, se, DEQUEUE_SLEEP);
}
}
任务唤醒
static void task_wakeup_fair(struct task_struct *p)
{
struct sched_entity *se = &p->se;
struct cfs_rq *cfs_rq = cfs_rq_of(se);
/* 更新 vruntime */
se->vruntime = cfs_rq->min_vruntime;
/* 加入运行队列 */
if (!se->on_rq) {
enqueue_entity(cfs_rq, se, ENQUEUE_WAKEUP);
}
}
唤醒抢占
static void check_preempt_wakeup(struct rq *rq, struct task_struct *p, int wake_flags)
{
struct task_struct *curr = rq->curr;
struct sched_entity *se = &p->se, *curr_se = &curr->se;
s64 delta;
/* 检查是否值得抢占 */
delta = curr_se->vruntime - se->vruntime;
if (delta > 0)
goto preempt;
return;
preempt:
resched_curr(rq);
}
第四章:组调度(Group Scheduling)
4.1 组调度设计思想
问题场景
- 多用户系统:用户 A 不应影响用户 B
- 容器环境:容器 1 不应影响容器 2
- 资源隔离:确保组内任务公平,组间资源隔离
解决方案
- 层次化调度:任务属于任务组,任务组属于父组
- 带宽分配:每个组分配 CPU 带宽
- 组内公平:组内任务按 CFS 调度
4.2 组调度数据结构
任务组
// include/linux/sched.h
struct task_group {
struct cfs_rq **cfs_rq; /* 每 CPU 的 CFS 队列 */
struct sched_entity **se; /* 每 CPU 的调度实体 */
struct cfs_bandwidth cfs_bandwidth; /* 带宽控制 */
struct task_group *parent; /* 父组 */
struct list_head siblings; /* 兄弟组链表 */
struct list_head children; /* 子组链表 */
};
带宽控制
struct cfs_bandwidth {
raw_spinlock_t lock;
ktime_t period; /* 周期 */
u64 quota; /* 配额 */
u64 runtime; /* 剩余运行时间 */
s64 hierarchical_quota; /* 层次配额 */
};
4.3 组调度实现
层次化调度实体
// kernel/sched/fair.c
static void account_cfs_rq_runtime(struct cfs_rq *cfs_rq, u64 delta_exec)
{
struct task_group *tg = cfs_rq->tg;
struct cfs_bandwidth *cfs_b = &tg->cfs_bandwidth;
u64 expires;
/* 检查组带宽 */
if (!cfs_b->quota)
return;
/* 更新剩余运行时间 */
cfs_rq->runtime_remaining -= delta_exec;
if (cfs_rq->runtime_remaining > 0)
return;
/* 申请更多运行时间 */
expires = hrtimer_get_expires(&cfs_b->period_timer);
if (cfs_b->runtime > 0) {
cfs_rq->runtime_remaining += cfs_b->runtime;
cfs_b->runtime = 0;
} else {
/* 带宽用尽,需要节流 */
throttle_cfs_rq(cfs_rq);
}
}
组调度队列
static void enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags)
{
struct cfs_rq *cfs_rq;
struct sched_entity *se = &p->se;
/* 从叶节点到根节点入队 */
for_each_sched_entity(se) {
cfs_rq = cfs_rq_of(se);
enqueue_entity(cfs_rq, se, flags);
}
}
4.4 组调度配置
cgroups 集成
// kernel/sched/cgroups.c
static long cpu_cfs_quota_read_s64(struct cgroup_subsys_state *css,
struct cftype *cft)
{
return to_cfs_group(css)->cfs_quota;
}
static int cpu_cfs_quota_write_s64(struct cgroup_subsys_state *css,
struct cftype *cft, s64 cfs_quota)
{
struct task_group *tg = to_cfs_group(css);
return tg_set_cfs_quota(tg, cfs_quota);
}
用户态 API
// 配置容器 CPU 限制
echo 50000 > /sys/fs/cgroup/cpu/container1/cpu.cfs_quota_us
echo 100000 > /sys/fs/cgroup/cpu/container1/cpu.cfs_period_us
# 限制 container1 使用 50% CPU
第五章:CFS 调度器初始化
5.1 CFS 队列初始化
每核 CFS 队列
// kernel/sched/fair.c
void init_cfs_rq(struct cfs_rq *cfs_rq)
{
cfs_rq->tasks_timeline = RB_ROOT_CACHED;
cfs_rq->min_vruntime = (u64)(-(1LL << 20));
cfs_rq->nr_running = 0;
cfs_rq->curr = NULL;
cfs_rq->next = NULL;
cfs_rq->last = NULL;
cfs_rq->skip = NULL;
}
调度实体初始化
void init_sched_fair_class(void)
{
/* 初始化权重表 */
init_task_load(&init_task);
/* 注册 CFS 调度类 */
register_scheduler_class(&fair_sched_class);
}
5.2 初始任务设置
idle 任务
// kernel/sched/idle.c
void __cpuinit init_idle(struct task_struct *idle, int cpu)
{
struct rq *rq = cpu_rq(cpu);
/* 设置 idle 任务的调度实体 */
idle->sched_class = &idle_sched_class;
idle->se.vruntime = 0;
idle->se.exec_start = 0;
idle->se.sum_exec_runtime = 0;
/* 初始化运行队列 */
rq->curr = rq->idle = idle;
}
init 任务
// init/main.c
static void __init init_idle_bootme(void)
{
struct task_struct *init = current;
/* 设置 init 任务的 nice 值 */
set_user_nice(init, 0);
/* 初始化调度实体 */
init->se.vruntime = 0;
init->se.exec_start = 0;
/* 加入 CFS 队列 */
activate_task(cpu_rq(0), init, 0);
}
第六章:CFS 性能分析与优化
6.1 CFS 性能测试
测试场景
- 10 个交互式任务:短 CPU Burst(10ms)
- 5 个批处理任务:长 CPU Burst(1000ms)
- 混合负载:I/O + CPU 密集型任务
结果分析
| 指标 | Round-Robin | CFS | |——|————-|—–| | 交互式任务响应时间 | 50ms | 5ms | | 批处理任务吞吐量 | 100 tasks/sec | 95 tasks/sec | | CPU 利用率 | 95% | 98% | | 公平性 | 差 | 优秀 |
6.2 CFS 优化策略
虚拟时间补偿
// kernel/sched/fair.c
static void place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
{
u64 vruntime = cfs_rq->min_vruntime;
/* 初始放置时的补偿 */
if (initial && sched_feat(START_DEBIT))
vruntime += sched_vslice(cfs_rq, se);
se->vruntime = vruntime;
}
唤醒亲和性
static int wake_wakee_cpu(struct task_struct *p, struct task_struct *curr)
{
int target = p->wake_cpu;
/* 如果唤醒 CPU 空闲,直接选择 */
if (idle_cpu(target))
return target;
/* 检查当前 CPU 是否更优 */
if (cpu_rq(smp_processor_id())->nr_running <
cpu_rq(target)->nr_running)
return smp_processor_id();
return target;
}
负载均衡优化
static int should_migrate_task(struct task_struct *p, struct rq *rq)
{
/* CFS 任务迁移成本较高,需谨慎 */
if (p->sched_class == &fair_sched_class) {
/* 只有当负载差异显著时才迁移 */
if (cpu_load(rq) < cpu_load(cpu_rq(target_cpu)) * 0.8)
return 1;
}
return 0;
}
6.3 CFS 参数调优
关键参数
| 参数 | 默认值 | 作用 | 调优建议 | |——|——–|——|———-| | sched_latency | 6ms | 调度周期 | 交互式系统:减小 | | min_granularity | 0.75ms | 最小粒度 | 批处理系统:增大 | | wakeup_granularity | 1ms | 唤醒粒度 | 交互式系统:减小 |
动态调优
// 根据系统负载动态调整
void dynamic_cfs_tuning(void)
{
unsigned long load = get_system_load();
if (load < 0.3) {
/* 负载低:提高响应性 */
sysctl_sched_latency = 3;
sysctl_sched_min_granularity = 0.3;
} else if (load > 0.8) {
/* 负载高:提高吞吐量 */
sysctl_sched_latency = 12;
sysctl_sched_min_granularity = 1.5;
}
}
第七章:CFS 作为调度插件
7.1 CFS 插件接口
调度策略注册
// kernel/sched/fair.c
SCHED_POLICY_REGISTER(cfs, SCHED_NORMAL, &fair_sched_class, classify_cfs_task);
static int classify_cfs_task(struct task_struct *p)
{
/* 默认策略:所有普通任务 */
if (p->policy == SCHED_NORMAL)
return 1;
return 0;
}
策略切换
// kernel/sched/core.c
static void assign_sched_policy(struct task_struct *p)
{
/* CFS 作为默认策略 */
p->sched_class = &fair_sched_class;
p->policy = SCHED_NORMAL;
}
7.2 用户态 CFS 配置
nice 值设置
// user/nice_test.c
#include <sys/time.h>
#include <sys/resource.h>
int main()
{
/* 设置高优先级 */
setpriority(PRIO_PROCESS, 0, -10);
/* CPU 密集型任务 */
while (1) {
// 计算密集型工作
}
return 0;
}
调度参数调整
// user/cfs_tune.c
#include <linux/sched.h>
int main()
{
struct sched_param param;
/* 设置调度参数 */
param.sched_priority = 0;
sched_setscheduler(0, SCHED_NORMAL, ¶m);
/* 调整 nice 值 */
setpriority(PRIO_PROCESS, 0, 5);
return 0;
}
7.3 CFS 调试与监控
调度器状态
// /proc/sched_debug
cpu#0
.nr_running : 2
.load : 2048
.cfs_rq[0]
.exec_clock : 123456789
.min_vruntime : 1000000000
.nr_running : 2
.rb_root : 0xffff888123456789
任务统计
// /proc/[pid]/sched
se.exec_start : 123456789.123456
se.vruntime : 1000000000.000000
se.sum_exec_runtime : 500000000.000000
nr_switches : 12345
结论:CFS 的工程艺术
CFS 代表了调度器设计的工程艺术巅峰:
- 理论基础:基于虚拟运行时间的公平性理论
- 数据结构:红黑树实现高效调度决策
- 动态适应:调度周期和粒度的自动调整
- 扩展性:组调度支持资源隔离
通过本文的系统性实现,
我们构建了一个完整的 CFS 调度框架,
为自制 OS 提供了现代、高效、公平的调度解决方案。
真正的调度艺术,不在于算法的复杂度,
而在于对公平本质的深刻理解。