Linux的CFS(完全公平调度)算法

时间:2022-05-05 16:16:19

1.几个重要的概念:
每个进程都有一个nice值, 表示其静态优先级, 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,
};

可以看到, nice值越小, 进程的权重越大

CFS调度器的一个调度周期值是固定的, 由sysctl_sched_latency变量保存.

一个进程在一个调度周期中的运行时间为:

分配给进程的运行时间 = 调度周期 * 进程权重 / 所有进程权重之和

可以看到, 进程的权重越大, 分到的运行时间越多

一个进程的实际运行时间和虚拟运行时间之间的关系为:

vruntime = 实际运行时间 * NICE_0_LOAD / 进程权重
         = 实际运行时间 * 1024 / 进程权重

NICE_0_LOAD = 1024, 表示nice值为0的进程权重

可以看到, 进程权重越大, 运行同样的实际时间, vruntime增长的越慢

一个进程在一个调度周期内的虚拟运行时间大小为:

vruntime = 进程在一个调度周期内的实际运行时间 * 1024 / 进程权重
         = (调度周期 * 进程权重 / 所有进程总权重) * 1024 / 进程权重 
         = 调度周期 * 1024 / 所有进程总权重 

可以看到, 一个进程在一个调度周期内的vruntime值大小是不和该进程自己的权重相关的, 所以所有进程的vruntime值大小都是一样的

总结:

进程的nice值越小, 权重越大, 所分到的运行时间也越多. 

两个权重不相同的进程, 运行相同的实际时间, 权重大的进程vruntime值增加的要少一些.

但是在一个调度周期内, 所有进程的vruntime值都是一样大的, 当每个 
进程都把分配给自己的运行时间运行完了时, 它们的vruntime值是一样
大的. 所以一个进程的vruntime值越大, 表示进程已运行的时间占调度
器分配给它的运行时间的比重也越大.

所以, vruntime值可以反映出该进程当前已经运行的时间多少, 可以让各个进程公平的竞争CPU, 而且让优先级大的进程实际的运行时间也都多一些.

2.几个重要的结构:

<完全公平运行队列>: 描述运行在同一个cpu上的处于TASK_RUNNING状态的普通进程的各种运行信息

struct cfs_rq {
    struct load_weight load;  //运行队列总的进程权重
    unsigned int nr_running, h_nr_running; //进程的个数

    u64 exec_clock;  //运行的时钟
    u64 min_vruntime; //该cpu运行队列的vruntime推进值, 一般是红黑树中最小的vruntime值

    struct rb_root tasks_timeline; //红黑树的根结点
    struct rb_node *rb_leftmost;  //指向vruntime值最小的结点
    //当前运行进程, 下一个将要调度的进程, 马上要抢占的进程, 
    struct sched_entity *curr, *next, *last, *skip;

    struct rq *rq; //系统中有普通进程的运行队列, 实时进程的运行队列, 这些队列都包含在rq运行队列中 
    ...
};

<调度实体>: 记录一个进程的运行状态信息

struct sched_entity {
    struct load_weight  load; //进程的权重
    struct rb_node      run_node; //运行队列中的红黑树结点
    struct list_head    group_node; //与组调度有关
    unsigned int        on_rq; //进程现在是否处于TASK_RUNNING状态

    u64         exec_start; //一个调度tick的开始时间
    u64         sum_exec_runtime; //进程从出生开始, 已经运行的实际时间
    u64         vruntime; //虚拟运行时间
    u64         prev_sum_exec_runtime; //本次调度之前, 进程已经运行的实际时间
    struct sched_entity *parent; //组调度中的父进程
    struct cfs_rq       *cfs_rq; //进程此时在哪个运行队列中
};

进程描述符 struct task_struct, 调度实体 struct sched_entity, 完全公平调度运行队列 struct cfs_rq之间的关系:

Linux的CFS(完全公平调度)算法

从上图可以看出, 所有进程描述符的sched_entity结构通过其run_node字段组成了一颗红黑树, 这颗红黑树的根结点在cfs_rq结构的task_timeline字段.

4.有几个过程与CFS有关:

创建新进程: 创建新进程时, 需要设置新进程的vruntime值以及将新进程加入红黑树中. 并判断是否需要抢占当前进程

进程唤醒: 唤醒进程时, 需要调整睡眠进程的vruntime值, 并且将睡眠进程加入红黑树中. 并判断是否需要抢占当前进程

