引言
CPU 调度器是操作系统内核中最核心的组件之一,它决定了哪个进程在何时获得 CPU 时间。Linux 内核自 2.6.23 版本引入了 完全公平调度器(Completely Fair Scheduler, CFS),彻底取代了之前 O(1) 调度器,成为 Linux 默认的进程调度类。十余年来,CFS 经历了无数优化与扩展,但其核心设计理念 —— "完美多任务下的公平性" —— 始终未变。
本文将从 CFS 的设计哲学出发,深入剖析其基于虚拟运行时间(vruntime)的红黑树调度模型、组调度(Group Scheduling)与 cgroup 集成、负载均衡机制、NUMA 感知调度,并结合实际案例分析延迟敏感型工作负载的调优策略。
一、设计哲学:完美多任务下的公平性
CFS 的核心思想非常直观:如果有 N 个可运行进程,每个进程应该获得 1/N 的 CPU 时间。在理想硬件(无限精度、无上下文切换开销)上,这意味着每个进程都恰好获得等量的 CPU 时间。这种状态称为"完美多任务"(Ideal Preemptive Multitasking)。
现实世界中,上下文切换有开销,精度有限,但 CFS 尽可能逼近理想状态。与 O(1) 调度器使用时间片(timeslice)不同,CFS 不使用时间片概念,而是通过虚拟运行时间(vruntime) 来追踪每个进程已获得的"有效 CPU 时间"。
关键设计决策:
- 无时间片:进程不在固定时间片结束时被抢占,而是当有更小的 vruntime 进程存在时才被抢占
- 红黑树作为运行队列:按 vruntime 排序的进程集合,最左侧节点(最小 vruntime)即为下一个调度目标
- 粒度控制:通过 sysctl_sched_min_granularity 和 sysctl_sched_wakeup_granularity 控制调度延迟与吞吐的平衡
二、虚拟运行时间(vruntime)
vruntime 是 CFS 调度的核心数据结构,定义于 struct sched_entity 中。其更新公式为:
delta_exec = now - se.exec_start;
delta_exec_weighted = delta_exec * (NICE_0_LOAD / se.load.weight);
se.vruntime = delta_exec_weighted;
其中:
delta_exec:实际执行时间(纳秒)NICE_0_LOAD:nice 值 0 对应的权重(1024)se.load.weight:该进程的调度权重,由 nice 值转换而来
权重越高 → vruntime 增长越慢 → 实际获得更多 CPU 时间。例如,nice 值 -20 的进程权重约为 88761,nice 值 19 的进程权重仅为 15。这意味着 nice -20 进程的 vruntime 增长速度仅为 nice 19 进程的约 1/5900。
load_weight 的结构(kernel/sched/)包含以下关键字段:
struct load_weight {
unsigned long weight;
u32 invweight; // premultiplied inverse: 2^32 / weight
};
使用 invweight 的乘法代替除法,是因为在热路径中乘法远快于除法。
三、调度器数据结构:cfs_rq 与 sched_entity
3.1 核心结构关系
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; // 当前运行实体
// ...
};
struct sched_entity {
struct load_weight load; // 权重
struct rb_node run_node; // 红黑树节点
u64 vruntime; // 虚拟运行时间
u64 exec_start; // 本次执行开始时间
u64 sum_exec_runtime; // 总实际执行时间
u64 prev_sum_exec_runtime; // 上次切换前执行时间
// ...
};
内核将进程(task_struct)、调度组实体、进程组都视为 sched_entity。一个 cfs_rq 可能对应一个 CPU 的运行队列、一个调度组,甚至一个 cgroup 的公平份额。
3.2 min_vruntime 的作用
min_vruntime 是红黑树中最左侧节点的 vruntime 值,作为所有进程的"vruntime 基准线"。新创建进程的 vruntime 会被设置为当前 min_vruntime(而非 0),避免长期休眠的进程在醒来后因 vruntime 过小而垄断 CPU(即睡眠者饥饿问题)。
当一个进程从一个 CPU 迁移到另一个 CPU 时,其 vruntime 会根据目标 cfs_rq 的 min_vruntime 进行调整,保证公平性不被破坏。
四、红黑树与调度选择
CFS 使用红黑树作为任务优先级队列,树键为 vruntime。最左侧节点即下一个被调度目标,时间复杂度为 O(1)(通过 rb_leftmost 缓存)。插入操作为 O(log n),在典型桌面负载(数十到数百进程)下开销极小。
调度主路径(pick_next_task_fair → pick_next_entity)流程:
- 从
cfs_rq->tasks_timeline获取最左侧节点 - 如果存在
cfs_rq->next(用户通过sched_setattr设置的 next_buddy),且其 vruntime 小于最左侧节点sysctl_sched_wakeup_granularity,则优先选择 next buddy - 同理处理
last(上一次运行的进程,利用缓存局部性) - 返回最左侧节点
这种 "last/next buddy" 机制允许用户空间显式提示调度器优先选择某个实体,对紧密通信的进程对(如生产者-消费者)特别有效。
五、唤醒抢占与放置策略
当一个进程从睡眠中被唤醒时,CFS 需要判断是否抢占当前运行进程。关键检查流程:
// check_preempt_wakeup:
granularity = sysctl_sched_wakeup_granularity;
if (vdiff= se->vruntime - curr->vruntime)

发表评论 取消回复