一、为什么Linux需要CFS
在CFS(Completely Fair Scheduler)出现之前,Linux使用的是O(1)调度器,该调度器虽然在高负载下表现良好,但存在复杂的启发式交互判断和不公平的CPU时间分配问题。2007年,Linux 2.6.23引入了CFS,它彻底抛弃了传统的时间片和固定优先级分配方式,转而采用一种基于虚拟运行时的全新调度模型。
CFS的核心思想极其简洁:模拟一个理想的、精确多任务处理的CPU,在这个理想CPU上,每个任务都能在1/n的时间内获得CPU(n为可运行任务数)。现实中的CPU只能在某个时刻运行一个任务,因此CFS的目标就是尽可能逼近这个理想模型。
二、CFS核心数据结构:红黑树与vruntime
CFS使用红黑树(Red-Black Tree)来组织所有可运行任务,以任务的虚拟运行时间vruntime作为排序键值。红黑树保证了O(log n)的插入、删除和查找最左节点(最小vruntime)操作效率。
// 核心数据结构(简化)
struct sched_entity {
u64 vruntime; // 虚拟运行时间(核心字段)
u64 exec_start; // 本次开始执行的实际时间
u64 sum_exec_runtime; // 累计实际运行时间
u64 prev_sum_exec_runtime; // 上次切换时的累计运行时间
};
struct cfs_rq {
struct rb_root_cached runqueue; // 红黑树根节点
struct sched_entity *curr; // 当前运行的任务
unsigned int nr_running; // 可运行任务数量
u64 min_vruntime; // 树中最小vruntime(用于归一化)
};
vruntime的计算公式:
vruntime += (delta_exec * NICE_0_LOAD) / se->load.weight
其中delta_exec是实际执行时间,NICE_0_LOAD是nice值为0的权重,load.weight是当前任务的权重。这意味着高优先级任务(nice值低)的vruntime增长更慢,因此能更频繁地被调度。
三、调度核心逻辑:pick_next_task_fair
当CPU需要选择下一个运行任务时,CFS调用pick_next_task_fair(),其核心步骤是:
- 从红黑树中找到vruntime最小的节点(最左节点)
- 将该任务从红黑树中移除(如果不在树上则为当前任务curr)
- 设置CFS运行队列的min_vruntime并更新
- 如果vruntime导致min_vruntime溢出,则归一化整个红黑树
static struct task_struct *pick_next_task_fair(struct rq *rq)
{
struct cfs_rq *cfs_rq = &rq->cfs;
struct sched_entity *se;
// 若无可运行任务,直接返回NULL
if (!cfs_rq->nr_running)
return NULL;
// 获取最左节点(最小vruntime)
se = pick_next_entity(cfs_rq);
set_next_entity(cfs_rq, se);
// 返回对应的task_struct
return task_of(se);
}
四、任务入队与出队:enqueue_entity / dequeue_entity
入队操作enqueue_entity:当一个新任务变为可运行状态(TASK_RUNNING),需要将其插入到CPU对应的CFS红黑树中。
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;
int leftmost = 1;
// 红黑树插入逻辑:按vruntime从左到右递增
while (*link) {
parent = *link;
entry = rb_entry(parent, struct se_node, run_node);
if (entity_before(se, entry)) {
link = &parent->rb_left;
} else {
link = &parent->rb_right;
leftmost = 0;
}
}
rb_link_node(&se->run_node, parent, link);
rb_insert_color(&se->run_node, &cfs_rq->tasks_timeline);
}
关键细节:新唤醒的线程的vruntime会被调整为max(se->vruntime, cfs_rq->min_vruntime - sysctl_sched_latency),防止饥饿问题——如果一个任务睡眠时间过长导致vruntime远小于其他任务,它会垄断CPU一段时间。
五、时间片与调度粒度:sched_slice与sched_period
CFS不使用固定时间片,而是动态计算每个任务应运行的时长(称为slice):
static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
unsigned int nr_running = cfs_rq->nr_running;
u64 slice = __sched_period(nr_running);
// 按权重分配slice
slice = div_u64(slice * se->load.weight, cfs_rq->load.weight);
return slice;
}
调度周期sysctl_sched_period默认为6ms(SCHED_PERIOD),最小粒度min_granularity一般为0.75ms。当运行任务数越多,每个任务的slice越小,保证公平性同时兼顾交互响应。
六、组调度与带宽控制:task_group与cfs_bandwidth
从Linux 2.6.24开始,CFS支持组调度(Group Scheduling),允许将任务分组并限制每个组的CPU使用率:
struct cfs_bandwidth {
raw_spinlock_t lock;
ktime_t period; // 周期长度(默认100ms)
u64 quota; // 周期内可用时间(-1为无限)
u64 runtime; // 剩余可用时间
struct hlist_head throttled_cfs_rq; // 被限流的CFS队列
};
通过cfs_period_us和cfs_quota_us两个cgroup参数,可以精确控制某个组(如Docker容器)的CPU使用上限。例如设置quota为period的0.5倍,则该组最多使用50%的CPU。
七、多核负载均衡:sched_domain与性能优化
在现代多核系统中,CFS的管理在硬件上扩展到多个CPU核心。每个CPU维护独立的CFS运行队列,但当某个CPU空闲时,会从繁忙CPU的任务队列中拉取任务,这就是负载均衡。
负载均衡的层级结构(sched_domain):
- MC(Multi-Core):同一物理CPU内部的多核心之间
- SYS(System):不同NUMA节点之间
负载均衡触发时机包括:定时tick检查、CPU进入空闲时、新fork子进程时。尽量在同一CPU的睡眠中上唤醒被同一运行队列管理的任务(smt亲和性),减少IPI(处理器间中断)开销。
八、CFS调优实践与内核参数
| 参数 | 默认值 | 说明 |
|---|---|---|
| sched_latency_ns | 24ms | 调度周期(默认让每个任务至少运行一次所需时间) |
| sched_min_granularity_ns | 3ms | 最小调度粒度(保证交互任务不被过度抢占) |
| sched_wakeup_granularity_ns | 4ms | 唤醒抢占粒度(控制新唤醒任务抢占当前任务的倾向) |
| sched_migration_cost_ns | 500000 | 缓存热迁移成本(防止不必要的迁移) |
交互密集型场景优化建议:减小sched_min_granularity_ns和sched_wakeup_granularity_ns可以提升桌面响应速度,但会增加上下文切换开销。服务器场景则可以适当增大这些值来减少切换开销。
九、最新进展:sched_ext与可拓展调度器
Linux 6.12引入了sched_ext(Scheduling Extension),允许用户空间自定义调度策略并加载到内核中。这为特殊场景(如游戏引擎的帧同步、AI推理的突发任务)提供了更灵活的调度能力,而无需修改内核代码。
基于调度分级(sched_class)的框架,CFS仍然是默认的公平调度器,但sched_ext的引入意味着Linux调度器进入了一个可扩展的新时代——用户可以根据自己的工作负载特征编写定制化的eBPF程序来实现特定的调度策略。
十、总结
CFS是Linux内核中设计最优雅的调度子系统之一。它通过虚拟运行时间的概念将公平用数学方式精确量化,利用红黑树高效地维护了O(log n)复杂度的任务选择,并通过负载均衡策略适配多核架构。理解CFS不仅有助于系统性能调优,更是深入学习操作系统调度原理的必经之路。
在当今容器化和云原生时代,CFS的组调度能力(cgroup cpu控制)和带宽管理功能成为了Kubernetes等容器编排平台CPU资源管理的底层基石,对理解整个云原生基础设施有着不可替代的价值。

发表评论 取消回复