进程的调度: 进程调度时, 需要把当前进程加入红黑树中, 还要从红黑树中挑选出下一个要运行的进程.

时钟周期中断: 在时钟中断周期函数中, 需要更新当前运行进程的vruntime值, 并判断是否需要抢占当前进程

5.进程创建时:
创建新进程的系统调用fork, vfork, clone. 这三个系统调用最终都是调用do_fork()函数.在do_fork()函数中, 主要就是设置新进程的vruntime值, 将新进程加入到红黑树中, 判断新进程是否可以抢占当前进程. do_fork()函数涉及的函数调用过程:

do_fork --> copy_process --> sched_fork --> task_fork_fair

do_fork --> wake_up_new_task --> activate_task

do_fork --> wake_up_new_task --> check_preempt_curr

task_fork_fair()函数主要是设置新进程的vruntime值

activate_task()函数主要是把新进程加入到红黑树中, 因为此处第三个参数传入的是0, 所以后面不会调用place_entity()来再次调整新进程的vruntime值

check_preempt_curr()函数主要是检查新进程是否可以抢占当前进程

task_fork_fair()函数 部分重要代码:

static void task_fork_fair(struct task_struct *p)
{
    struct cfs_rq *cfs_rq;
    struct sched_entity *se = &p->se, *curr;
    int this_cpu = smp_processor_id();
    struct rq *rq = this_rq();
    unsigned long flags;

    raw_spin_lock_irqsave(&rq->lock, flags);

    update_rq_clock(rq);

    cfs_rq = task_cfs_rq(current);
    curr = cfs_rq->curr;

    rcu_read_lock();
    __set_task_cpu(p, this_cpu); //设置新进程在哪个cpu上运行
    rcu_read_unlock();

    update_curr(cfs_rq); //更新当前进程的vruntime值

    if (curr)
        se->vruntime = curr->vruntime; //先以父进程的vruntime为基础
    place_entity(cfs_rq, se, 1); //设置新进程的vruntime值, 1表示是新进程

    if (sysctl_sched_child_runs_first && curr && entity_before(curr, se)) { //sysctl_sched_child_runs_first值表示是否设置了让子进程先运行

        swap(curr->vruntime, se->vruntime); //当子进程的vruntime值大于父进程的vruntime时, 交换两个进程的vruntime值 
        resched_task(rq->curr); //设置重新调度标志TIF_NEED_RESCHED
    }

    se->vruntime -= cfs_rq->min_vruntime; //防止新进程运行时是在其他cpu上运行的, 这样在加入另一个cfs_rq时再加上另一个cfs_rq队列的min_vruntime值即可(具体可以看enqueue_entity函数)

    raw_spin_unlock_irqrestore(&rq->lock, flags);
}

task_fork_fair()函数是调用 place_entity函数来设置新进程的vruntime值的, place_entity()函数的 部分代码:

static void
place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
{
    u64 vruntime = cfs_rq->min_vruntime; //以当前运行队列的min_vruntime值为基础
    //START_DEBIT表示新进程是否需要"记账", 需要则加上新进程在一
    //个调度周期内的vruntime值大小, 表示新进程在这一个调度周期内已经运行过了
    if (initial && sched_feat(START_DEBIT))
        vruntime += sched_vslice(cfs_rq, se);//sched_vslice函数就是计算一个调度周期内新进程的vruntime值大小

    if (!initial) { //新进程时传入的initial值是1
        unsigned long thresh = sysctl_sched_latency;

        if (sched_feat(GENTLE_FAIR_SLEEPERS))
            thresh >>= 1;

        vruntime -= thresh;
    }

    //该函数调用之前, 执行了se->vruntime = curr->vruntime语句
    se->vruntime = max_vruntime(se->vruntime, vruntime);//此处表明, 对于新进程而言, 新进程的vruntime值是大于等于父进程vruntime值的. 
    //所以后面如果没有设置子进程先运行, 则只要父进程本次调度运行的实际时间没有超过调度周期分配的实际时间值, 父进程就会先运行, 否则, 父子进程的先后执行顺序不确定. 
    //所以在没有设置子进程先运行的系统上, 父进程先运行的概率还是很大的, 毕竟父进程本次调用运行的实际时间一般都不会超过调度周期分配的实际时间 
}

sched_vslice = (调度周期 * 进程权重 / 所有进程总权重) * NICE_0_LOAD / 进程权重

