目录
二、完全公平调度器(Completely Fair Scheduler, CFS)
一、Linux内核中的调度器
1、调度器
1.1、概念
在Linux内核中,调度器(Scheduler)是操作系统内核的关键组件之一,负责管理和协调CPU资源在多个进程间高效、公正地分配。调度器的主要目标是确保CPU时间片能在系统中的各个进程间合理分配,从而最大化系统资源利用率,同时也要考虑进程的响应时间和整体系统性能。
1.2、结构框图
Linux内核中用来安排调度进程 (一段程序的执行过程) 执行的模块称为调度器(Scheduler),它可以切换进程状态 (Process status) 。比如: 执行、可中断睡眠、不可中断睡眠、退出、暂停等。
调度器是 CPU 中央处理器的管理员,主要负责完成做两件事情:一、选择某些就绪进 程来执行。二、是打断某些执行的进程让它们变为就绪状态。调度器分配CPU 时间的基本依据:进程的优先级。上下文 切换(context switch ):将进程在 CPU 中切换执行的过程,内核承担此任务,负责重建和存储被切换掉之前的CPU 状态。
1.3、Linux下的调度类介绍(sched_class)
①、sched_class数据结构介绍
调度类是内核调度框架中更为抽象的概念,它是一种组织和管理调度策略的结构化方式。在Linux内核中,每个调度类都对应一个具体的调度策略,或者一套相关的调度策略,并提供了一系列与调度相关的函数接口,如进程入队、出队、选择下一个运行进程等操作。
sched_class结构体是在代码中体现调度类,描述了调度器的基本操作接口,内核根据不同调度策略(如CFS、实时调度等)实现了对应的调度类,这些调度类则通过实现下面的接口来参与到内核的整体调度流程中。
数据结构定义在kernel/sched/sched.h
struct sched_class {
#ifdef CONFIG_UCLAMP_TASK
int uclamp_enabled;
#endif
/* 将进程加入到执行队列当中,即将调度实体(进程)存放到红黑树中,并对nr_running变量自动会加1*/
void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
/* 从执行队列当中删除进程,并对nr_running变量自动减1 */
void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
/*放弃CPU执行权,实际上该函数执行先出队后入队,在这种情况下,它直接将调度实体放在红黑树的最右端 */
void (*yield_task) (struct rq *rq);
bool (*yield_to_task)(struct rq *rq, struct task_struct *p);
/* 用于检查当前进程是否可被新进程抢占 */
void (*check_preempt_curr)(struct rq *rq, struct task_struct *p, int flags);
/* 选择下一个应用要运行的进程*/
struct task_struct *(*pick_next_task)(struct rq *rq);
/*将进程放回到运行队列当中 */
void (*put_prev_task)(struct rq *rq, struct task_struct *p);
void (*set_next_task)(struct rq *rq, struct task_struct *p, bool first);
#ifdef CONFIG_SMP
int (*balance)(struct rq *rq, struct task_struct *prev, struct rq_flags *rf);
/* 为进程选择一个合适的CPU */
int (*select_task_rq)(struct task_struct *p, int task_cpu, int flags);
struct task_struct * (*pick_task)(struct rq *rq);
/* 迁移任务到另一个CPU */
void (*migrate_task_rq)(struct task_struct *p, int new_cpu);
/*专门用于唤醒进程 */
void (*task_woken)(struct rq *this_rq, struct task_struct *task);
/* 修改进程在CPU的亲和力 */
void (*set_cpus_allowed)(struct task_struct *p,
const struct cpumask *newmask,
u32 flags);
/* 启动运行队列 */
void (*rq_online)(struct rq *rq);
/* 禁止运行队列 */
void (*rq_offline)(struct rq *rq);
struct rq *(*find_lock_rq)(struct task_struct *p, struct rq *rq);
#endif
/* 调用自time_tick函数,它可能引起进程切换,将驱动运行时(running)抢占 */
void (*task_tick)(struct rq *rq, struct task_struct *p, int queued);
/* 当进程创建的时候调用、不同调度策略的进程初始化也是不一样的*/
void (*task_fork)(struct task_struct *p);
/* 进程退出的时候使用*/
void (*task_dead)(struct task_struct *p);
/*
* The switched_from() call is allowed to drop rq->lock, therefore we
* cannot assume the switched_from/switched_to pair is serialized by
* rq->lock. They are however serialized by p->pi_lock.
*/
/* 专门用于进程的切换*/
void (*switched_from)(struct rq *this_rq, struct task_struct *task);
void (*switched_to) (struct rq *this_rq, struct task_struct *task);
/*进程优先级的更改 */
void (*prio_changed) (struct rq *this_rq, struct task_struct *task,
int oldprio);
unsigned int (*get_rr_interval)(struct rq *rq,
struct task_struct *task);
void (*update_curr)(struct rq *rq);
#ifdef CONFIG_FAIR_GROUP_SCHED
/* */
void (*task_change_group)(struct task_struct *p);
#endif
};
比较重要的几个成员接口:
- enqueue_task: 向就绪队列添加一个进程,某个任务进入可运行状态时,该函数将会调用,它将调度实体放入到红黑树当中。
- dequeue_task: 将一个进程从就绪队列中进行删除,当某个任务退出可运行状态时调用该函数,将从红黑树中去掉对应调度实体。
- yield_task:在进程想要资源放弃对处理器的控制权时,可使用在sched_yiled系统调用,会调用内核API去处理操作。
- check_preempt_curr: 检查当前运行的任务是否被抢占。
- pick_next_task: 选择下来要运行的最合适的实体 (进程)。
- put_prev_task: 用于另一个进程代替当前运行的进程。
- set_curr_task: 当任务修改它调用类或修改它的任务组时,将调用这个函数。
- task_tick: 在每次激活周期调度器时,由周期性调度器调用。
②、Linux下的五大调度类
Linux内核中实例化了五个调度类,代码如下:
extern const struct sched_class stop_sched_class;
extern const struct sched_class dl_sched_class;
extern const struct sched_class rt_sched_class;
extern const struct sched_class fair_sched_class;
extern const struct sched_class idle_sched_class;
stop_sched_class
停机调度类: 优先级是最高的调度类,停机进程是优先级最高的进程,可以抢占所有其它进程,其他进程不可能抢占停机进程。- dl_sched_class 指Deadline调度类,它指的是具有固定截止期限(deadline)的调度策略,这类策略适用于对完成时间有严格限制的任务
rt_sched_class
表示实时调度类(Real-Time Scheduling Class),这是用于实时进程调度的策略,包括SCHED_FIFO(先进先出)和SCHED_RR(循环轮转)两种调度策略。实时进程在调度时可以获得更高的优先级,并能抢占其他非实时进程以保证及时响应。fair_sched_class
即完全公平调度类(Completely Fair Scheduler, CFS),它是Linux内核自2.6.23以来用于普通进程的标准调度策略。CFS设计的目标是在所有可运行进程之间公平地分配CPU时间,基于进程的动态优先级(由nice值和CPU使用时间等因素决定)进行调度。idle_sched_class
是空闲调度类,它负责调度idle任务,即idle进程。当系统的所有其他进程都不需要CPU时,idle进程会被调度执行,通常它的作用是降低CPU利用率,避免无谓的忙碌等待,并在必要时触发电源管理相关的操作。
③、调度器结构分析
当处理器空闲时,它会向主调度器请求分配新任务,主调度器依据任务列表及其优先级等信息作出决策,通过上下文切换将CPU使用权转交给合适的任务。此外,周期性调度器定期唤醒主调度器以重新评估系统负载,并据此可能调整任务执行计划,如在高负载时减少低优先级任务执行,确保关键服务获得足够资源。
1.5、调度优先级
![](https://i-blog.csdnimg.cn/blog_migrate/17f68ef6ae1e2ca03643644cfb5907ed.png)
实时进程与普通进程有着明确的优先级区间:实时进程的优先级范围是0至99,这类进程往往对应于对响应时间和执行效率要求极高的应用,比如音频视频流媒体播放、工业控制等领域。数字越小的实时进程享有更高的优先级,因此在争夺CPU资源时能优先得到执行。
另一方面,普通进程的优先级设定在100至139之间,这类进程主要包括日常的非实时应用,如Web服务器、数据库管理系统等。同样遵循数值越小优先级越高的原则,即使在普通进程中,较小优先级的进程也会相对优先被执行。
1.6、调度策略
Linux内核提供了一些调度策略用来选择调度器
/*
* Scheduling policies
*/
#define SCHED_NORMAL 0
#define SCHED_FIFO 1
#define SCHED_RR 2
#define SCHED_BATCH 3
/* SCHED_ISO: reserved but not implemented yet */
#define SCHED_IDLE 5
#define SCHED_DEADLINE 6
-
SCHED_NORMAL
:- 定义了正常进程调度策略,即非实时调度策略。在Linux内核中,使用完全公平调度器(Completely Fair Scheduler, CFS)来实现,它旨在为所有进程提供公平的CPU时间分配,依据进程的nice值(优先级)和虚拟运行时间来平衡各进程的执行机会。
-
SCHED_FIFO
:- 代表先进先出(First In, First Out)的实时调度策略。在该策略下,实时进程按到达就绪队列的顺序执行,具有该调度策略的进程一旦获得CPU就不会被更低优先级的进程抢占,除非自身主动释放CPU,或者有更高优先级的SCHED_FIFO或SCHED_RR实时进程变得可运行。
-
SCHED_RR
:- 轮转(Round Robin)实时调度策略,类似于SCHED_FIFO,但是实时进程在一个时间片(quantum)结束之后会被放置到同优先级实时进程队列的末尾,等待再次轮到自己执行,这样保证同一优先级的实时进程可以轮流获得执行机会。
-
SCHED_BATCH
:- 批处理调度策略,适用于CPU密集型且对响应时间要求不高的批处理作业。使用此策略的进程会尽量减少上下文切换,从而提高CPU缓存的局部性并减少开销,有利于提高整体系统吞吐量。
-
SCHED_IDLE
:- 空闲调度策略,应用于低优先级后台任务。这类任务只有在系统完全空闲时才会得到执行,不会影响其他任何进程的调度。
-
SCHED_DEADLINE
:- 截止期限(Deadline)调度策略,专为那些有严格截止时间约束的进程设计。此类进程需要在预设的截止时间内完成计算任务,调度器会确保进程在规定时间内获得足够的CPU资源来满足其执行需求。
SCHED_NORMAL、SCHED_BATCH、SCHED_IDLE直接被映射到fair_sched_class
SCHED_RR、SCHED_FIFO与rt_schedule_class进行相关联
二、完全公平调度器(Completely Fair Scheduler, CFS)
1、CFS概念和特性
CFS全称为Completely Fair Scheduler(完全公平调度器),是Linux内核中用于普通(非实时)进程调度的主要算法,从Linux 2.6.23内核版本开始成为默认的调度策略。CFS的设计目标是确保所有进程在长期内都能公平地共享CPU资源,而不考虑进程的类型或历史执行情况。
CFS主要引入了下面几个特性:
-
虚拟运行时间(vruntime):CFS摒弃了传统固定时间片的轮转调度方式,转而使用虚拟运行时间来衡量进程的执行时间。每个进程在运行时,其虚拟运行时间会逐渐增加,未运行的进程则保持不变。调度器会比较所有进程的虚拟运行时间来决定下一个应该运行的进程,确保所有进程在虚拟运行时间上的相对差距反映其应该得到的CPU时间比例。
-
权重计算:CFS根据进程的nice值(优先级)来调整其权重。nice值范围通常是从-20(最高优先级)到19(最低优先级,默认值为0)。nice值越高,进程的权重越小,意味着它在同等时间内获得的CPU份额越少,反之亦然。
-
红黑树排序:CFS使用红黑树来组织就绪队列,进程节点按照其虚拟运行时间排序。每次调度时,会选择vruntime最小的进程执行,这样可以保证所有进程在长时间运行后达到近似的执行时间比例。
-
动态优先级调整: 随着进程的执行,CFS会动态调整进程的优先级,确保即使在短时间段内也能表现得相对公平。当一个进程使用完其相对应的CPU时间后,其虚拟运行时间将会增加,从而降低其在红黑树中的优先级。
-
其他特性:CFS还支持多核环境下的负载均衡,即在不同CPU间迁移进程以达到总体负载的均衡分布。此外,CFS设计时考虑了系统性能和电源管理,确保在兼顾公平性的同时,还能在适当时候将CPU资源交给idle进程,进而触发节能措施。
完全公平调度算法(CFS)体现在对待每个进程都是公平的,让每个进程都运行一段相同的时间片,这就是基于时间片轮询调度算法。
CFS定义一种新调度模型,它给cfs rg (cfs的run queue) 中的每一个进程都设置一个虚拟时钟-virtual runtime(vruntime)。如果一个进程得以执行,随着执行时间的不断增长,vruntime也将不断增大,没有得到执行的进程vruntime将保持不变。根据这种“不公平时间”也就是就绪队列中进程的等待时间分配CPU资源。
2、CFS如何实现“完全公平”
在Linux内核的Completely Fair Scheduler (CFS)调度器中,真实运行时间和虚拟运行时间是用来衡量进程执行状况的两个重要概念:
- 真实运行时间(Real Runtime): 真实运行时间是指进程在物理CPU上实际消耗的时间,即从进程开始执行到目前为止,所经历的 wall clock 时间。这个时间包括进程在用户空间执行的时间(即进程执行指令的时间)以及在内核空间执行系统调用和服务的时间。真实运行时间是由系统时钟精确测量的,反映了进程实际占用CPU资源的情况。
- 虚拟运行时间(Virtual Runtime, vruntime): 虚拟运行时间在CFS调度器中是决定进程调度顺序的关键因素。它并不是指进程实际占用CPU的时间,而是经过权重调整后用来衡量进程“应当”消耗的CPU时间。CFS调度器根据每个进程的nice值、进程优先级和其他相关因素计算得出每个进程的虚拟运行时间,并用它来决定下一个将被执行的进程。在CFS调度器的红黑树结构中,虚拟运行时间最短的进程将被优先调度。
在CFS中真实运行时间决定了进程对CPU资源占用时间,而虚拟运行时间只是决定了就绪队列中的调度次序。虚拟运行时间虽不直接影响进程实际占用CPU的时间长度,但确实决定了进程在就绪队列中的排序和调度器选择下一个进程的决策依据。在CFS调度策略下,通过维护虚拟运行时间,内核可以确保在长时段内,所有进程都能得到与其优先级和系统负载相对应的CPU时间分配,从而实现“完全公平”的调度效果。
3、虚拟运行时间和真实运行时间的计算
举两个简单的例子加深理解一下虚拟运行时间和真实运行时间
- 假设系统有两个进程A和B,初始时,它们的虚拟运行时间(vruntime)均为0。
- 进程A的nice值为0(权重较高),进程B的nice值为1(权重较低)。
- 假设一个时间片(tick)的长度为1ms。
- 当进程A连续运行了5个时间片(即5ms)后,其真实运行时间为5ms。
- 由于CFS调度器会根据权重调整虚拟运行时间的增长速率,此处假设进程A的权重是B的两倍,那么在这5ms内,进程A的虚拟运行时间增长比进程B更快。
- 假设虚拟运行时间的增长与权重成反比(仅为简化举例),则在5ms后,进程A的虚拟运行时间增长了5ms,而进程B若也运行5ms,则其虚拟运行时间只增长了2.5ms(假设权重比为1:2,增长速度为A的一半)。
- 在下次调度的时候会选择虚拟运行时间最短的进程进行调度
三、实时调度器类
1、实时调度类
1.1、概念
实时调度类是Linux内核中专门为实时任务设计的调度策略集合。实时任务是指那些对响应时间有严格要求的应用程序或进程,必须在一定时限内完成特定任务,否则可能导致严重后果,如数据丢失、设备损坏、系统崩溃等。实时调度类确保这些任务能在任何时刻都能够获得足够的CPU资源,优先于非实时任务执行。
1.2、实时调度实体
struct sched_rt_entity {
struct list_head run_list;
unsigned long timeout;
unsigned long watchdog_stamp;
unsigned int time_slice;
unsigned short on_rq;
unsigned short on_list;
struct sched_rt_entity *back;
#ifdef CONFIG_RT_GROUP_SCHED
struct sched_rt_entity *parent;
/* rq on which this entity is (to be) queued: */
struct rt_rq *rt_rq;
/* rq "owned" by this entity/group: */
struct rt_rq *my_q;
#endif
} __randomize_layout;
- run_list:
list_head
类型的结构,这是一个双向链表头,用于将实时进程实体链接到其所在运行队列(rt_rq)的实时就绪队列列表中。当进程准备好运行时,它会被插入到这个链表中。 - timeout:一个长整型变量,表示该实时进程下次可能超时的时间点。在实时调度中,尤其是对于SCHED_RR(轮转实时调度策略)中的进程,这个字段用于跟踪进程剩余的时间片。
- watchdog_stamp:另一个长整型变量,通常用于记录实时进程上次被调度器监视的时间戳,它可以用于监控进程的执行是否超过预期时间,以防止死锁或其他调度异常。
- time_slice:整型变量,表示实时进程在SCHED_RR调度策略下的时间片大小。在轮转调度中,每个实时进程在一个时间片结束时会被重新放回就绪队列的末尾等待再次调度。
- on_rq 和 on_list:这两个 unsigned short 类型的标志位,用于标记实时进程调度实体是否已经加入到运行队列(runqueue,rq)或某种列表中。on_rq 通常用于指示进程是否正在运行队列上等待调度,而 on_list 可能用于记录进程在其他列表中的状态。
- back:指向另一个
struct sched_rt_entity
的指针,用于在红黑树或其他数据结构中维护前后关系,方便在调度过程中遍历和查找。 - parent 和 rt_rq:在支持实时进程组调度(CONFIG_RT_GROUP_SCHED)的配置下,parent 指针指向实时进程所属的父调度实体(如进程组的代表实体),rt_rq 指针则指向当前进程所在的实时运行队列。
- my_q:也是一个指向
struct rt_rq
的指针,它代表了该实体拥有的或者“关联”的运行队列,可能在进程组调度时使用,表示该进程组或实体自身的资源队列。
1.3实时调度类
const struct sched_class rt_sched_class = {
.next = &fair_sched_class,
.enqueue_task = enqueue_task_rt,
.dequeue_task = dequeue_task_rt,
.yield_task = yield_task_rt,
.check_preempt_curr = check_preempt_curr_rt,
.pick_next_task = pick_next_task_rt,
.put_prev_task = put_prev_task_rt,
#ifdef CONFIG_SMP
.select_task_rq = select_task_rq_rt,
.set_cpus_allowed = set_cpus_allowed_common,
.rq_online = rq_online_rt,
.rq_offline = rq_offline_rt,
.task_woken = task_woken_rt,
.switched_from = switched_from_rt,
#endif
.set_curr_task = set_curr_task_rt,
.task_tick = task_tick_rt,
.get_rr_interval = get_rr_interval_rt,
.prio_changed = prio_changed_rt,
.switched_to = switched_to_rt,
.update_curr = update_curr_rt,
};
-
.next
: 指向下一个调度类,这里是&fair_sched_class
,即完全公平调度类。这意味着在实时调度类无法调度任务时,会尝试转到公平调度类进行调度。 -
.enqueue_task
: 指向enqueue_task_rt
函数,用于将一个实时进程实体加入到运行队列(runqueue)中。 -
.dequeue_task
: 指向dequeue_task_rt
函数,用于从运行队列中移除一个实时进程实体。 -
.yield_task
: 指向yield_task_rt
函数,处理实时进程主动放弃CPU执行权的操作。 -
.check_preempt_curr
: 指向check_preempt_curr_rt
函数,检查当前运行的进程是否可以被新到达的实时进程抢占。 -
.pick_next_task
: 指向pick_next_task_rt
函数,用于选择下一个将要执行的实时进程。 -
.put_prev_task
和.set_curr_task
: 分别指向put_prev_task_rt
和set_curr_task_rt
函数,用于调度过程中的任务切换操作。 -
#ifdef CONFIG_SMP
部分:.select_task_rq
: 指向select_task_rq_rt
函数,在多处理器(SMP)环境下,用于为进程选择合适的运行队列(CPU)。.set_cpus_allowed
: 指向set_cpus_allowed_common
函数,用于设置进程允许运行的CPU集合。.rq_online
和.rq_offline
: 分别指向rq_online_rt
和rq_offline_rt
函数,用于在线或离线运行队列时的处理。.task_woken
: 指向task_woken_rt
函数,当实时进程被唤醒时调用。.switched_from
: 指向switched_from_rt
函数,当进程从某CPU上下文切换出去时调用。
-
.task_tick
: 指向task_tick_rt
函数,在时钟节拍中断处理时调用,可能会触发进程调度。 -
.get_rr_interval
: 指向get_rr_interval_rt
函数,获取实时进程的时间片长度。 -
.prio_changed
: 指向prio_changed_rt
函数,实时进程优先级发生改变时调用。 -
.switched_to
: 指向switched_to_rt
函数,当进程被切换到CPU上准备执行时调用。 -
.update_curr
: 指向update_curr_rt
函数,用于更新当前正在运行的实时进程的相关信息。
1.4、实时调度类和调度实体之间的关系
在Linux内核中,实时调度类定义了一套针对实时进程的调度算法和操作,而调度实体(如
struct sched_rt_entity
)则是这些算法操作的具体载体。每个实时进程都会封装在其对应的调度实体中,实时调度类通过调度实体来管理和调度这些实时进程,包括但不限于进程的入队、出队、优先级更改、时间片分配、抢占检查等一系列调度相关的操作。换言之,实时调度类提供了调度策略,而调度实体则是这些策略作用的具体对象。
2、实时调度类用到的调度策略
-
SCHED_FIFO(先进先出实时调度策略): 使用SCHED_FIFO策略的实时进程一旦获得CPU执行权,就会一直运行下去,直到该进程自愿放弃CPU(如调用了阻塞系统调用或显式释放CPU),或者有更高优先级的实时进程变得可运行。实时进程按照优先级排队,优先级高的进程始终排在优先级低的进程之前。
-
SCHED_RR(轮转实时调度策略): 类似于SCHED_FIFO,但每个SCHED_RR实时进程在执行完一个时间片(quantum)后,即使没有完成任务,也会被迫让出CPU给同一优先级的其他SCHED_RR进程。这样一来,同一优先级的实时进程能够实现时间片轮转,确保在紧迫性相同的情况下公平分配CPU时间。
实时进程的优先级通常通过nice值来表示,但与非实时进程(采用CFS调度策略)的nice值不同,实时进程的优先级范围通常是0至MAX_RT_PRIO(通常是99,取决于内核配置)。优先级越高,表明该进程的实时性要求越强,获得CPU的可能性也就越大。
总结
本文全面探讨了Linux内核的调度器机制,首先从基础概念入手,阐述了调度器在操作系统中扮演的角色——负责管理和协调系统中各个进程对CPU资源的访问,确保系统整体性能和响应能力。文章深入解析了Linux内核对调度器的具体实现。
接下来,文章详尽解读了Linux内核中的两种主要调度策略:Completely Fair Scheduler (CFS) 和实时调度类。CFS作为Linux内核对非实时进程采用的默认调度策略,通过虚拟运行时间(vruntime)确保所有进程在长期运行中能得到公平的CPU时间分配,兼顾了系统响应速度和资源利用率。而实时调度类则面向对时间敏感的实时进程,提供了SCHED_FIFO(先进先出)和SCHED_RR(时间片轮转)两种调度策略,确保实时进程能在满足其时间约束的前提下得到优先执行权,以满足实时性要求。