schedule与CFS算法

一、调度类与调度实体

调度类是系统为了对不同进程调度进行区分而用的数据结构,其中记录了关于调度不同进程所需要的函数,每种调度算法都有其自己的调度类

调度实体是每个进程都有的数据结构,其中记录了调度此进程所需要使用的所有信息,且普通进程和实时进程有着不同的调度实体

关于不同种类的进程,主要是3种,普通进程、实时进程以及空闲进程(idle)

#define SCHED_NORMAL 0 /* 普通进程调度策略,即CFS算法 */
#define SCHED_FIFO 1 /* 实时进程的调度策略 */
#define SCHED_RR 2 /* 实时进程的调度策略 */
#define SCHED_BATCH 3
#define SCHED_IDLE 5 /* 空闲进程 */
#define SCHED_RESET_ON_FORK 0x40000000

SCHED_FIFO代表先入先出的调度算法:不使用时间片,一旦一个此种调度策略的进程处于可执行状态就会一直执行,直到它自己受阻塞或者释放处理器,只有更高级的SCHED_FIFO和SCHED_RR任务能够抢占它。

SCHED_RR代表实时轮流调度算法,与前者大体相同,只是其中每个任务都有着时间片,当耗完了时间片就不能再继续执行,时间片耗尽后同优先级的其他实时进程会被轮流调度。

SCHED_NORMAL代表普通进程的调度策略,我看的是2.6.32版本的内核代码,使用的是CFS(完全公平调度算法)。当系统中有实时进程时,普通进程是不会被调度执行的,只有当实时进程都结束以后才开始运用此调度算法选择普通进程运行

二、schedule函数主要流程

schedule()是进程调度的主要入口,作用是选择下一个运行的进程,且它在每次的进程选择时会找到最高优先级的调度类,每个调度类都有其自己的就绪队列,然后再从其中找出下一个该运行的进程。

首先声明了调度中需要用到的一些数据结构

asmlinkage void __sched schedule(void)
{
    struct task_struct *prev, *next;    
    unsigned long *switch_count;   
    struct rq *rq;
    int cpu;

禁止内核抢占,获取当前cpu的编号,获取当前cpu对应的就绪队列rq,当前进程的描述符prev,获取当前进程的切换次数switch_count,释放大内核锁,完成对时间调试的检查与统计

preempt_disable();   
    cpu = smp_processor_id(); 
    rq = cpu_rq(cpu); 
    rcu_sched_qs(cpu); 
    prev = rq->curr;  
    switch_count = &prev->nivcsw; 
    release_kernel_lock(prev);
    hedule_debug(prev);
    if (sched_feat(HRTICK))  
        hrtick_clear(rq);

对当前的就绪队列锁上自旋锁,然后更新就绪队列上的时钟,清除当前进程的重新调度标志 TIF_NEED_RESCHED

    spin_lock_irq(&rq->lock);
    update_rq_clock(rq);
    clear_tsk_need_resched(prev);

判断如果进程不是TASK_RUNNING(state>0)且此调度函数不是由于抢占而调用执行的(即当前进程不是被抢占的),那么如果当前进程仍有未处理信号就将其状态置为TASK_RUNNING,在后面会重新将其插入就绪队列中,否则将清除出就绪队列(此种情况应该是当前进程执行结束,所以自然要出就绪队列)

    if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) 
    {   
        if (unlikely(signal_pending_state(prev->state, prev))) 
            prev->state = TASK_RUNNING;
        else
            deactivate_task(rq, prev, 1);
        switch_count = &prev->nvcsw;
    }

判断如果当前cpu的就绪队列中没有了可运行的进程,那么调用负载均衡,从另一个较忙的cpu的就绪队列中拉一个进程过来执行

    pre_schedule(rq, prev);
    if (unlikely(!rq->nr_running)) 
        idle_balance(cpu, rq);

将prev重新插入到就绪队列中的合适位置,对应之前的判断是否抢占的语句,之后执行pick_next_task函数来获得下一个要运行的进程
这里的两个函数都是钩子函数,由于不同的调度类有着不同的运行队列,所以要区分出prev是插入到哪种进程的就绪队列,下一个进程是调用实时进程还是普通进程

    put_prev_task(rq, prev);     
    next = pick_next_task(rq);

判断选中的下一个要运行的进程是否是上一个进程,如果不是就更新进程描述符的相关字段,然后调用context_switch进行上下文切换(在此函数中会解开之前上的就绪队列的自旋锁),如果就是上一个进程的话,就不用麻烦了,直接解开就绪队列的自旋锁就行了

    if (likely(prev != next)) { 
        sched_info_switch(prev, next);
        perf_event_task_sched_out(prev, next, cpu);

        rq->nr_switches++;
        rq->curr = next;
        ++*switch_count;

        context_switch(rq, prev, next);
        cpu = smp_processor_id();
        rq = cpu_rq(cpu);
    } else
        spin_unlock_irq(&rq->lock);