设置了新进程的vruntime大小后, 就要回到do_fork()函数中, 接着马上调用wake_up_new_task()函数, 在wake_up_new_task()函数中, 调用activate_task()将新进程加入红黑树中. 调用check_preempt_cur()函数来判断新进程是否可以抢占当前进程,

activate_task()函数等到后面在讲述.

check_preempt_curr()函数内部就是调用check_preempt_wakeup()函数:

static void check_preempt_wakeup(struct rq *rq, struct task_struct *p, int sync)  
{  
    struct task_struct *curr = rq->curr;  
    struct sched_entity *se = &curr->se, *pse = &p->se; //se是当前进程,pse是新进程 

    if (sched_feat(LAST_BUDDY) && likely(se->on_rq && curr != rq->idle))  
        set_last_buddy(se);  
    set_next_buddy(pse);  
    while (se) {  
        if (wakeup_preempt_entity(se, pse) == 1) {  //判断新进程是否可以抢占当前进程
            resched_task(curr);  
            break;  
        }  
        se = parent_entity(se);  
        pse = parent_entity(pse);  
    }  
}  

wakeup_preempt_entity()函数判断新进程是否可以抢占当前进程:

static int  
wakeup_preempt_entity(struct sched_entity *curr, struct sched_entity *se)  
{  
    s64 gran, vdiff = curr->vruntime - se->vruntime;  
    if (vdiff <= 0)  //新进程的vruntime值比当前进程的大, 则不发生抢占
        return -1;  
    gran = wakeup_gran(curr);  //判断发生抢占时的调度粒度
    if (vdiff > gran)  //两个进程之间的差值大于调度粒度时, 发生抢占
        return 1;  
    return 0;  //小于调度粒度, 则表示不发生
}  

调度粒度的设置, 是为了防止这么一个情况: 新进程的vruntime值只比当前进程的vruntime小一点点, 如果此时发生重新调度, 则新进程只运行一点点时间后, 其vruntime值就会大于前面被抢占的进程的vruntime值, 这样又会发生抢占, 所以这样的情况下, 系统会发生频繁的切换.

故, 只有当新进程的vruntime值比当前进程的vruntime值小于调度粒度之外, 才发生抢占.

至此, 与CFS有关的创建新进程部分就讲解完了.

6.进程的唤醒:
进程的默认唤醒函数是try_to_wake_up(), 该函数主要是调整睡眠进程的vruntime值, 以及把睡眠进程加入红黑树中, 并判断是否可以发生抢占:

try_to_wake_up涉及的函数调用过程:

try_to_wake_up --> ttwu_queue --> ttwu_do_activate --> ttwu_activate --> activate_task

try_to_wake_up --> ttwu_queue --> ttwu_do_activate --> ttwu_do_wakeup --> check_preempt_curr

activate_task()函数主要是调整睡眠进程的vruntime值, 并把睡眠进程加入红黑树中.

check_preempt_curr()函数主要是判断是否可以发生抢占, 前面已经讲过

activate_task()函数的调用过程:

activate_task
–>enqueue_task_fair
–>enqueue_entity

enqueu_entity()函数 部分代码:

static void
enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
    //现在的内核代码是这样处理睡眠进程vruntime值的

    //睡眠进程出队时, 其vruntime值没变. 
    //但是睡眠进程入队时, 先减去其原来cfs_rq队列上此时的
    //min_vruntime值, 再选择cpu, 再加上选
    //择的cfs_rq队列的min_vruntime值(下面的if语句处), 再调用place_entity来调整
    if (!(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_WAKING))
        se->vruntime += cfs_rq->min_vruntime;

    update_curr(cfs_rq); //更新当前进程的时间值

    if (flags & ENQUEUE_WAKEUP) {
        place_entity(cfs_rq, se, 0); //调整睡眠进程的vruntime, 0表示是睡眠进程
        enqueue_sleeper(cfs_rq, se);
    }

    if (se != cfs_rq->curr)
        __enqueue_entity(cfs_rq, se); //将进程加入红黑树
    se->on_rq = 1;
}

因为这次设置的是睡眠进程的vruntime值, 所以还需要再次看下place_entity()函数:

static void
place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
{
    u64 vruntime = cfs_rq->min_vruntime;

    if (initial && sched_feat(START_DEBIT))
        vruntime += sched_vslice(cfs_rq, se);

    if (!initial) {//睡眠进程时传入的initial为0
        unsigned long thresh = sysctl_sched_latency;//一个调度周期时间值

        if (sched_feat(GENTLE_FAIR_SLEEPERS))
            thresh >>= 1; //一般的调度周期时间值

        vruntime -= thresh; //对睡眠进程的vruntime补偿
    }

    se->vruntime = max_vruntime(se->vruntime, vruntime);//防止短期睡眠的进程vruntime值获得补偿
}

