Linux 进程调度


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,所以进程是一个可调度实体,当然可调度实体不一定是进程。














评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值