linux内核之进程调度

来源:互联网 发布:ug编程安全几何体 编辑:程序博客网 时间:2024/05/16 15:49

进程调度程序可看做在可运行态进程之间分配有限的处理器时间资源的内核子系统。Linux提供了一种抢占式的多任务模式。

1、调度策略

         Linux主要使用了以下集中调度策略(include/uapi/linux/sched.h):

#define SCHED_NORMAL    0                                                                                                                    

#define SCHED_FIFO        1                                                                                                                         

#define SCHED_RR         2                                                                                                                        

#define SCHED_BATCH     3                                                                                                                        

/* SCHED_ISO: reserved but not implementedyet */                                                                                                

#define SCHED_IDLE      5

         SCHED_NORMAL:普通调度策略(大部分进程属于这个),静态优先级100~139,对应的nice值从-20~19,代码里会进行转化,使用CFS调度类。

SCHED_FIFO和SCHED_RR:实时调度策略,静态优先级从0~99,使用rt调度类。

后两者用的比较少

         rt调度类的优先级高于CFS调度类,因此使用实时调度策略的进程总是可以抢占普通调度策略进程。内核先从高优先级调度类中选择高优先级进程,再从低优先级调度类中选择高优先级进程。

 

2、调度算法

(1)实时策略FIFO和RR

         FIFO实现了一种简单、先入先出的调度算法:不使用时间片。这意味着一旦该类进程处于执行状态,就一直执行下去,直到它自己受阻塞或显示的释放处理器为止,只有更要优先级的SCHED_FIFO或SCHED_RR任务才能抢占它。处于可运行状态的该类进程会比任何SCHED_NORMAL级的进程都先得到调度。

         RR(实时轮转调度算法)可以说是带有时间片的SCHED_FIFO。当SCHED_RR进程耗尽它的时间片是,在同一级的其他实时进程被轮流调度。时间片只用来重新调度同一优先级的进程,也就是说时间片耗完仍然处于可执行队列,只是保证同一优先级的其他进程可以执行,而低优先级想执行只能等到同一优先级所有进程都处于不可执行状态才有机会。

         两种实时算法都是静态优先级0~99,内核不计算动态优先级,从而保证高优先级的进程总是能够抢占低优先级的进程。只有当高优先级不可执行后,低优先级才有机会执行。

(2)普通策略CFS

    CFS(完全公平调度)的基本原理:设定一个调度周期(sched_latency_ns),目标是让每个进程在这个周期内至少有机会运行一次,换一种说法就是每个进程等待CPU的时间最长不超过这个调度周期;然后根据进程的数量,大家平分这个调度周期内的CPU使用权;nice优先级不再用于计算时间片,而是作为进程获得的CPU运行比的权重,nice越大(优先级越低)权重越大,反之越小;每个进程的累计运行时间保存在自己的vruntime字段里,哪个进程的vruntime最小就获得本轮运行的权利。

    调度周期:所有可运行状态进程都调度执行一遍的时间。涉及结构体数据:

struct task_struct {

const struct sched_class*sched_class;  //指向调度器类链表

struct sched_entity se; //cfs调度器实体结构

struct sched_rt_entity rt;  //rt调度器实体结构

}

structsched_entity {

    struct load_weight  load;      /* for load-balancing */

    structrb_node      run_node;   //红黑树

    struct list_head    group_node;

    unsigned int        on_rq;

 

    u64        exec_start;

    u64        sum_exec_runtime;

    u64         vruntime;   //虚拟运行时间

    u64        prev_sum_exec_runtime;

   

u64         nr_migrations;

}

注:相关运行时间都是ns为单位

vruntime变量存放进程的虚拟运行时间,该运行时间的计算是经过了所有可运行进程总数的标准化,是CFS调度的核心数据。基本计算方法如下:

A.分配给进程的实际运行时间=调度周期 * 进程权重 / 所有进程权重之和  (公式1

    sysctl_sched_latency:存放调度周期;每个nice都有一个权重值,内核变量定义如下:

staticconst int prio_to_weight[40] = {

 /* -20 */    88761,     71755,     56483,    46273,     36291,

 /* -15 */    29154,     23254,     18705,    14949,     11916,

 /* -10 */     9548,      7620,      6100,     4904,      3906,

 /*  -5*/      3121,      2501,     1991,      1586,      1277,

 /*   0*/      1024,       820,       655,       526,       423,

 /*   5*/       335,       272,       215,       172,       137,

 /*  10*/       110,        87,        70,        56,        45,

 /*  15*/        36,        29,        23,        18,       15,

};

 

B.虚拟vruntime

vruntime= 实际运行时间 * 1024 / 进程权重   (公式2

1024是nice=0时的权重值,其他nice值的进程以该值为基准,也就是说nice=0的进程虚拟运行时间和实际运行时间相等,而nice越小优先级越高,虚拟运行时间越小被调度的机会就越多。

将公式1代入公式2:

vruntime= (调度周期 * 进程权重 / 所有进程权重之和) * 1024 / 进程权重

         =调度周期 * 1024 / 所有进程权重之和   (公式3

从该公式可以知道一个调度周期内,不同进程的vruntime相同,体现了该调度器完全公平一说。

 

C.虚拟运行时间和实际运行时间区别

    从上面公式可以看到,在一个调度周期内,不同进程的虚拟运行时间是相同的,但真实时间是不同的。

从公式(2)可知:vruntime的增加量和进程权重成反比,权重越大(优先级越高)vruntime增长越少,反之越多。CFS总是选择vruntime最小的进程进行调度,所有权重越大得到cpu的机会越高。因此,理论上一个调度周期为了保持vruntime相同,进程权重越大的,那么实际运行时间也要越长。

注:新进程、休眠进程、进程切换CPU等vruntime都会进行相应的补偿,免得和当前相差太大,一直处于执行。

 

红黑树:

    CFS调度算法的核心:选择具有最小的vruntime任务。CFS使用红黑树来组织可运行进程队列,并利用其迅速找到最小vruntime值的进程。

挑选下一个任务:schedule()-> pick_next_task() -> pick_next_task_fair() -> pick_next_entity()-> __pick_next_entity() -> rb_entry()

向树中加入进程:enqueue_entity(),在进程变为可运行装填或者fork第一次创建进程时

从树中删除进程:dequeue_entity(),发生进程阻塞或者终止时。

 

proc信息:

目录:/proc/sys/kernel下面:

sched_latency_ns:sysctl_sched_latency,表示一个运行队列所有进程运行一次的周期(即上面的调度周期)

sched_min_granularity_ns:sysctl_sched_min_granularity,表示进程最少运行时间,防止频繁的切换,对于交互系统(如桌面),该值可以设置得较小,这样可以保证交互得到更快的响应

sched_wakeup_granularity_ns:sysctl_sched_wakeup_granularity,该变量表示进程被唤醒后至少应该运行的时间的基数,它只是用来判断某个进程是否应该抢占当前进程,并不代表它能够执行的最小时间(sysctl_sched_min_granularity),如果这个数值越小,那么发生抢占的概率也就越高

sched_features:sysctl_sched_features,该变量表示调度器支持的特性

sched_child_runs_first:sysctl_sched_child_runs_first,该变量表示在创建子进程的时候是否让子进程抢占父进程,即使父进程的vruntime小于子进程,这个会减少公平性,但是可以降低write_on_copy,具体要根据系统的应用情况来考量使用哪种方式。

sched_rt_period_us / sched_rt_runtime_us:sysctl_sched_rt_period/sysctl_sched_rt_runtime,两个参数一起决定了实时进程在以sysctl_sched_rt_period为周期的时间内,实时进程最多能够运行的总的时间不能超过sysctl_sched_rt_runtime。

sched_compat_yield:sysctl_sched_compat_yield,该参数可以让sched_yield()系统调用更加有效,让它使用更少的cpu,对于那些依赖sched_yield来获得更好性能的应用可以考虑设置它为1

sched_migration_cost:sysctl_sched_migration_cost该变量用来判断一个进程是否还是hot,如果进程的运行时间(now - p->se.exec_start)小于它,那么内核认为它的code还在cache里,所以该进程还是hot,那么在迁移的时候就不会考虑它

sched_nr_migrate:sysctl_sched_nr_migrate,在多CPU情况下进行负载均衡时,一次最多移动多少个进程到另一个CPU上

sched_tunable_scaling:sysctl_sched_tunable_scaling,当内核试图调整sched_min_granularity,sched_latency和sched_wakeup_granularity这三个值的时候所使用的更新方法,0为不调整,1为按照cpu个数以2为底的对数值进行调整,2为按照cpu的个数进行线性比例的调整

 

3、调度实现

(1)调度函数

         进程调度的主要入口函数是schedule(kernel/sched/core.c),它选择哪个进程可以运行,何时将其投入运行。它会找到一个最高优先级的调度类,然后从该类中选择优先级最高的进程。主要由接口pick_next_task实现:

static inline struct task_struct *pick_next_task(struct rq *rq)                                                                                 

{                                                                                                                                                

   const struct sched_class *class;                                                                                                             

   struct task_struct *p;                                                                                                                       

                                                                                                                                                 

   /*                                                                                                                                           

    * Optimization: we know that if all tasks are in                                                                                            

    * the fair class we can call that function directly:                                                                                        

    */                                                                                                                                          

   if (likely(rq->nr_running == rq->cfs.h_nr_running)) {                                                                                        

       p = fair_sched_class.pick_next_task(rq);                                                                                                 

       if (likely(p))                                                                                                                           

           return p;                                                                                                                             

   }                                                                                                                                            

                                                                                                                                                  

   for_each_class(class) {                                                                                                                      

       p = class->pick_next_task(rq);                                                                                                           

       if (p)                                                                                                                                   

           return p;                                                                                                                            

   }                                                                                                                                                                                                                             

}

该函数会以优先级为序,从高到低,依次检查每一个调度类,并且从最高优先级的调度类中,选择最高优先级的进程。

上面先执行了CFS调度类查找,这样做的主要目的:系统运行的绝大多数进程都是普通进程,这样可以加速选择下一个CFS进程。

(2)睡眠和唤醒

A.休眠

休眠的进程处于一个特殊的不可执行状态。进程休眠有多种原因,但肯定都是为了等待一些事件。对于内核操作都是相同:进程把自己标记成休眠状态,从可执行红黑树中移出,放入等待队列,然后调用schedule()选择和执行一个其他进程。休眠通过等待队列进行处理,等待队列是由等待某些事件发生的进程组成的简单链表。

 

B.唤醒

         唤醒:进程被设置为可执行状态,然后在从等待队列中移到可执行的红黑树中。通过函数wake_up进行,它会唤醒指定的等待队列上的所有进程,如果被唤醒的进程优先级比当前正在执行的进程优先级高,还要设置need_resched标志。

(3)抢占

内核提供了一个need_resched标志来表明是否需要重新执行一次调度。当某个进程应该被抢占时,scheduler_tick()会设置;当一个优先级高的进程进入可执行状态,try_to_wake_up()会设置;返回用户空间以及从中断返回的时候,内核也会检查need_resched标志。

A.用户抢占

         内核即将返回用户空间的时候,如果need_resched标志被设置,会导致schedule()被调用,此时就会发生用户抢占。在内核返回用户空间的时候,知道自己是安全的,因此就可以选择一个新的进程去执行。

         用户抢占发生在以下情况:

从系统调用返回用户空间

从中断处理程序返回用户空间

 

B.内核抢占

         只要重新调度是安全的,内核就可以在任何时间抢占正在执行的任务。只要没有持有锁,重新调度就是安全的,内核就可以进行抢占。锁是非抢占区域的标志。

         内核抢占发生在一下情况:

中断处理程序正在执行,且返回内核空间之前

内核代码再一次具有可抢占性的时候

如果内核中的任务显式调用schedule()

如果内核中的任务阻塞(这同样也会导致调用schedule)

原创粉丝点击