调度器的实现

     调度器的实现基于两个函数:周期性调度器函数和主调度器函数。这些函数根据现有进程的优先级分配CPU时间:
好文参考:https://blog.csdn.net/janneoevans/article/details/8125106
1.主调度器
 在内核中的许多地方,如果要将CPU分配给与当前活动进程不同的另一个进程,都会直接调用主
调度器函数(schedule)。在从系统调用返回之后,内核也会检查当前进程是否设置了重调度标志
TIF_NEED_RESCHED,例如,前述的scheduler_tick通过调度类就会设置该标志。如果是这样,则内核会调用
schedule。该函数假定当前活动进程一定会被另一个进程取代。网友相关理解:

        主调度器就负责将CPU使用权限从一个进程切换到另一个进程。。上图中,三种不同颜色的长条分别表示CPU分配给进程A、B、C的一小段执行时间,执行顺序是:A,B,C。竖直的虚线表示当前时间,也就是说;A已经在CPU上执行完CPU分配给它的时间,马上轮到B执行了。这时主调度器shedule就负责完成相关处理工作然后将CPU的使用权交给进程B。
        总之,主调度器的工作就是完成进程间的切换。
主调度器基本流程:

看下具体代码实现:
schedule函数的实现:
asmlinkage void __sched schedule(void)
{
    /*  获取当前的进程  */
    struct task_struct *tsk = current;
     /*  避免死锁 */
    sched_submit_work(tsk);
    __schedule();
}

sched_submit_work函数的实现:
static inline void sched_submit_work(struct task_struct *tsk)
{    
     /*  检测tsk->state是否为0 (runnable), 若为运行态时则返回,
         tsk_is_pi_blocked(tsk),检测tsk的死锁检测器是否为空,若非空的话就return*/
    if (!tsk->state || tsk_is_pi_blocked(tsk))
        return;
    /*
     * If we are going to sleep and we have plugged IO queued,
     * make sure to submit it to avoid deadlocks.
     */
    if (blk_needs_flush_plug(tsk)) /*  然后检测是否需要刷新plug队列,用来避免死锁  */
        blk_schedule_flush_plug(tsk);
}

