Linux 内核 CFS 完全公平调度器深度实战
Linux 内核的进程调度器是操作系统的核心组件之一。自 2.6.23 版本起,CFS(Completely Fair Scheduler,完全公平调度器)取代了之前的 O(1) 调度器,成为 Linux 默认的进程调度器。CFS 的设计理念极其优雅:它并不追求绝对的时间片均分,而是通过红黑树和虚拟运行时间(vruntime)的概念,让每个进程\"感觉\"自己获得了公平的 CPU 时间份额。本文将深入剖析 CFS 的核心数据结构、调度算法、带宽控制机制,并结合内核源码与实战调优案例,带你彻底掌握这一调度器的工作原理。
一、CFS 设计哲学:从时间片到虚拟运行时间
传统调度器通常为每个进程分配一个固定长度的时间片(如 100ms),时间片用完便触发调度。这种方式的问题在于:当进程数量增多时,每个进程获得的时间片缩短,上下文切换开销急剧上升。CFS 的做法完全不同——它不使用时间片,而是追踪每个进程已经运行了多少时间,并始终选择运行时间最少的进程来运行。
CFS 引入了\"虚拟运行时间\"(virtual runtime,简称 vruntime)的概念。vruntime 的计算公式为:
vruntime = 实际运行时间 × (NICE_0_LOAD / 进程权重) 其中 NICE_0_LOAD 是 nice 值为 0 的进程的权重基准值(通常为 1024)。这意味着:
- 高权重进程(低 nice 值)的 vruntime 增长更慢,获得更多实际 CPU 时间
- 低权重进程(高 nice 值)的 vruntime 增长更快,获得更少实际 CPU 时间
- 所有公平进程中,vruntime 的值大致保持同步增长
这种设计的精妙之处在于:CFS 不需要关心\"每个进程应该运行多久\",它只需要保证\"每个进程的 vruntime 增长速率与其权重成正比\",公平性就自然实现了。
二、核心数据结构
2.1 调度实体:sched_entity
每个进程在内核中由一个 task_struct 表示,而调度相关的信息则内嵌在 sched_entity 结构体中:
struct sched_entity { struct load_weight load; // 进程权重(决定 vruntime 增长速率) struct rb_node run_node; // 红黑树节点 u64 exec_start; // 本次开始执行的时间戳 u64 sum_exec_runtime; // 累计运行时间 u64 vruntime; // 虚拟运行时间 u64 prev_sum_exec_runtime; // 上次切换时的累计运行时间 // ... }; 关键字段说明:
- load:权重值,nice 0 → 1024,nice -1 → 约 1277,nice 1 → 约 820。权重越大,vruntime 增长越慢,CPU 份额越多
- vruntime:核心调度依据,CFS 总是选择 vruntime 最小的进程运行
- run_node:红黑树节点,用于在红黑树中定位该进程
2.2 运行队列:cfs_rq
每个 CPU 都有一个运行队列,CFS 运行队列结构体定义如下:
struct cfs_rq { struct load_weight load; // 运行队列上所有进程的总权重 unsigned int nr_running; // 运行队列中的进程数量 u64 min_vruntime; // 队列中最小的 vruntime 值(基准线) struct rb_root tasks_timeline; // 红黑树根节点 struct rb_node *rb_leftmost; // 最左侧节点(vruntime 最小 = 下一个要运行的进程) struct sched_entity *curr; // 当前正在运行的调度实体 // ... }; 2.3 红黑树组织
CFS 使用红黑树来组织所有可运行进程,键值为 vruntime。红黑树的优势在于插入、删除和查找最小值的时间复杂度均为 O(log n)。rb_leftmost 指针指向树的最左端,即 vruntime 最小的进程,这是下一个被调度的候选。
三、调度算法详解
3.1 入队操作:enqueue_entity
当进程变为可运行状态时,需要插入红黑树:
static void enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags) { // 计算入队时该进程应该对齐到的 vruntime 基准 if (flags

发表评论 取消回复