Linux内核源码分析 (4)实时调度类及SMP
一、实时调度类分析
- 实时进程与普通进程有一个根本的不同之处:如果系统中有一个实时进程且可运行,那么调度器总是会选中它运行,除非有另一个优先级更高的实时进程。
- 现有的两种实时类,不同之处如下所示。
- 循环进程(
SCHED_RR
)有时间片,其值在进程运行时会减少,就像是普通进程。在所有的时间段都到期后,则该值重置为初始值,而进程则置于队列的末尾。这确保了在有几个优先级相同的SCHED_RR
进程的情况下,它们总是依次执行。 - 先进先出进程(
SCHED_FIFO
)没有时间片,在被调度器选择执行后,可以运行任意长时间。
- 循环进程(
调度器数据结构
- 调度器使用一系列数据结构,来排序和管理系统中的进程。调度器的工作方式与这些结构的设计密切相关。几个组件在许多方面彼此交互。下面概述了这些组件的关联。下文中将这两个组件称为通用调度器(
generic scheduler
)或核心调度器(core scheduler
)。
- 主调度器:通过调用
schedule()
函数来完成进程的选择和切换。 - 周期性调度器:根据固定频率自动调用
scheduler_tick()
函数,不时检测是否有必要进行进程切换。 - 上下文切换:主要做两个事情(切换地址空间、切换寄存器和栈空间)
- 主调度器:通过调用
- 调度器类用于判断接下来运行哪个进程。内核支持不同的调度策略(完全公平调度、实时调度、在无事可做时调度空闲进程),调度类使得能够以模块化方法实现这些策略,即一个类的代码不需要与其他类的代码交互。在调度器被调用时,它会查询调度器类,得知接下来运行哪个进程。
- 在选中将要运行的进程之后,必须执行底层任务切换。这需要与CPU的紧密交互。
- 每个进程都刚好属于某一调度类,各个调度类负责管理所属的进程。通用调度器自身完全不涉及进程管理,其工作都委托给调度器类。
task_struct中与调度有关的的成员
include/linux/<sched.h>
struct task_struct {
...
/*prio和normal_prio表示动态优先级,static_prio表示进程的静态优先级*/
int prio, static_prio, normal_prio;
/*表示实时进程的优先级,范围是[0,99]*/
unsigned int rt_priority;
/*表示该进程所属的调度器类*/
const struct sched_class *sched_class;
/*调度实体的实例。注意:调度器不限于调度进程,还可以处理更大的实体。这可以用于实现组调度:可用的CPU时间
可以首先在一般的进程组(例如,所有进程可以按所有者分组)之间分配,接下来分配的时间在组内再次分配。*/
struct sched_entity se;
/*policy保存了对该进程应用的调度策略,Linux支持5种可能的值:
SCHED_NORMAL: 通过完全公平调度器来处理,用于普通进程
SCHED_BATCH: 通过完全公平调度器来处理,用于非交互、CPU使用密集的批处理进程,不会干扰交互式进程
SCHED_IDLE: 通过完全公平调度器来处理,其相对权重总是最小的。
SCHED_RR: 通过实时调度器处理,用于实现软实时进程,实现了一种循环方法
SCHED_FIFO: 通过实时调度器处理,用于实现软实时进程,实现了一种先进先出机制。
*/
unsigned int policy;
/*一个位域,在多处理器系统上使用,用来限制进程可以在哪些CPU上运行*/
cpumask_t cpus_allowed;
/*是一个表头,用于维护包含各进程的一个运行表,该成员实时调度器需要,不用于CFS*/
struct list_head run_list;
/*指定进程可使用CPU的剩余时间段,该成员实时调度器需要,不用于CFS*/
unsigned int time_slice;
...
}
就绪队列
- 核心调度器用于管理活动进程的主要数据结构称之为就绪队列。各个CPU都有自身的就绪队列,各个活动进程只出现在一个就绪队列中。在多个CPU上同时运行一个进程是不可能的(但发源于同一进程的各线程可以在不同处理器上执行,因为进程管理对进程和线程不作重要的区分)。
- 注意,进程并不是由就绪队列(
rq
)的成员直接管理的!这是各个调度器类(如stop_sched_class
、dl_sched_class
、rt_sched_class
、fair_sched_class
和idle_sched_class
)的职责,因此在各个就绪队列中嵌入了特定于调度器类的子就绪队列(struct cfs_rq cfs;
和struct rt_rq rt;
)。 - 就绪队列主要使用下列数据结构实现
kernel/sched.cstruct rq { /*指定了队列上可运行进程的数目,不考虑其优先级或调度类*/ unsigned long nr_running; ... /*提供了就绪队列当前负荷的度量,队列的负荷本质上与队列上当前活动进程的数目成正比。*/ struct load_weight load; #define CPU_LOAD_IDX_MAX 5 /*用于跟踪此前的负荷状态*/ unsigned long cpu_load[CPU_LOAD_IDX_MAX]; /*cfs和rt是嵌入的子就绪队列,分别用于完全公平调度器和实时调度器。*/ struct cfs_rq cfs; struct rt_rq rt; /*curr指向当前运行的进程的task_struct实例。idle指向idle进程的task_struct实例, 该进程亦称为idle线程,在无其他可运行进程时执行*/ struct task_struct *curr, *idle; /*用于实现就绪队列自身的时钟。每次调用周期性调度器时,都会更新clock的值。*/ u64 clock; ... };
- 系统的所有就绪队列都在
runqueues
数组中,该数组的每个元素分别对应于系统中的一个CPU
。在单处理器系统中,由于只需要一个就绪队列,数组只有一个元素。
kernel/sched.cstatic DEFINE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues);
调度实体
- 由于调度器可以操作比进程更一般的实体(进程嵌入了
sched_entity
实例,所以进程是可调度实体,还有更大的可调度实体比如组调度等 ),因此需要一个适当的数据结构来描述此类实体。其定义如下:
include/linux/<sched.h>struct sched_entity { /*用于负载均衡,决定了各个实体占队列总负荷的比例*/ struct load_weight load; /*run_node是标准的树结点,使得实体可以在红黑树上排序*/ struct rb_node run_node; /*表示该实体当前是否在就绪队列上接受调度*/ unsigned int on_rq; /*记录消耗的CPU时间,以用于完全公平调度器。跟踪运行时间是由update_curr不断累积完成的,调度器中许多地方都会调用该函数,例如,新进程加入就绪队列时,或者周期性调度器中。每次调用时,会计算当前时间和exec_start之间的差值,exec_start则更新到当前时间。差值则被加到sum_exec_runtime。*/ u64 sum_exec_runtime; u64 exec_start; /*在进程执行期间虚拟时钟上流逝的时间数量*/ u64 vruntime; /*在进程被撤销CPU时,其当前sum_exec_runtime值保存到prev_exec_runtime。此后,在进程抢占时又需要该数据。 注意:此过程并不会更新sum_exec_runtime。*/ u64 prev_sum_exec_runtime; ... }