一、为什么需要理解 CFS
Linux 内核的进程调度器是整个系统的灵魂。自 2.6.23 版本以来,Completely Fair Scheduler(CFS)取代了之前的 O(1) 调度器,成为 Linux 默认的调度算法。CFS 的设计哲学并非追求复杂的优先级队列,而是模拟一个"完美的多任务处理器"——让每个任务都觉得自己独占了 CPU。理解 CFS,是深入 Linux 系统性能调优和内核开发的关键一步。
二、核心概念:虚拟运行时间(vruntime)
CFS 的核心创新在于引入了一个度量标准——虚拟运行时间(virtual runtime,简称 vruntime)。它的精妙之处在于将实际运行时间按照任务权重进行归一化,使得不同优先级的任务可以在同一个尺度上比较"谁更应该被调度"。
vruntime += delta_exec × (NICE_0_LOAD / weight)
其中:
- delta_exec:任务实际执行的物理时间
- NICE_0_LOAD:nice 值为 0 的任务的基准权重(通常为 1024)
- weight:当前任务的权重,由 nice 值决定
这意味着优先级越高(weight 越大)的任务,其 vruntime 增长越慢——它会被调度器"偏爱"地给予更多 CPU 时间。反之,低优先级任务的 vruntime 会快速增长,在红黑树中逐渐右移。
三、数据结构:红黑树与调度实体
CFS 使用红黑树(Red-Black Tree)来组织所有可运行的任务。每个调度实体(sched_entity)都通过其 vruntime 作为键值插入红黑树中。红黑树的最左节点,就是 vruntime 最小的任务——也就是当前最应该获得 CPU 的任务。
// 内核中的调度实体结构(简化)
struct sched_entity {
struct load_weight load; // 权重
struct rb_node run_node; // 红黑树节点
u64 vruntime; // 虚拟运行时间
u64 exec_start; // 开始执行时间
u64 sum_exec_runtime; // 总执行时间
};
// CFS 运行队列
struct cfs_rq {
struct rb_root_cached tasks_timeline; // 红黑树根
struct sched_entity *curr; // 当前运行任务
struct sched_entity *next; // 下一个任务(用于抢占)
unsigned long nr_running; // 可运行任务数
};
红黑树的选择并非偶然。相比堆或链表,红黑树在插入、删除和查找最小值操作上都能保持 O(log n) 的复杂度,这对于每秒需要执行数千次调度的操作系统内核至关重要。
四、调度流程:从时钟中断到上下文切换
CFS 的调度流程由周期性时钟中断(tick)驱动:
- 时钟中断触发
scheduler_tick()函数 - 更新当前任务的 vruntime
- 检查当前任务的 vruntime 是否大于最左节点任务的 vruntime(考虑了一个
sysctl_sched_min_granularity的粒度阈值,防止频繁切换) - 如果需要抢占,设置
TIF_NEED_RESCHED标志 - 在适当的时机(系统调用返回、中断返回等)调用
schedule() schedule()从红黑树中取出最左节点,进行上下文切换
// 简化的 pick_next_task_cfs 逻辑
static struct task_struct *pick_next_task_fair(struct rq *rq)
{
struct sched_entity *se = pick_next_entity(cfs_rq);
struct task_struct *p = task_of(se);
set_next_task(rq, p); // 设置当前运行任务
return p;
}
五、组调度与 cgroup 支持
CFS 并非只关注单个进程。通过 组调度(Group Scheduling) 机制,CFS 可以将调度粒度扩展到 cgroup 级别。这意味着容器中的进程组可以获得公平的 CPU 份额,这是 Docker/Kubernetes 资源隔离的底层基础。
在 cgroup v2 中,CPU 资源通过 cpu.weight 文件直接控制组内所有进程的权重,其值范围 1-10000,默认 100。每个 cgroup 内部维护自己的 cfs_rq,组与组之间也按照 vruntime 原则公平调度。
六、NUMA 感知调度
在现代多核 NUMA 架构中,远程内存访问的延迟可能是本地访问的 2-3 倍。CFS 的 NUMA 平衡机制会在以下情况下触发跨节点迁移:
- 任务的大部分内存页面在远程节点
- 目标节点的 CPU 负载并未显著高于当前节点
- 迁移收益超过迁移开销
通过 numa_balancing 内核参数和 proc/sys/kernel/numa_balancing_scan_period_min_ms 等参数,可以精细控制 NUMA 平衡的激进程度。
七、性能调优实战
理解 CFS 原理的最终目的是服务于实际性能优化。以下是常见调优场景:
- 减少调度延迟:降低
sched_migration_cost_ns让任务更快迁移到空闲核心 - 交互式任务优先:CFS 自动识别睡眠时间长的交互式任务(通过 vruntime 补偿延迟敏感型应用)
- CPU 绑核:通过
cpusetcgroup 或taskset将关键任务绑定到特定核心,避免缓存失效 - 实时任务保障:对延迟敏感的任务使用
SCHED_FIFO或SCHED_RR策略,CFS 会让位于这些实时策略
八、总结
CFS 的伟大之处在于用极简的数据结构(红黑树)和优雅的概念模型(vruntime)实现了一个真正公平的调度器。它的设计思想——通过虚拟时间归一化实际时间、通过自平衡树高效组织任务、通过组调度支持资源隔离——不仅影响了 Linux 内核,也为其他操作系统的调度设计提供了重要参考。对于希望深入 Linux 性能优化的开发者和运维工程师而言,理解 CFS 是不可或缺的核心能力。

发表评论 取消回复