__schedule函数的实现:
static void __sched __schedule(void)
{
    struct task_struct *prev, *next;
    unsigned long *switch_count;
    struct rq *rq;
    int cpu;

need_resched:
    preempt_disable();   /*  关闭内核抢占  */
    /*  ==1==  
        找到当前cpu上的就绪队列rq
        并将正在运行的进程curr保存到prev中  */
    cpu = smp_processor_id();
    rq = cpu_rq(cpu);
    /*  更新全局状态,
     *  标识当前CPU发生上下文的切换  */
    rcu_note_context_switch(cpu);
    prev = rq->curr;
    
    /*  如果禁止内核抢占,而又调用了cond_resched就会出错
     *  这里就是用来捕获该错误的  */
    schedule_debug(prev);

    if (sched_feat(HRTICK))
        hrtick_clear(rq);

    /*
     * Make sure that signal_pending_state()->signal_pending() below
     * can't be reordered with __set_current_state(TASK_INTERRUPTIBLE)
     * done by the caller to avoid the race with signal_wake_up().
     */
    smp_mb__before_spinlock();
     /*  锁住该队列  */
    raw_spin_lock_irq(&rq->lock);
    /*  切换次数记录, 默认认为非主动调度计数(抢占)  */
    switch_count = &prev->nivcsw;
    /*
     *  scheduler检查prev的状态state和内核抢占标志   
     *  如果prev是不可运行的, 并且在内核态没有被抢占
     *  
     *  此时当前进程不是处于运行态, 并且不是被抢占
     *  此时不能只检查抢占计数
     *  因为可能某个进程(如网卡轮询)直接调用了schedule
     *  如果不判断prev->stat就可能误认为task进程为RUNNING状态
     *  到达这里,有两种可能,一种是主动schedule, 另外一种是被抢占
     *  被抢占有两种情况, 一种是时间片到点, 一种是时间片没到点
     *  时间片到点后, 主要是置当前进程的need_resched标志
     *  接下来在时钟中断结束后, 会preempt_schedule_irq抢占调度
     *  
     *  那么我们正常做的是应该将进程prev从就绪队列rq中删除, 
     *  但是如果当前进程prev有非阻塞等待信号, 
     *  并且它的状态是TASK_INTERRUPTIBLE
     *  我们就不应该从就绪队列总删除它 
     *  而是配置其状态为TASK_RUNNING, 并且把他留在rq中

    /*  如果内核态没有被抢占, 并且内核抢占有效
        即是否同时满足以下条件:
        1  该进程处于停止状态
        2  该进程没有在内核态被抢占 */
    if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
        /*  如果当前进程有非阻塞等待信号,并且它的状态是TASK_INTERRUPTIBLE  */
        if (unlikely(signal_pending_state(prev->state, prev))) {
             /*  将当前进程的状态设为:TASK_RUNNING  */
            prev->state = TASK_RUNNING;
        } else {/*  否则需要将prev进程从就绪队列中删除*/
         /*  将当前进程从runqueue(运行队列)中删除  */
            deactivate_task(rq, prev, DEQUEUE_SLEEP);
            /*  标识当前进程不在runqueue中  */
            prev->on_rq = 0;

            /*
             * If a worker went to sleep, notify and ask workqueue
             * whether it wants to wake up a task to maintain
             * concurrency.
             */
            if (prev->flags & PF_WQ_WORKER) {
                struct task_struct *to_wakeup;

                to_wakeup = wq_worker_sleeping(prev, cpu);
                if (to_wakeup)
                    try_to_wake_up_local(to_wakeup);
            }
        }
        /*  如果不是被抢占的,就累加主动切换次数  */
        switch_count = &prev->nvcsw;
    }

    pre_schedule(rq, prev);
    //当前CPU运行队列为NULL的时候
    if (unlikely(!rq->nr_running))
        idle_balance(cpu, rq);//进行CPU负载平衡
    /*CPU负载平衡有两种方式:pull和push,即空闲CPU从其他忙的CPU队列中拉一个进程到当前CPU队列;
    或者忙的CPU队列将一个进程推送到空闲的CPU队列中。idle_balance干的则是pull的事情*/

    /*put_prev_task在另一个进程代替当前运行的进程之前调用,它负责向进程提供或撤销CPU。
    但在不同进程之间切换,仍然需要执行一个底层的上下文切换*/
    put_prev_task(rq, prev);
    //选择下一个即将运行的进程
    next = pick_next_task(rq);
    /*清除pre的TIF_NEED_RESCHED(该进程应该或者想要调度器选择另一个进程替换本进程执行)标志*/ 
    clear_tsk_need_resched(prev);
    rq->skip_clock_update = 0;
     /*  如果prev和next非同一个进程  */
    if (likely(prev != next)) {
        rq->nr_switches++;/*  队列切换次数更新  */
        rq->curr = next; /*  将next标记为队列的curr进程  */
        ++*switch_count; /* 进程切换次数更新  */

#ifdef CONFIG_IDLESTATS
        if(cpu_accurate_status_enable) {
            cpu_accurate_schedule_begin();
            context_switch(rq, prev, next); /* unlocks the rq */
            cpu_accurate_schedule_end();
        }
        else
            context_switch(rq, prev, next); /* unlocks the rq */
#else
         /*  进程之间上下文切换    */
        context_switch(rq, prev, next); /* unlocks the rq */
#endif
        /*
         * The context switch have flipped the stack from under us
         * and restored the local variables which were saved when
         * this task called schedule() in the past. prev == current
         * is still correct, but it can be moved to another cpu/rq.
         */
        cpu = smp_processor_id();
        rq = cpu_rq(cpu);
    } else /* 如果prev和next为同一进程,则不进行进程切换  */
        raw_spin_unlock_irq(&rq->lock);

    post_schedule(rq);

    sched_preempt_enable_no_resched(); /*  开启内核抢占  */
    /*检测当前进程的重调度位是否设置,并跳转到如上所述的标号,重新开始搜索一个新
    进程*/
    if (need_resched())
        goto need_resched;
}
STACK_FRAME_NON_STANDARD(__schedule); /* switch_to() */
2. 周期调度器
     同样参照上图:周期性调度不关注进程切换,而是把A在CPU上执行的过程放大后观察细节。
在A享用它得到的CPU时间的过程中,系统会定时调用周期性调度器(即定时执行周期性调度函数scheduler_tick())。

在此版本的内核中,这个周期为10ms(这个10ms是这样得来的:内中定义了一个宏变量:HZ=100,它表示每秒钟周期性
调度器执行的次数,那么时间间隔t=1/HZ=1/100s=10ms。
       周期性调度器是用中断实现的:系统定时产生一个中断,然后在中断过程中执行scheduler_tick()函数,执行完毕后将CPU使用权限还给A
(有可能不会还给A了,细节后续在讨论),下一个时间点到了,系统会再次产生中断,然后去执行scheduler_tick()函数。(中断过程对进程
A是透明的,所以A是一个傻子,它以为自己连续享用了自己得到的CPU时间段,其实它中途被scheduler_tick()中断过很多次)。
      周期性调度器在scheduler_tick中实现。如果系统正在活动中,内核会按照频率HZ自动调用该
函数。如果没有进程在等待调度,那么在计算机电力供应不足的情况下,也可以关闭该调度器以减少
电能消耗。 该函数有下面两个主要任务。
(1) 管理内核中与整个系统和各个进程的调度相关的统计量。其间执行的主要操作是对各种计数器加1。
            a)    更新时钟(rq->clock),这个任务由__update_rq_clock()完成。
            b)   更新rq结构体的的cpu_load[ ]数组。它保留了5(由 CPU_LOAD_IDX_MAX指定)个历史load值,update_cpu_load(rq)则是将每个元素往后移,
