CFS调度器类
首先明确一点,CFS并不是调度器,而是一种调度器类。
传统的调度器使用时间片的概念,对系统中的进程分别计算时间片,使得进程运行至时间片结束。在所有的进程时间片都以用尽后,重新计算时间片。而CFS调度器完全摒弃了时间片,会重点考虑进程的等待时间。
CFS调度器的目的是,向系统中每个激活的进程提供最大的公正性,或者说确保没有进程被亏待。注意CFS调度器仅对作用于调度类型为SCHED_NORMAL, SCHED_IDLE和SCHED_BATCH的进程。对于调度类型为SCHED_RR和SCHED_FIFO的实时进程,CFS调度器类并不起作用
CFS引入了虚拟时钟的概念,该时钟的流逝速度小于实时时钟,精确的速度依赖于当前等待调度器挑选的进程的数目。假如队列上有4个进程,那么虚拟时钟的速度是实际时钟的1/4。如果实时时钟过了20秒,那么虚拟时钟仅仅是5秒,从完全公平的方式来分配CPU,那么虚拟时钟的5秒恰好可以作为这个基准。
假定就绪队列的虚拟时钟为fair_clock,进程的等待时间保存在wait_runtime,fair_clock表达了在完全公平条件下,进程需要运行的时间,而wait_runtime则表明了该进程受到的不公平程度。fait_clock - wait_runtime越小表示越不公平,因此越需要被调度执行。
CFS引入了红黑树来管理就绪队列中的进程,键值使用fair_clock-wait_runtime,因此最左边的节点表示最不公平的进程。在这个进程获得运行后,键值要减去已经运行的时间,这样在这棵红黑树中被移动到了右边的一点。此时,另外一个进程变成了最左边的节点,下一次会被调度器选择执行。
上面的模型仅仅是一种理想的情况,实际运用中,我们还要考虑很多因素
- 进程的不同优先级,高优先级的进程理应获得更多的时间
- 进程不能切换太频繁,切换会导致进程上下文改变,页表需要重新刷新,代价很大。
调度器数据结构
调度器
调度器使用一系列数据结构来排序和管理系统中的进程。调度器的工作方式与这些结构的设计密切相关。
可以使用两种方法激活调度:
1. 直接激活,比如进程打算休眠或者其他原因放弃CPU
2. 周期性机制,内核以固定的频率,不时的检测是否有必要进行进程切换。
task_struct成员
各个进程的task_struct有几个结构和调度相关。
struct task_struct {
......
int prio, static_prio, normal_prio;
struct list_head run_list;
const struct sched_class *sched_class;
struct sched_entity se;
unsigned int policy;
cpumask_t cpus_allowed;
unsigned int time_slice;
unsigned int rt_priority;
.......
}
static_prio是进程的静态优先级,是在进程启动时分配的优先级,可以通过nice调用设置优先级
prio和normal_prio是进程的动态优先级,
rt_priority是实时进程的优先级,最低的实时优先级是0,最高的实时优先级是99,值越大优先级越高,这和普通进程的静态优先级恰好相反
sched_class 是进程所属的调度器类
policy表示了该进程的调度策略,当前支持5种调度策略:
- SCHED_NORMAL,我们主要讲这一类进程,CFS调度策略可以处理此类进程
- SCHED_BATCH和SCHED_IDLE也通过CFS调度策略处理此类进程
- SCHED_RR和SCHED_FIFO用于实现软实时进程。这些不是由CFS调度类来出的,而是使用实时调度器类来处理的。
调度类
调度器类提供了通用调度器和各个调度方法之间的关联。调度器类由特定数据结构中汇集的几个函数指针表示。全局调度器请求的各个操作都是可以由一个指针表示。这使得无需了解不同调度器类的内部工作原理,即可创建通用调度器。
对于各个调度类,都必须提供struct sched_class的一个实例。调度类之间的层次结构是平坦的:实时进程最终要,在完全公平进程之前处理;而完全公平进程则优先于空闲进程,空闲进程只有CPU无事可做时才处于活动状态。
用户进程无法直接和调度器类交互,他们只需要定义policy为SCHED_xyz。调度器会负责根据调度策略找到相应的调度器类。SCHED_NORMAL, SCHED_BATCH, SCHED_IDLE会映射为fair_sched_class,而SCHED_RR和SCHED_FIFO与rt_sched_class关联。fair_sched_class和rt_sched_class都是struct sched_class的实例,分别表示完全公平调度器和实时调度器。
就绪队列
核心调度器用来管理活动进程的主要数据结构称为就绪队列。各个CPU都有自己的就绪队列,各个活动进程只出现在一个就绪队列中。在多个CPU中同时运行一个进程是不可能的。
就绪队列是全局调度器许多操作的起点。但是注意,进程并不是由就绪队列的成员直接管理的。这是调度器的职责,因此在就绪队列中内嵌了特定与调度器类的子就绪队列。
就绪队列是使用下列数据结构实现的。
所有的就绪队列都在runqueues数组中,该数组的每个元素分别对应于系统中的一个CPU。在单处理器系统中,由于只有一个CPU,因此数组仅有一个元素。
271 /*
272 * This is the main, per-CPU runqueue data structure.
273 *
274 * Locking rule: those places that want to lock multiple runqueues
275 * (such as the load balancing or the thread migration code), lock
276 * acquire operations must be ordered by ascending &runqueue.
277 */
278 struct rq {
279 /* runqueue lock: */
280 spinlock_t lock;
281
282 /*
283 * nr_running and cpu_load should be in the same cacheline because
284 * remote CPUs use both these fields when doing load calculation.
285 */
286 unsigned long nr_running;
287 #define CPU_LOAD_IDX_MAX 5
288 unsigned long cpu_load[CPU_LOAD_IDX_MAX];
289 unsigned char idle_at_tick;
290 #ifdef CONFIG_NO_HZ
291 unsigned char in_nohz_recently;
292 #endif
293 /* capture load from *all* tasks on this cpu: */
294 struct load_weight load;
295 unsigned long nr_load_updates;
296 u64 nr_switches;
297
298 struct cfs_rq cfs;
299 #ifdef CONFIG_FAIR_GROUP_SCHED
300 /* list of leaf cfs_rq on this cpu: */
301 struct list_head leaf_cfs_rq_list;
302 #endif
303 struct rt_rq rt;
304
305 /*
306 * This is part of a global counter where only the total sum
307 * over all CPUs matters. A task can increase this counter on
308 * one CPU and if it got migrated afterwards it may decrease
309 * it on another CPU. Always updated under the runqueue lock:
310 */
311 unsigned long nr_uninterruptible;
312
313 struct task_struct *curr, *idle;
314 unsigned long next_balance;
315 struct mm_struct *prev_mm;
316
317 u64 clock, prev_clock_raw;
318 s64 clock_max_delta;
319
320 unsigned int clock_warps, clock_overflows;
321 u64 idle_clock;
322 unsigned int clock_deep_idle_events;
323 u64 tick_timestamp;
324
325 atomic_t nr_iowait;
326
327 #ifdef CONFIG_SMP
328 struct sched_domain *sd;
329
330 /* For active balancing */
331 int active_balance;
332 int push_cpu;
333 /* cpu of this runqueue: */
334 int cpu;
335
336 struct task_struct *migration_thread;
337 struct list_head migration_queue;
304
305 /*
306 * This is part of a global counter where only the total sum
307 * over all CPUs matters. A task can increase this counter on
308 * one CPU and if it got migrated afterwards it may decrease
309 * it on another CPU. Always updated under the runqueue lock:
310 */
311 unsigned long nr_uninterruptible;
312
313 struct task_struct *curr, *idle;
314 unsigned long next_balance;
315 struct mm_struct *prev_mm;
316
317 u64 clock, prev_clock_raw;
318 s64 clock_max_delta;
319
320 unsigned int clock_warps, clock_overflows;
321 u64 idle_clock;
322 unsigned int clock_deep_idle_events;
323 u64 tick_timestamp;
324
325 atomic_t nr_iowait;
326
327 #ifdef CONFIG_SMP
328 struct sched_domain *sd;
329
330 /* For active balancing */
331 int active_balance;
332 int push_cpu;
333 /* cpu of this runqueue: */
334 int cpu;
335
336 struct task_struct *migration_thread;
337 struct list_head migration_queue;
nr_running指定了队列上可运行进程的数目,包括所有优先级和调度类上的可运行进程。
load提供了就绪队列当前的负载度量,负载本质上和当前就绪队列上活动进程的数目成正比,其中每个进程还要使用他们的优先级作为权重。每个就绪队列的虚拟时钟就依赖于这个信息。
cpu_load 可以用来跟踪此前的CPU负荷负载状态,在周期性调度scheduler_tick中会调用update_cpu_load函数来更新CPU负荷状态,这个数组在负载迁移时会用到。
idle指向idle进程的task_struct结构。
cfs和rt是嵌入的子就绪队列,分别用于CFS调度和实时调度。
clock,prev_clock_raw,用于实现就绪队列自身的时钟,周期性调度器会调用__update_rq_clock更新这两个值,clock记录的是本次更新时的时钟值,而prev_clock_raw是上一次调用__update_rq_clock的值。
tick_timestamp,每次调用scheduler都会更新tick_timestamp为当前的tick
不重要成员变量
clock_warps 和clock_overflows,由于硬件的原因,在更新就绪队列时钟时系统会出现时间回滚,或者时间前跳的问题,这两个变量是为了统计需要,记录回滚和前跳发生的次数。
nr_load_updates 是系统调用update_cpu_load的次数。
由于调度器需要调度更具体化的实体,因此需要一个适当的数据结构来描述此类实体。定义如下:
struct sched_entity {
struct load_weight load; /* for load-balancing */
struct rb_node run_node;
unsigned int on_rq;
u64 exec_start;
u64 sum_exec_runtime;
u64 vruntime;
u64 prev_sum_exec_runtime;
}
load制定了权重,决定了各个实体占队列总负荷的比例。计算负荷权重是调度器的一项重要任务。因为CFS所需的虚拟时钟的速度最终依赖于负荷。
run_node是红黑树的节点,调度实体通过这个节点挂接在红黑树上
on_rq 表示该实体当前是否在就绪队列上调度。
sum_exec_runtime 在进程运行时,我们需要记录消耗的CPU时间,以变用于CFS调度。这个时间是实际时间。跟踪运行时间是由update_curr不断积累完成的。每次调用update_curr,都会计算当前时间和exec_start之间的差值,累加到sum_exec_runtime上,然后exec_start更新为当前时间。
vruntime 是执行期间,虚拟时间的流逝数量。
prev_sum_exec_runtime 在进程被撤销CPU时,sum_exec_runtime保存到prev_sum_exec_runtime中。在进程抢占时会用到该数据。
每一个task都会内嵌一个sched_entity,所以进程是一个可调度实体,当然可调度实体不一定是进程。