至此, 与CFS有关的进程唤醒部分就讲完了.

7.进程的调度:
进程的主动调度函数是schedule():

asmlinkage void __sched schedule(void)  
{  
    struct task_struct *prev, *next;  
    unsigned long *switch_count;  
    struct rq *rq;  
    int cpu;  
need_resched:  
    preempt_disable(); //在这里面被抢占可能出现问题,先禁止它! 
    cpu = smp_processor_id();  
    rq = cpu_rq(cpu);  
    rcu_qsctr_inc(cpu);  
    prev = rq->curr;  
    switch_count = &prev->nivcsw;  
    release_kernel_lock(prev);  
need_resched_nonpreemptible:  
    spin_lock_irq(&rq->lock);  
    update_rq_clock(rq);  
    clear_tsk_need_resched(prev); //清除需要调度的位 

    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);  //出队, 此处主要是把prev->on_rq赋值为0, 因为当前进程本来就没在红黑树中. on_rq为0后, 后面的put_prev_task函数就不会把当前进程加入红黑树了
        switch_count = &prev->nvcsw;  
    }  
    if (unlikely(!rq->nr_running))  
        idle_balance(cpu, rq);  

    prev->sched_class->put_prev_task(rq, prev);  //把当前进程加入红黑树中
    next = pick_next_task(rq, prev);  //从红黑树中挑选出下一个要运行的进程, 并将其设置为当前进程
    if (likely(prev != next)) {  
        sched_info_switch(prev, next);  
        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);  
    if (unlikely(reacquire_kernel_lock(current) < 0))  
        goto need_resched_nonpreemptible;  
    preempt_enable_no_resched();  
    //这里新进程也可能有TIF_NEED_RESCHED标志,如果新进程也需要调度则再调度一次 
    if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))  
        goto need_resched;  
}  

put_prev_task函数调用公平调度类的put_prev_task_fair函数, 该函数主要是把当前进程加入红黑树中:

    static void put_prev_entity(struct cfs_rq *cfs_rq, struct sched_entity *prev)  
    {  
        if (prev->on_rq)  
            update_curr(cfs_rq);  
        check_spread(cfs_rq, prev);  

        if (prev->on_rq) {  //此处表明只把还处于运行状态的进程加入红黑树, 如果是主动睡眠的进程, on_rq此时就为0了, 那么就不会把睡眠进程加入红黑树了
            update_stats_wait_start(cfs_rq, prev);  

            __enqueue_entity(cfs_rq, prev);  //加入红黑树中
        }  
        //没有当前进程了,这个当前进程将在pick_next_task中更新 
        cfs_rq->curr = NULL;  
    }  

再看schedule函数中的pick_next_task()函数:

static struct task_struct *pick_next_task_fair(struct rq *rq)  
{  
    struct task_struct *p;  
    struct cfs_rq *cfs_rq = &rq->cfs;  
    struct sched_entity *se;  
    if (unlikely(!cfs_rq->nr_running))  
        return NULL;  
    do {  
        se = pick_next_entity(cfs_rq); // 选出下一个要运行的进程
        set_next_entity(cfs_rq, se);  //将选出的进程设置为当前进程
        cfs_rq = group_cfs_rq(se);  
    } while (cfs_rq);  
    p = task_of(se);  
    hrtick_start_fair(rq, p);  
    return p;  
}

pick_next_entity和set_next_entity函数的 部分代码:

    static struct sched_entity *pick_next_entity(struct cfs_rq *cfs_rq)  
    {  
        //__pick_next_entity就是直接选择红黑树缓存的最左结点,也就是vruntime最小的结点 
        struct sched_entity *se = __pick_next_entity(cfs_rq);  
        if (cfs_rq->next && wakeup_preempt_entity(cfs_rq->next, se) < 1)  
            return cfs_rq->next;  
        if (cfs_rq->last && wakeup_preempt_entity(cfs_rq->last, se) < 1)  
            return cfs_rq->last;  
        return se;  //如果选出的进程vruntime值比next和last指向的进程的vruntime值小到粒度之外, 则返回新选出的进程
    }  

    static void  
    set_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)  
    {  
        if (se->on_rq) {  

            update_stats_wait_end(cfs_rq, se);  
            //就是把结点从红黑树上取下来. 前面说过, 当前运行进程不在红黑树上 
            __dequeue_entity(cfs_rq, se);  //把新选出的进程移出红黑树
        }  
        update_stats_curr_start(cfs_rq, se);  
        cfs_rq->curr = se;  //设置为当前进程

        se->prev_sum_exec_runtime = se->sum_exec_runtime;  //记录本次调度之前总的已运行时间
    }  

