Linux作为多任务操作系统,进程调度是内核的核心子系统之一,其作用是在多CPU/多核环境下,合理分配CPU执行时间给各个进程,实现“伪并行”的多任务运行。而CFS(Completely Fair Scheduler,完全公平调度器) 是Linux 2.6.23之后默认的进程调度器,彻底替代了传统的O(1)调度器,成为桌面、服务器、嵌入式等所有场景的核心调度实现。
多数开发者对进程调度的认知停留在“时间片轮转”“高优先级进程先执行”的表层,却不清楚CFS如何实现“公平”、时间片在CFS中为何是动态计算而非固定值、优先级如何影响调度权重、内核又如何通过调度实体、红黑树等结构完成进程的选择与切换。
本文将从Linux内核视角,彻底拆解CFS调度器的设计核心与底层实现,厘清调度实体、虚拟运行时间、红黑树调度队列三大核心组件,详解CFS中时间片的动态计算逻辑和优先级的底层映射原理,同时结合内核源码片段和调度流程,让你从“使用进程调度”升级为“理解内核如何实现进程调度”。
一、进程调度的核心基础:先搞懂3个核心问题
在深入CFS之前,需先理清Linux进程调度的核心设计目标、调度层级和进程分类,这是理解CFS设计原理的前提,也是区分“传统调度器”和“CFS”的关键。
1. 进程调度的核心设计目标
Linux内核进程调度器的设计围绕公平性、高效性、响应性三大核心目标展开,三者相互平衡,适配不同应用场景:
- 公平性:确保所有进程都能获得合理的CPU执行时间,避免某一进程长期占用CPU,其他进程饥饿;
- 高效性:调度器本身的开销要极小(调度器属于内核核心路径,频繁执行),要求调度算法的时间复杂度尽可能低(CFS为O(logN));
- 响应性:保证交互式进程(如终端、桌面应用)的低延迟,让用户感受到“系统流畅”,这类进程需要被快速调度,减少等待时间。
核心矛盾:后台批处理进程(如数据计算、文件压缩)需要长时运行以提升吞吐量,而交互式进程需要快速响应以提升体验,调度器需在两者间做动态平衡——这也是CFS的核心设计难点。
2. Linux的调度层级:全局调度与局部调度
Linux为了适配多CPU/多核架构,将进程调度分为全局调度和局部调度两层,基于NUMA架构和CPU亲和性做了极致优化:
- 局部调度:每个CPU核心拥有独立的运行队列(runqueue),调度器仅在当前CPU的运行队列中选择进程执行,避免跨CPU的锁竞争,提升调度效率;
- 全局调度:当某一CPU核心的运行队列负载过高(进程过多),而其他CPU核心空闲时,内核通过负载均衡将部分进程从高负载CPU的运行队列迁移到低负载CPU,实现全局负载均衡。
CFS的核心范围:CFS主要负责单个CPU核心的局部调度(运行队列管理、进程选择、时间片计算),而全局调度由内核的负载均衡子系统完成,两者协同工作。
3. Linux的进程分类:按调度策略划分
Linux内核根据进程的运行特性,为不同进程分配不同的调度策略,CFS是普通非实时进程的默认调度策略,对应内核的SCHED_OTHER(也叫SCHED_NORMAL)策略。Linux的主要调度策略分为两大类:
(1)普通非实时进程(SCHED_OTHER/SCHED_BATCH/SCHED_IDLE)
- 占系统中进程的99%,如Web服务、脚本、终端程序、后台批处理任务等;
- 由CFS调度器负责调度,核心原则是公平性,支持动态优先级调整;
SCHED_BATCH:批处理进程,牺牲响应性换取吞吐量,CFS会为其分配更长的运行时间;SCHED_IDLE:空闲进程,仅当系统无其他进程运行时才执行,优先级最低。
(2)实时进程(SCHED_FIFO/SCHED_RR)
- 用于对延迟要求极高的场景,如工业控制、嵌入式实时系统、音视频硬解码等;
- 由实时调度器负责调度,核心原则是优先级至上,实时进程的优先级(099)高于所有普通进程(-2019);
SCHED_FIFO:先进先出实时调度,高优先级实时进程一旦开始运行,会一直执行到主动放弃CPU或被更高优先级实时进程抢占;SCHED_RR:时间片轮转实时调度,为实时进程分配固定时间片,时间片用完后让出CPU,避免单个实时进程长期占用。
核心规则:实时进程永远优先于普通非实时进程执行——只要系统中有可运行的实时进程,CFS调度器就不会被触发,直到所有实时进程进入阻塞/终止状态。
本文的核心讲解对象是CFS调度器,即普通非实时进程的调度实现。
二、CFS调度器的核心设计:抛弃固定时间片,以“公平”为核心
传统的进程调度器(如Linux早期的O(1)调度器、Windows的调度器)核心是固定时间片+优先级轮转:为不同优先级的进程分配固定长度的时间片,高优先级进程时间片更长,时间片用完后放入就绪队列尾部,调度器选择下一个进程执行。
但这种设计存在明显缺陷:固定时间片无法适配动态的进程数量和优先级变化,容易导致低优先级进程饥饿,且时间片的长度难以在“响应性”和“吞吐量”间做平衡(时间片太短会导致进程切换过于频繁,开销增大;时间片太长会导致交互式进程响应延迟)。
CFS调度器彻底抛弃了固定时间片的设计,提出了全新的核心设计理念:以“公平”为核心,让所有进程获得相同的CPU执行比例,动态计算每个进程的运行时间,通过虚拟运行时间衡量进程的“公平性”。
CFS的核心设计理念:完全公平的本质
CFS对“公平”的定义非常简单:在相同的时间窗口内,所有进程获得的CPU执行时间与自身的权重成比例,权重越高,获得的CPU时间越多(优先级越高,权重越大)。
为了实现这一公平性,CFS做了两个关键设计:
- 抛弃固定时间片:不再为进程预先分配固定的时间片,而是根据当前运行队列中的进程数量和进程的权重,动态计算每个进程本次可运行的最大时间(即CFS中的“时间片”,本质是进程的最大运行时长);
- 引入虚拟运行时间(vruntime):为每个进程维护一个虚拟运行时间,表示进程在CPU上的“实际运行时间按权重归一化后的时间”。CFS的核心调度原则是:始终选择虚拟运行时间最小的进程执行,确保“跑得少的进程先跑”,从根本上避免进程饥饿。
举个通俗的例子:
- 系统中有两个普通进程A(优先级默认,权重1024)和B(优先级更高,权重2048);
- CFS的公平性要求:A和B的CPU执行时间比例为1:2(权重比1:2);
- 若当前CPU总时间窗口为30ms,则A可运行10ms,B可运行20ms,动态计算而非固定分配;
- 运行过程中,A的vruntime按实际运行时间×(1024/1024) 累加,B的vruntime按实际运行时间×(1024/2048) 累加(权重越高,vruntime增长越慢);
- 若A运行10ms后,vruntime增加10ms,B运行20ms后,vruntime仅增加10ms,两者vruntime相等,实现“按权重比例公平运行”。
这一设计彻底解决了传统固定时间片调度器的缺陷,让CFS能自适应进程数量和优先级变化,在公平性、响应性和吞吐量间实现完美平衡。
三、CFS调度器的三大核心组件:内核实现的基础
CFS的底层实现依赖Linux内核的三个核心数据结构/组件,它们是CFS管理进程、计算虚拟运行时间、实现调度选择的基础,所有调度逻辑都围绕这三个组件展开。
1. 调度实体(sched_entity):进程的调度抽象
Linux内核中,进程(task_struct) 是资源管理的基本单位,而调度实体(sched_entity) 是调度器的基本调度单位——CFS不直接调度进程,而是调度调度实体,一个进程可以对应一个或多个调度实体(如线程组的进程会共享调度实体)。
调度实体嵌入在进程的task_struct结构中,核心作用是存储与调度相关的所有信息,包括虚拟运行时间、权重、所属的运行队列等。内核源码中sched_entity的核心字段(简化版,基于Linux 5.10):
// 内核源码:include/linux/sched.h
structsched_entity {
structload_weightload;// 进程的负载权重,与优先级直接相关
u64 vruntime; // 核心:虚拟运行时间,纳秒级
u64 prev_vruntime; // 上一次的虚拟运行时间
structrb_noderun_node;// 红黑树节点,用于将调度实体加入CFS的红黑树运行队列
structcfs_rq *cfs_rq;// 指向该调度实体所属的CFS运行队列
structsched_entity *parent;// 父调度实体(用于组调度)
// 其他调度相关字段...
};
// 进程的task_struct中嵌入sched_entity
structtask_struct {
pid_t pid;
structsched_entityse;// CFS调度实体
int prio; // 进程的静态优先级(-20~19)
int nice; // 普通进程的nice值(对应静态优先级,-20~19)
// 其他进程相关字段...
};
核心字段说明:
load:负载权重,是优先级的底层映射,load.weight为具体的权重值(默认1024),优先级越高,权重越大;vruntime:虚拟运行时间,纳秒级,CFS调度的核心依据,始终选择vruntime最小的调度实体执行;run_node:红黑树节点,CFS将所有可运行的调度实体组织在红黑树中,通过该节点实现插入、删除、查找;cfs_rq:指向所属的CFS运行队列,每个CPU核心的运行队列(runqueue)中包含一个CFS运行队列(cfs_rq),专门管理普通非实时进程。
2. CFS运行队列(cfs_rq):每个CPU的专属调度队列
如前文所述,Linux为每个CPU核心分配一个独立的全局运行队列(runqueue),而每个runqueue中又包含一个CFS运行队列(cfs_rq),专门用于管理当前CPU上的可运行普通非实时进程的调度实体。
CFS运行队列的核心作用是维护当前CPU上所有可运行的调度实体,存储调度队列的全局状态(如总权重、虚拟时间基准),内核源码中cfs_rq的核心字段(简化版):
// 内核源码:include/linux/sched.h
structcfs_rq {
structrb_root_cachedtasks_timeline;// 核心:红黑树的根节点,组织所有可运行的调度实体
structsched_entity *curr;// 当前正在CPU上运行的调度实体
structsched_entity *next;// 下一个即将被调度的调度实体
u64 min_vruntime; // 队列中最小的虚拟运行时间,优化红黑树查找
structload_weightload;// 队列中所有调度实体的总负载权重
unsignedint nr_running; // 队列中可运行的进程数量
u64 clock; // CFS的全局时钟,用于计算虚拟运行时间
// 其他队列管理字段...
};
核心字段说明:
tasks_timeline:红黑树的根节点,CFS将所有可运行的调度实体按vruntime升序组织在这棵红黑树中,红黑树的最左节点就是vruntime最小的调度实体,也是CFS下一次要调度的进程——这一设计让CFS能在O(logN)时间内找到下一个要执行的进程,保证调度效率;curr:当前正在CPU上运行的调度实体,若CPU空闲,则为NULL;min_vruntime:队列中所有调度实体的最小vruntime,CFS通过该字段避免每次都遍历红黑树查找最左节点,提升调度效率;load:队列中所有可运行调度实体的总权重,是CFS动态计算每个进程运行时间的核心依据;nr_running:队列中可运行的进程数量,用于判断CPU是否空闲。
核心特性:每个CPU的cfs_rq是独占的,无需跨CPU加锁,仅在负载均衡时才会被其他CPU访问,这一设计让CFS的调度开销降到最低。
3. 虚拟运行时间(vruntime):CFS的“公平性标尺”
虚拟运行时间(vruntime) 是CFS调度器的核心,是衡量进程“运行时长”的公平性标尺,其本质是进程的实际CPU运行时间,按自身权重做归一化后的时间值,单位为纳秒(ns)。
CFS的所有调度逻辑都围绕vruntime展开,其核心规则可总结为三句话:
- 可运行进程的vruntime会随实际运行时间累加
- CFS始终选择vruntime最小的可运行进程执行
- 进程被阻塞/挂起时,vruntime停止累加,再次唤醒时,其vruntime仍保持阻塞前的值
(1)vruntime的计算公式
CFS为了实现“权重越高,获得的CPU时间越多”,将进程的实际运行时间按自身权重做归一化,得到虚拟运行时间,核心计算公式为:
$$
vruntime_{累加值} = 实际运行时间(ns) × \frac{默认权重(1024)}{进程自身权重(weight)}
$$
默认权重为1024,是Linux内核为普通进程(nice=0,优先级默认)设置的基准权重。
公式解读:
- 若进程权重=1024(默认优先级),则
vruntime累加值=实际运行时间,vruntime与实际运行时间同步增长; - 若进程权重>1024(优先级更高,nice值更小),则
vruntime累加值 < 实际运行时间,vruntime增长更慢——意味着该进程能获得更长的CPU运行时间,且更容易被调度; - 若进程权重<1024(优先级更低,nice值更大),则
vruntime累加值 > 实际运行时间,vruntime增长更快——意味着该进程获得的CPU时间更短,调度优先级更低。
(2)vruntime的全局同步:避免时钟漂移
为了保证多CPU环境下的公平性,CFS引入了全局虚拟时间基准,所有CPU的cfs_rq.clock会同步到系统的全局时钟,避免不同CPU上的进程因时钟漂移导致vruntime计算不公平。
同时,当进程从一个CPU的cfs_rq迁移到另一个CPU的cfs_rq时(负载均衡),CFS会将该进程的vruntime做偏移修正,使其与目标CPU的min_vruntime对齐,保证迁移后进程的公平性。
四、CFS的核心调度流程:从进程唤醒到进程切换
理解了CFS的三大核心组件后,我们从内核视角拆解CFS的完整调度流程——从进程被唤醒加入可运行队列,到调度器选择下一个进程,再到进程切换执行,全程分为4个核心阶段,覆盖CFS的核心调度逻辑。
以下流程基于单个CPU核心的局部调度(CFS的核心范围),假设系统中无实时进程(实时进程优先于普通进程执行)。
阶段1:进程唤醒,加入CFS红黑树运行队列
当进程从阻塞状态(如等待IO、sleep、锁)被唤醒时,内核会执行以下操作,将其加入CFS的可运行队列:
- 初始化/更新调度实体:若进程是首次被唤醒,初始化其
sched_entity的load(权重)、vruntime(初始为当前cfs_rq.min_vruntime,保证公平);若为再次唤醒,保持其vruntime为阻塞前的值; - 更新CFS运行队列状态:将该进程的
sched_entity按vruntime升序插入到当前CPU的cfs_rq.tasks_timeline红黑树中,成为红黑树的一个节点;同时更新cfs_rq的load(总权重累加)、nr_running(可运行进程数+1)、min_vruntime(更新为队列最小vruntime); - 触发调度:内核调用
schedule()函数触发调度,通知CFS调度器当前CPU的运行队列有新进程加入,需要重新选择下一个执行的进程。
阶段2:计算进程的最大运行时间(动态时间片)
CFS抛弃了固定时间片,但为了避免单个进程长期占用CPU,会为每个进程计算本次可运行的最大时长(即CFS中的“动态时间片”),当进程的实际运行时间达到该值时,会被强制让出CPU,重新加入红黑树队列。
该最大运行时间的计算基于当前CFS运行队列的总权重和系统的调度周期,核心计算公式为:
$$
进程最大运行时间 = 调度周期 × \frac{进程自身权重}{队列总权重}
$$
关键参数说明:
- 调度周期(sched_period):CFS的全局调度周期,是指CFS将运行队列中所有可运行进程都调度一次的总时间,由内核动态计算,公式为:
$$
sched_period = \begin{cases}
10ms, & 进程数≤8 \
10ms + (进程数-8)×2ms, & 进程数>8
\end{cases}
$$
调度周期的默认最小值为10ms,最大值为200ms,确保进程切换的频率不会过高(避免调度开销),也不会过低(保证响应性)。 - 进程自身权重:由进程的优先级(nice值)映射而来,权重越大,最大运行时间越长;
- 队列总权重:当前
cfs_rq.load.weight,即所有可运行进程的权重之和——进程数越多,单个进程的最大运行时间越短。
举例计算:
- 系统调度周期=10ms,CFS队列中有2个进程,A(权重1024,默认优先级)、B(权重2048,更高优先级);
- A的最大运行时间=10ms × (1024/3072) ≈ 3.33ms;
- B的最大运行时间=10ms × (2048/3072) ≈ 6.67ms;
- 结果:高优先级的B获得更长的运行时间,符合“权重越高,CPU时间越多”的公平性原则。
核心特性:CFS的动态时间片随进程数量和权重动态变化,进程数越多,时间片越短;优先级越高,时间片越长——完美适配系统的动态负载。
阶段3:选择下一个进程:红黑树最左节点优先
CFS调度器的核心选择逻辑极其简单:始终选择CFS红黑树中vruntime最小的调度实体执行,而红黑树的最左节点就是vruntime最小的节点,CFS通过cfs_rq.min_vruntime可直接定位该节点,无需遍历红黑树,提升效率。
当内核调用schedule()函数触发调度时,CFS执行以下选择操作:
- 检查当前运行进程:若当前CPU上有运行的进程(
cfs_rq.curr非空),判断其实际运行时间是否达到最大运行时间,若达到,则将其让出CPU,并将其sched_entity重新插入红黑树(此时其vruntime已累加,位置会随vruntime变化); - 查找下一个进程:从
cfs_rq.tasks_timeline红黑树中找到最左节点,该节点对应的调度实体就是下一个要执行的进程; - 更新队列状态:将
cfs_rq.next指向该调度实体,更新cfs_rq.curr为该实体,标记其为“正在运行”。
核心效率:红黑树的插入、删除、查找最左节点的时间复杂度均为O(logN),N为可运行进程数,即使系统中有上千个可运行进程,CFS的调度选择开销也极小,保证了调度器的高效性。
阶段4:进程切换:上下文保存与恢复
找到下一个要执行的进程后,内核会执行进程切换操作,将CPU的执行权从当前进程(若有)切换到新进程,这一操作是所有调度器的通用步骤,由内核的上下文切换子系统完成,核心分为两步:
- 保存当前进程的上下文:将当前进程的CPU寄存器状态(如程序计数器rip、栈指针rsp、通用寄存器等)、进程状态保存到其
task_struct和内核栈中,确保进程再次被调度时能恢复到当前执行位置; - 恢复新进程的上下文:从新进程的
task_struct和内核栈中加载其CPU寄存器状态,将程序计数器rip指向新进程的下一条执行指令,更新CPU的页表基址寄存器(CR3),切换到新进程的地址空间; - 执行新进程
调度开销:进程切换的开销主要来自上下文保存/恢复和页表切换,Linux内核通过TLB缓存(快表)优化页表切换的开销,将调度切换的总开销控制在微秒级,对系统性能的影响极小。
核心触发时机:CFS的进程切换主要在以下场景触发:
- 进程主动放弃CPU(如调用
sleep()、wait()、yield()); - 有新的高权重进程被唤醒,其vruntime远小于当前运行进程;
五、优先级的底层原理:从nice值到权重映射
在Linux中,普通非实时进程的优先级由nice值表示,范围为**-20~19**,共40个级别,其核心规则是:
- nice值越小,优先级越高(nice=-20为最高优先级,nice=19为最低优先级);
- 默认nice值为0,对应默认优先级,权重为1024;
- 用户可通过
nice、renice命令修改进程的nice值,如nice -n 5 python app.py(设置nice=5,降低优先级)。
多数开发者只知道nice值对应优先级,但不清楚nice值的底层是如何影响CFS调度的——其核心是:nice值通过固定的映射表,转换为调度实体的权重(load.weight),权重直接决定了进程的vruntime增长速度和动态时间片长度,这是优先级影响调度的唯一底层路径。
1. nice值与权重的固定映射表
Linux内核为nice值(-20~19)预定义了固定的权重映射表,该表是内核硬编码的,不会动态变化,核心原则是:nice值每降低1(优先级提高1),权重约增加10%;nice值每增加1(优先级降低1),权重约减少10%。
以下是核心的nice值-权重映射关系(节选,完整表见内核源码kernel/sched/sched.h):
注:Linux 5.10之后,内核将基准权重从1024调整为10240,放大了权重的数值范围,提升了调度的精度,但映射规则和核心逻辑不变。
核心结论:进程的nice值仅用于查表获取权重,CFS调度器不直接使用nice值,所有与优先级相关的调度逻辑(vruntime计算、动态时间片分配)都基于权重实现——这是理解Linux进程优先级的关键。
2. 优先级对调度的影响:权重决定一切
nice值转换为权重后,权重通过两个核心维度影响CFS的调度,最终实现“高优先级进程获得更多CPU时间”:
(1)权重决定vruntime的增长速度
根据vruntime的计算公式,权重越高,vruntime的累加速度越慢——高优先级进程在CPU上运行相同的实际时间,其vruntime增长更少,会更快成为红黑树中vruntime最小的节点,从而被更频繁地调度。
举例:进程A(nice=-20,权重88761)和进程B(nice=0,权重10240),均运行10ms:
- A的vruntime累加值=10ms × (10240/88761) ≈ 1.15ms;
- B的vruntime累加值=10ms × (10240/10240) = 10ms;
- 结果:A的vruntime远小于B,会被优先调度,获得更多的CPU执行机会。
(2)权重决定动态时间片的长度
根据动态时间片的计算公式,权重越高,进程的最大运行时间越长——高优先级进程每次被调度时,能在CPU上运行更长的时间,进一步提升其CPU时间占比。
核心总结:Linux普通进程的优先级,底层是nice值→权重→vruntime增长速度/动态时间片的单向映射,CFS通过权重实现了优先级对调度的影响,既保证了高优先级进程的优势,又遵循了“按权重比例分配CPU时间”的公平性原则。
3. 动态优先级:内核的交互式进程优化
除了静态优先级(由nice值决定),Linux内核还为交互式进程(如终端、桌面应用、Web服务)做了动态优先级提升优化——内核会根据进程的运行特性(如睡眠时间、运行时间),动态提升交互式进程的优先级(降低nice值,增加权重),让其获得更多的CPU时间,提升系统的响应性。
内核判断交互式进程的核心依据是:进程的睡眠时间(等待IO、用户输入)远大于运行时间——这类进程对延迟敏感,需要快速响应。
例如:终端进程大部分时间在等待用户输入(睡眠时间长),仅当用户输入时才短暂运行(运行时间短),内核会动态提升其优先级,确保用户输入后能被立即调度,让用户感受到“终端操作无延迟”。
核心特性:动态优先级提升是内核自动完成的,无需用户干预,且仅适用于普通非实时进程,不会影响实时进程的调度。
六、CFS调度器的内核源码核心片段:关键逻辑实现
为了让你更直观地理解CFS的底层实现,以下选取Linux 5.10内核中CFS调度器的核心源码片段,标注关键逻辑,覆盖vruntime计算、进程选择、红黑树操作三大核心点(源码做了简化,去除了冗余注释和边界处理)。
1. vruntime计算核心函数:__update_curr
__update_curr是CFS的核心函数,用于更新当前运行进程的vruntime,在进程运行过程中被频繁调用,核心逻辑是计算实际运行时间,按权重归一化后累加到vruntime。
// 内核源码:kernel/sched/fair.c
staticvoid __update_curr(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
// 1. 获取当前进程的实际运行时间(当前时钟 - 上一次时钟)
u64 now = cfs_rq_clock(cfs_rq);
u64 delta_exec = now - se->exec_start;
if (delta_exec == 0)
return;
// 2. 保存当前时钟,为下一次计算做准备
se->exec_start = now;
// 3. 按权重归一化,计算vruntime的累加值(核心公式)
u64 delta_vruntime = calc_delta_fair(delta_exec, se);
// 4. 将累加值累加到进程的vruntime
se->vruntime += delta_vruntime;
// 5. 更新CFS运行队列的min_vruntime
cfs_rq->min_vruntime = min(cfs_rq->min_vruntime, se->vruntime);
}
// 归一化计算delta_vruntime的核心函数
static u64 calc_delta_fair(u64 delta, struct sched_entity *se)
{
// 核心公式:delta_vruntime = delta × (NICE_0_LOAD / se->load.weight)
// NICE_0_LOAD为默认权重(10240)
if (unlikely(se->load.weight != NICE_0_LOAD))
delta = __calc_delta(delta, NICE_0_LOAD, &se->load);
return delta;
}
2. 选择下一个进程核心函数:pick_next_task_fair
pick_next_task_fair是CFS的进程选择函数,核心逻辑是从红黑树中找到最左节点(vruntime最小),作为下一个要执行的进程。
// 内核源码:kernel/sched/fair.c
struct task_struct *pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
structcfs_rq *cfs_rq = &rq->cfs;
structsched_entity *se;
structtask_struct *p;
// 1. 若CFS队列为空,返回NULL(无普通进程可运行)
if (!cfs_rq->nr_running)
returnNULL;
// 2. 从红黑树中找到最左节点(vruntime最小的调度实体)
se = __pick_first_entity(cfs_rq);
// 3. 将调度实体转换为对应的进程task_struct
p = task_of(se);
// 4. 标记该进程为即将运行,更新队列状态
__set_next_entity(cfs_rq, se);
return p;
}
// 找到红黑树最左节点的核心函数
staticstructsched_entity *__pick_first_entity(structcfs_rq *cfs_rq)
{
// 红黑树的最左节点就是vruntime最小的节点
structrb_node *left = rb_first_cached(&cfs_rq->tasks_timeline);
return rb_entry(left, struct sched_entity, run_node);
}
3. 进程加入红黑树核心函数:enqueue_entity
enqueue_entity用于将进程的调度实体插入CFS红黑树,在进程被唤醒时调用,核心逻辑是按vruntime升序插入红黑树,并更新队列的总权重和可运行进程数。
// 内核源码:kernel/sched/fair.c
staticvoidenqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
// 1. 调整vruntime,与队列的min_vruntime对齐,保证公平性
if (flags & ENQUEUE_WAKEUP)
se->vruntime = max(se->vruntime, cfs_rq->min_vruntime);
// 2. 将调度实体按vruntime升序插入红黑树
rb_insert_cached(&se->run_node, &cfs_rq->tasks_timeline);
// 3. 更新CFS队列的总权重和可运行进程数
cfs_rq->load.weight += se->load.weight;
cfs_rq->nr_running++;
// 4. 更新队列的min_vruntime
update_min_vruntime(cfs_rq);
}
七、CFS调度器的优势与适用场景
CFS调度器自Linux 2.6.23发布以来,成为默认调度器并沿用至今,其核心优势在于优秀的公平性、自适应的动态调度、极低的调度开销,完美适配Linux的所有应用场景,相比传统的O(1)调度器,CFS的核心优势可总结为4点:
- 极致的公平性:基于虚拟运行时间和权重的设计,确保所有进程按权重比例获得CPU时间,从根本上避免进程饥饿;
- 动态适配负载:抛弃固定时间片,根据进程数量和权重动态计算运行时间,适配系统的动态负载变化;
- 高效的调度算法:基于红黑树的O(logN)时间复杂度,即使系统中有上千个可运行进程,调度开销也极小;
- 良好的响应性:通过动态优先级提升优化交互式进程,保证桌面、终端、Web服务等交互式场景的低延迟。
CFS的适用场景:
- 桌面系统
- 服务器系统:保证多进程/多线程服务的公平性,避免某一服务长期占用CPU;
- 嵌入式系统
- 云计算/容器:保证多个容器/虚拟机的CPU资源公平分配,符合云原生的资源隔离要求。
唯一的短板:CFS是为普通非实时进程设计的,对实时进程的支持有限——但Linux内核单独提供了实时调度器负责实时进程,两者协同工作,弥补了这一短板。
八、总结:CFS调度器的核心本质与学习意义
Linux CFS调度器的核心本质,是以“公平性”为设计核心,通过虚拟运行时间、动态权重、红黑树运行队列,实现了普通非实时进程的高效、公平调度,其抛弃固定时间片、以权重映射优先级的设计,彻底解决了传统调度器的缺陷,成为Linux内核的核心基石之一。
从内核视角看,CFS的核心逻辑可总结为三个核心、一个原则、一个映射:
- 三个核心:调度实体(sched_entity)、CFS运行队列(cfs_rq)、虚拟运行时间(vruntime);
- 一个原则
- 一个映射:nice值(静态优先级)→ 权重 → vruntime增长速度/动态时间片,实现优先级对调度的影响。
核心学习收获
- Linux进程调度分为实时调度和普通调度,CFS是普通非实时进程的默认调度器,实时进程永远优先执行;
- CFS抛弃了固定时间片,动态计算进程的最大运行时间,计算依据为调度周期×进程权重/总权重;
- 虚拟运行时间(vruntime)是CFS的公平性标尺,其累加值与进程权重成反比,权重越高,vruntime增长越慢;
- CFS将所有可运行进程的调度实体组织在红黑树中,最左节点为vruntime最小的进程,调度选择的时间复杂度为O(logN);
- Linux普通进程的优先级(nice值),底层是通过固定映射表转换为权重,CFS通过权重实现优先级对调度的影响,不直接使用nice值;
- 进程切换的核心是上下文保存与恢复,Linux通过TLB缓存优化页表切换开销,让调度切换的总开销控制在微秒级。