将最老的值从数组中移出,将当前的load添加进去。
            c)    接下来更新进程的vruntime,sum_exec_runtime等参数。这任务由该进程指向的调度类所指向的task_tick()函数完成。看调度类(sched_class)结构体
的定义,你会发现task_tick只是一个函数指针。如果当前进程是普通进程,它对应的sched_class的task_tick()函数指针是指向task_tick_fair()这个函数的,如果
是空闲(idle)进程,它对应的sched_class的task_tick()指向的是task_tick_idle()函数,如果是实时进程则是task_tick_rt()。
(2) 激活负责当前进程的调度类的周期性调度方法。
看看scheduler_tick具体代码实现:
void scheduler_tick(void)
{
    /*  1.  获取当前cpu上的全局就绪队列rq和当前运行的进程curr  */

    /*  1.1 在于SMP的情况下,获得当前CPU的ID。如果不是SMP,那么就返回0  */
    int cpu = smp_processor_id();
    /*  1.2 获取cpu的全局就绪队列rq, 每个CPU都有一个就绪队列rq  */
    struct rq *rq = cpu_rq(cpu);
    /*  1.3 获取就绪队列上正在运行的进程curr  */
    struct task_struct *curr = rq->curr;

    sched_clock_tick();
     /*  2 更新rq上的统计信息, 并执行进程对应调度类的周期性的调度  */

    /*  加锁 */
    raw_spin_lock(&rq->lock);
     /*  2.1 更新rq的当前时间戳(包括任务时间戳).即使rq->clock变为当前时间戳  */
    update_rq_clock(rq);
    /*  2.2 更新rq的负载信息,  即就绪队列的cpu_load[]数据
     *  本质是讲数组中先前存储的负荷值向后移动一个位置,
     *  将当前负荷记入数组的第一个位置 */
    update_cpu_load_active(rq);
    /*  2.3 执行当前运行进程所在调度类的task_tick函数进行周期性调度  */
    curr->sched_class->task_tick(rq, curr, 0);
     /* 解锁 */
    raw_spin_unlock(&rq->lock);
    /* 与perf计数事件相关 */
    perf_event_task_tick();

#ifdef CONFIG_SMP
    rq->idle_balance = idle_cpu(cpu);
    trigger_load_balance(rq, cpu);
#endif
    rq_last_tick_reset(rq);
}
     如果当前进程应该被重新调度,那么调度器类方法会在task_struct中设置TIF_NEED_RESCHED
标志,以表示该请求,而内核会在接下来的适当时机完成该请求。
举例:如果当前进程是普通进程(调度类采用公平调度),那么周期性调度器的执行流程如下(其中省略了判断语句和其他的细节):

看看task_tick_fair的实现;
/*
 * scheduler tick hitting a task of our scheduling class:
 */
//task_tick_fair()负责更新普通进程的统计信息。
static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued)
{
    struct cfs_rq *cfs_rq;
     /*  获取到当前进程curr所在的调度实体  */
    struct sched_entity *se = &curr->se;
     /* for_each_sched_entity
     * 在不支持组调度条件下, 只循环一次
     * 在组调度的条件下, 调度实体存在层次关系,
     * 更新子调度实体的同时必须更新父调度实体
     * for_each_sched_entity函数便是依次往上遍历,直至它的最高层祖先。  
     * #definefor_each_sched_entity(se) \
        for (; se; se = se->parent)*/
    for_each_sched_entity(se) {
        /*  获取当当前运行的进程所在的CFS就绪队列  */
        cfs_rq = cfs_rq_of(se);
        /*  entity_tick完成周期性调度,负责二个工作:
            1.调用update_curr()函数更新相关统计量,
            2.调用check_preempt_tick(),检查进程本次被获得CPU使用权执行的时间是否超过了它对应的ideal_runtime值,
            如果超过了,则将当前进程的TIF_NEED_RESCHED标志位置位。  */
        entity_tick(cfs_rq, se, queued);----------------->1
    }

    if (numabalancing_enabled)
        task_tick_numa(rq, curr);

    update_rq_runnable_avg(rq, 1);
}
------------------->1再看看entity_tick的代码实现:
static void
entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
{
    /*
     * Update run-time statistics of the 'current'.
    调用update_curr()函数更新相关统计量
     */
    update_curr(cfs_rq);-------->1

    /*
     * Ensure that runnable average is periodically updated.
     */
    update_entity_load_avg(curr, 1);
    update_cfs_rq_blocked_load(cfs_rq, 1);
    update_cfs_shares(cfs_rq);


    if (cfs_rq->nr_running > 1)
        check_preempt_tick(cfs_rq, curr);-------------------->2
}
----------->1看看update_curr的代码实现:
    /*
 * Update the current task's runtime statistics.
 */