至此, 与CFS有关的调度部分也讲完了.

8.时钟周期中断:
时钟周期中断函数主要是更新当前进程的vruntime值和实际运行时间值, 并判断当前进程在本次调度中实际运行时间是否超过了调度周期分配的实际运行时间, 如果是则设置重新调度标志TIF_NEED_RESCHED. 时钟周期中断函数的调用过程:

tick_periodic --> scheduler_tick --> task_tick_fair --> entity_tick. 

entity_tick函数主要是更新当前进程的vruntime值和实际运行时间, 并判断是否需要设置重新调度标志:

static void  entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)  
    {  

        update_curr(cfs_rq);  //更新当前进程的时间值
        //....无关代码 
        if (cfs_rq->nr_running > 1 || !sched_feat(WAKEUP_PREEMPT))  
            check_preempt_tick(cfs_rq, curr);  //判断是否需要设置重新调度标志
    }  

接下来看下update_curr函数:

static void update_curr(struct cfs_rq *cfs_rq)  
{  
    struct sched_entity *curr = cfs_rq->curr;  
    u64 now = rq_of(cfs_rq)->clock; //这个clock刚刚在scheduler_tick中更新过 
    unsigned long delta_exec;  

    delta_exec = (unsigned long)(now - curr->exec_start);  //得到本次tick实际运行的时间值
    __update_curr(cfs_rq, curr, delta_exec);  //将本次tick实际运行的时间值更新到vruntime和实际运行时间
    curr->exec_start = now;   //设置下次tick的开始时间
    if (entity_is_task(curr)) {  
        struct task_struct *curtask = task_of(curr);  
        cpuacct_charge(curtask, delta_exec);  
        account_group_exec_runtime(curtask, delta_exec);  
    }  
}  

static inline void  
__update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr,  
          unsigned long delta_exec)  
{  
    unsigned long delta_exec_weighted;  
    //前面说的sum_exec_runtime就是在这里计算的,它等于进程从创建开始占用CPU的总时间 
    curr->sum_exec_runtime += delta_exec;   
    //下面变量的weighted表示这个值是从运行时间考虑权重因素换算来的vruntime,再写一遍这个公式 
    //vruntime(delta_exec_weighted) = 实际运行时间(delta_exe) * 1024 / 进程权重 
    delta_exec_weighted = calc_delta_fair(delta_exec, curr);  
    //将进程刚刚运行的时间换算成vruntime后立刻加到进程的vruntime上
    curr->vruntime += delta_exec_weighted;  
    //因为有进程的vruntime变了,因此cfs_rq的min_vruntime可能也要变化,更新它。 
    //就是先取tmp = min(curr->vruntime,leftmost->vruntime) 
    //然后cfs_rq->min_vruntime = max(tmp, cfs_rq->min_vruntime) 
    update_min_vruntime(cfs_rq);  
}  

接着回到entity_tick函数中, 调用check_preempt_tick()函数, 该函数主要是判断当前进程本次调度运行的实际时间是否已经大于其在一个调度周期分配得到的实际时间值, 如果是则设置重新调度标志:

static void  
check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)  
{  
    unsigned long ideal_runtime, delta_exec;  
    //这里sched_slice跟上面讲过的sched_vslice很象,不过sched_vslice换算成了vruntime, 
    //而这里这个就是实际时间,没有经过换算,返回值的就是此进程在一个调度周期中应该运行的时间 
    ideal_runtime = sched_slice(cfs_rq, curr);  

    delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;  //得到本次调度已经运行的实际时间
    if (delta_exec > ideal_runtime)  
        resched_task(rq_of(cfs_rq)->curr);  //设置TIF_NEED_RESCHED标志值
}

至此, 与CFS有关的时钟周期中断部分也讲完了

本文引述自:
http://blog.csdn.net/peimichael/article/details/5218335
http://edsionte.com/techblog/archives/category/linux%E5%86%85%E6%A0%B8/page/7