这里的句子不懂是在干什么,其中有检查内核锁,解开内核抢占,判断是否有重新调度标志

    post_schedule(rq);
    if (unlikely(reacquire_kernel_lock(current) < 0)) 
        goto need_resched_nonpreemptible;
    preempt_enable_no_resched();
    if (need_resched())
        goto need_resched;
}

三、CFS算法

CFS即完全公平调度算法,在2.6.23的内核版本中代替了原来的O(1)调度算法

实际上CFS算法不像以往有着时间片的概念,取而代之的是时间片的使用比,任何进程所获得的处理器时间都是他自己和其他所有可运行进程静态优先级值的相对值所决定的,而不像以往是静态优先级有着固定的算法来分配时间片

公式:分配给进程的运行时间=调度周期*(进程的权重值/所有进程总权重值)

其中权重是由nice值决定的,内核中有prio_to_weight数组来查找不同nice值的权重(nice的值可用ps -el命令看到,其中的NI字段即nice值)。

static const 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,
};

在之前的O(1)算法中通过进程的优先级来选择下一个要运行的进程,而在CFS算法中则是通过对vruntime(虚拟运行时间)的比较来选择下一个进程,vruntime最小的最先执行

vruntime通过task_new_fair函数中的update_curr()以及plaec_entity()计算出来,记录了一个进程到底运行了多长时间以及它还应该在运行多久

关于CFS算法,还有个很重要的就是它如果找到最小vruntime的进程,其利用的是红黑树

红黑树(Red Black Tree) 是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构。
红黑树在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能,它可以在O(log n)时间内做查找,插入和删除,这里的n 是树中元素的数目。

schedule与CFS算法_第1张图片

如图中就是一个红黑树,其中节点中的是进程的vruntime减去就绪队列的min_vruntime的值,此时调度的话就会选择最左边的节点的进程运行

但是新的进程是如何插入到红黑树中的呢?
实际上在进程被新建出来插入就绪队列时就已经完成了计算其的vruntime,并且在将其插入红黑树时就会判定新加入的进程的位置,如果它被放到了最左边,则在CFS算法的就绪队列的数据结构中有一个名为rb_leftmost的指针就会指向它,当执行调度函数选择下一个进程时,就会直接找到rb_leftmost所指向的进程,从而免去了查找的时间

一个新的进程插入红黑树的大致过程:
首先是do_fork中调用的wake_up_new_task()函数,其中执行了p->sched_class->task_new(rq,p),即通过不同的调度类来将进程插入适当的就绪队列

void wake_up_new_task(struct task_struct *p, unsigned long clone_flags)
{
    ...
    if (!p->sched_class->task_new || !current->se.on_rq) {
        activate_task(rq, p, 0);
    } else {
        p->sched_class->task_new(rq, p);
        inc_nr_running(rq);
    }
    ...
}

CFS的调度类fair_sched_class中task_new对应的是task_new_fair函数,其中通过update_curr()与place_entity()计算vruntime的值,最后调用enqueue_task_fair()将新的进程插入到普通进程的就绪队列中

static void task_new_fair(struct rq *rq, struct task_struct *p)
{   
    ...
    update_curr(cfs_rq);  
    ...
    place_entity(cfs_rq, se, 1);
    ...
    enqueue_task_fair(rq, p, 0);
}

在enqueue_task_fair()中调用enqueue_entity,不过在enqueue_entity中实际调用的是__enqueue_entity来实现红黑树的插入
在__enqueue_entity主要就是一个while循环,它先将leftmost置为1,当新进程的比较节点的key值时,小于即向左走,leftmost不变,否则就是向右走,使leftmost=0,即此进程不是红黑树中下次调度要选择的进程


static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)/*将一个实体(进程)插入到红黑树中*/
{
    struct rb_node **link = &cfs_rq->tasks_timeline.rb_node;
    struct rb_node *parent = NULL;
    struct sched_entity *entry;
    s64 key = entity_key(cfs_rq, se);
    int leftmost = 1;
    while (*link) {
        parent = *link;
        entry = rb_entry(parent, struct sched_entity, run_node);
        if (key < entity_key(cfs_rq, entry)) {
            link = &parent->rb_left;   /*红黑树的比较中向左走*/
        } else {                        /*红黑树的比较中向右走,leftmost=0*/
            link = &parent->rb_right;
            leftmost = 0;
        }
    }

你可能感兴趣的:(算法,源代码)