static void update_curr(struct cfs_rq *cfs_rq)
{
    struct sched_entity *curr = cfs_rq->curr;
    u64 now = rq_clock_task(rq_of(cfs_rq));
    u64 delta_exec;

    if (unlikely(!curr))
        return;
    /*是计算周期性调度器上次执行时到周期性这次执行之间,进程实际执行的CPU时间(如果周期性调度器每1ms执行一次,
    delta_exec就表示没1ms内进程消耗的CPU时间,这个在前面讲了),它是一个实际运行时间。
    */
    delta_exec = now - curr->exec_start;
    if (unlikely((s64)delta_exec <= 0))
        return;
    //更新exec_start
    curr->exec_start = now;

    schedstat_set(curr->statistics->exec_max,
              max(delta_exec, curr->statistics->exec_max));
    //更新当前进程的实际运行时间sum_exec_runtime
    curr->sum_exec_runtime += delta_exec;
    schedstat_add(cfs_rq, exec_clock, delta_exec);
    /*    calc_delta_fair:更新当前进程的虚拟时间vruntime
    */
    curr->vruntime += calc_delta_fair(delta_exec, curr);
    //更新cfs_rq->min_vruntime。
    update_min_vruntime(cfs_rq);

    if (entity_is_task(curr)) {
        struct task_struct *curtask = task_of(curr);

        trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);
        cpuacct_charge(curtask, delta_exec);
        account_group_exec_runtime(curtask, delta_exec);
    }

    account_cfs_rq_runtime(cfs_rq, delta_exec);
}
------------->2》check_preempt_tick的代码实现:
/*
 * Preempt the current task with a newly woken task if needed:
 */
static void
check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
    unsigned long ideal_runtime, delta_exec;
    struct sched_entity *se;
    s64 delta;
    /*  计算curr的理论上应该运行的时间  */
    ideal_runtime = sched_slice(cfs_rq, curr);
    /*  计算curr的实际运行时间
     *  sum_exec_runtime: 进程执行的总时间
     *  prev_sum_exec_runtime:进程在切换进CPU时的sum_exec_runtime值  */
    delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
     /*  如果实际运行时间比理论上应该运行的时间长
     *  说明curr进程已经运行了足够长的时间
     *  应该调度新的进程抢占CPU了  */
    if (delta_exec > ideal_runtime) {
        /*resched_task发出重调度请求. 这会在task_struct中设置TIF_NEED_RESCHED标志, 
        核心调度器会在下一个适当的时机发起重调度.*/
        resched_curr(rq_of(cfs_rq));
        /*
         * The current task ran long enough, ensure it doesn't get
         * re-elected due to buddy favours.
         */
        clear_buddies(cfs_rq, curr);
        return;
    }
}
实时进程的情形:
      实时进程的调度不涉及虚拟运行时间,所以比更新统计量的工作普通进程要简单很多。该函数首先调用update_curr_rt()函数
更新当前进程的统计信息。然后根据判断条件进行必要的操作。
     实时进程有两种调度策略,FIFO(先进先出)和RR(时间片轮转)。FIFO策略很简单:得到CPU使用权的进程可以执行任意长
时间,直到它主动放弃CPU。RR策略呢,则是给每个进程分配一个时间片,当前进程的时间片消耗完毕则切换至下一个进程。
     所以,接下来的代码,判断调度策略是不是RR,如果不是RR则无事可做。如果是RR,则将其时间片减一,如果时间片不为零,该进程
可以继续执行,那么什么都不需要做。如果时间片为0,则重新给它分配时间片(长度由DEF_TIMESLICE指定),如果可运行进程大于一个,
就调用requeue_task_rt()将当前进程放到实时就绪队列的末尾,并将TIF_NEED_RESCHED标志位置为,提示系统需要进行进程切换。
如果当前进程是实时进程,周期性调度器的执行流程如下:

猜你喜欢

转载自blog.csdn.net/hzj_001/article/details/81542336