zoukankan      html  css  js  c++  java
  • 浅析Linux中的进程调度

    2016-11-22

     前面在看软中断的时候,牵扯到不少进程调度的知识,这方面自己确实一直不怎么了解,就趁这个机会好好学习下。


    现代的操作系统都是多任务的操作系统,尽管随着科技的发展,硬件的处理器核心越来越多,但是仍然不能保证一个进程对应一个核心,这就势必需要一个管理单元,负责调度进程,由管理单元来决定下一刻应该由谁使用CPU,这里充当管理单元的就是进程调度器。

      进程调度器的任务就是合理分配CPU时间给运行的进程,创造一种所有进程并行运行的错觉。这就对调度器提出了要求:

    1、调度器分配的CPU时间不能太长,否则会导致其他的程序响应延迟,难以保证公平性。

    2、调度器分配的时间也不能太短,每次调度会导致上下文切换,这种切换开销很大。

    而调度器的任务就是:1、分配时间给进程     2、上下文切换

    所以具体而言,调度器的任务就明确了:用一句话表述就是在恰当的实际,按照合理的调度算法,切换两个进程的上下文。

    调度器的结构

     在Linux内核中,调度器可以分成两个层级,在进程中被直接调用的成为通用调度器或者核心调度器,他们作为一个组件和进程其他部分分开,而通用调度器和进程并没有直接关系,其通过第二层的具体的调度器类来直接管理进程。具体架构如下图:

    如上图所示,每个进程必然属于一个特定的调度器类,Linux会根据不同的需求实现不同的调度器类。各个调度器类之间具备一定的层次关系,即在通用调度器选择进程的时候,会从最高优先级的调度器类开始选择,如果通用调度器类没有可运行的进程,就选择下一个调度器类的可用进程,这样逐层递减。

    每个CPU会维护一个调度队列称之为就绪队列,每个进程只会出现在一个就绪队列中,因为同一进程不能同时被两个CPU选中执行。就绪队列的数据结构为struct rq,和上面的层次结构一样,通用调度器直接和rq打交道,而具体和进程交互的是特定于调度器类的子就绪队列。

    调度器类

    在linux内核中实现了一个调度器类的框架,其中定义了调度器应该实现的函数,每一个具体的调度器类都要实现这些函数 。

    在当前linux版本中(3.11.1),使用了四个调度器类:stop_sched_class、rt_sched_class、fair_sched_class、idle_sched_class。在最新的内核中又添加了一个调度类dl_sched_class,但是由于笔者能力所限,且大部分进程都是属于实时调度器和完全公平调度器,所以我们主要分析实时调度器和完全公平调度器。

    看下调度器类的定义:

     1 struct sched_class {
     2     const struct sched_class *next;
     3 
     4     void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
     5     void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
     6     void (*yield_task) (struct rq *rq);
     7     bool (*yield_to_task) (struct rq *rq, struct task_struct *p, bool preempt);
     8 
     9     void (*check_preempt_curr) (struct rq *rq, struct task_struct *p, int flags);
    10 
    11     struct task_struct * (*pick_next_task) (struct rq *rq);
    12     void (*put_prev_task) (struct rq *rq, struct task_struct *p);
    13 
    14 #ifdef CONFIG_SMP
    15     int  (*select_task_rq)(struct task_struct *p, int sd_flag, int flags);
    16     void (*migrate_task_rq)(struct task_struct *p, int next_cpu);
    17 
    18     void (*pre_schedule) (struct rq *this_rq, struct task_struct *task);
    19     void (*post_schedule) (struct rq *this_rq);
    20     void (*task_waking) (struct task_struct *task);
    21     void (*task_woken) (struct rq *this_rq, struct task_struct *task);
    22 
    23     void (*set_cpus_allowed)(struct task_struct *p,
    24                  const struct cpumask *newmask);
    25 
    26     void (*rq_online)(struct rq *rq);
    27     void (*rq_offline)(struct rq *rq);
    28 #endif
    29 
    30     void (*set_curr_task) (struct rq *rq);
    31     void (*task_tick) (struct rq *rq, struct task_struct *p, int queued);
    32     void (*task_fork) (struct task_struct *p);
    33 
    34     void (*switched_from) (struct rq *this_rq, struct task_struct *task);
    35     void (*switched_to) (struct rq *this_rq, struct task_struct *task);
    36     void (*prio_changed) (struct rq *this_rq, struct task_struct *task,
    37                  int oldprio);
    38 
    39     unsigned int (*get_rr_interval) (struct rq *rq,
    40                      struct task_struct *task);
    41 
    42 #ifdef CONFIG_FAIR_GROUP_SCHED
    43     void (*task_move_group) (struct task_struct *p, int on_rq);
    44 #endif
    45 };

    enqueue_task向就绪队列添加一个进程,该操作发生在一个进程变成就绪态(可运行态)的时候。

    dequeue_task就是执行enqueue_task的逆操作,在一个进程由运行态转为阻塞的时候就会发生该操作。

    yield_task用于进程自愿放弃控制权的时候。

    pick_next_task用于挑选下一个可运行的进程,发生在进程调度的时候,又调度器调用。

    set_curr_task当进程的调度策略发生变化时,需要执行此函数

    task_tick,在每次激活周期调度器时,由周期调度器调用。

    task_fork用于建立fork系统调用和调度器之间的关联,每次新进程建立后,就调用该函数通知调度器。

     就绪队列

    如前所述,每个CPU维护一个就绪队列,由结构struct rq表示,通用调度器直接和rq交互,在rq中又维护了子就绪队列,这些子就绪队列和具体的调度器类相关,进程入队出队都需要根据调度器类的具体算法。

    rq为维护针对当前CPU而言全局的信息,其结构比较庞大,这里就不详细列举。其大致内容包括当前CPU就绪队列包含的所有进程数、记载就绪队列当前负荷的度量,并嵌入子就绪队列cfs_rq和rt_rq等,系统所有的就绪队列位于一个runqueues数组中,每个CPU对应一个元素。

    内核也定义了一些宏,操作这些全局的队列:

    1 #define cpu_rq(cpu)        (&per_cpu(runqueues, (cpu)))
    2 #define this_rq()        (&__get_cpu_var(runqueues))
    3 #define task_rq(p)        cpu_rq(task_cpu(p))
    4 #define cpu_curr(cpu)        (cpu_rq(cpu)->curr)
    5 #define raw_rq()        (&__raw_get_cpu_var(runqueues))

    调度实体

    linux中可调度的不仅仅是进程,也可能是一个进程组,所以LInux就把调度对象抽象化成一个调度实体。就像是很多结构中嵌入list_node用于连接链表一样,这里需要执行调度的也就需要加入这样一个调度实体。实际上,调度器直接操作的也是调度实体,只是会根据调度实体获取到其对应的结构。

     1 struct sched_entity {
     2     struct load_weight    load;        /* for load-balancing */
     3     struct rb_node        run_node;
     4     struct list_head    group_node;
     5     unsigned int        on_rq;
     6 
     7     u64            exec_start;
     8     u64            sum_exec_runtime;
     9     u64            vruntime;
    10     u64            prev_sum_exec_runtime;
    11 
    12     u64            nr_migrations;
    13 
    14 #ifdef CONFIG_SCHEDSTATS
    15     struct sched_statistics statistics;
    16 #endif
    17 
    18 #ifdef CONFIG_FAIR_GROUP_SCHED
    19     struct sched_entity    *parent;
    20     /* rq on which this entity is (to be) queued: */
    21     struct cfs_rq        *cfs_rq;
    22     /* rq "owned" by this entity/group: */
    23     struct cfs_rq        *my_q;
    24 #endif
    25 
    26 #ifdef CONFIG_SMP
    27     /* Per-entity load-tracking */
    28     struct sched_avg    avg;
    29 #endif
    30 };

    load用于负载均衡,决定了各个实体占队列中负荷的比例,计算负荷权重是调度器的主要责任,因为选择下一个进程就是要根据这些信息。run_node是一个红黑树节点,用于把实体加入到红黑树,on_rq表明该实体是否位于就绪队列,当为1的时候就说明在就绪队列中,一个进程在得到调度的时候会从就绪队列中摘除,在让出CPU的时候会重新添加到就绪队列(正常调度的情况,不包含睡眠、等待)。在后面有一个时间相关的字段,exec_start记录进程开始在CPU上运行的时间;sum_exec_time记录进程一共在CPU上运行的时间,pre_sum_exec_time记录本地调度之前,进程已经运行的时间。在进程被调离CPU的时候,会把sum_exec_time的值保存到pre_sum_exec_time,而sum_exec_time并不重置,而是一直随着在CPU上的运行递增。而vruntime 记录在进程执行期间,在虚拟时钟上流逝的时间,用于CFS调度器,后面会具体讲述。后面的parent、cfs_rq、my_rq是和组调度相关的,这里我们暂且不涉及。

    看下load字段

    struct load_weight {
        unsigned long weight, inv_weight;
    };

    这里有两个字段,weight和inv_weight。前者是当前实体优先级对应的权重,这个可以根据prio_to_weight数组转化得到。而后者是是用于快速计算vruntime用的,可以通过prio_to_wmult数组得到,后者是一个和prio_to_weight同样大小的数组,每一项的值为2^32/weight,内核中的除法运算没那么简单,为了加速操作,选取的折中办法。vruntime的计算可以参考calc_delta_mine函数。

    到这里,调度器的基本架构就比较清楚了,调度过程中需要计算进程的优先级,这点是比较复杂的过程,我们单独分一节描述,下面根据CFS调度类探索下进程调度的过程。

    • 什么时候调度
    • 如何进行调度

    进程调度并不是什么时候都可以,前面也说过,系统会有一个周期调度器,根据频率自动调用schedule_tick函数。其主要作用就是根据进程运行时间触发调度;在进程遇到资源等待被阻塞也可以显示的调用调度器函数进行调度;另外在有内核空间返回到用户空间时,会判断当前是否需要调度,在进程对应的thread_info结构中,有一个flag,该flag字段的第二位(从0开始)作为一个重调度标识TIF_NEED_RESCHED,当被设置的时候表明此时有更高优先级的进程,需要执行调度。另外目前的内核支持内核抢占功能,在适当的时机可以抢占内核的运行。关于内核抢占,我们最后论述。

    而至于如何进行调度呢?就要看具体调度器类了。一旦确定了要进行调度,那么schedule函数被调用。注意,周期性调度器并不直接调度,至多设置进程的重调度位TIF_NEED_RESCHED,这样在返回用户空间的时候仍然由主调度器执行调度。跟踪下schedule函数,其实具体实现由__schedule函数完成,直接看该函数:

     1 static void __sched __schedule(void)
     2 {
     3     struct task_struct *prev, *next;
     4     unsigned long *switch_count;
     5     struct rq *rq;
     6     int cpu;
     7 
     8 need_resched:
     9     /*禁止内核抢占*/
    10     preempt_disable();
    11     cpu = smp_processor_id();
    12     /*获取CPU 的调度队列*/
    13     rq = cpu_rq(cpu);
    14     rcu_note_context_switch(cpu);
    15     /*保存当前任务*/
    16     prev = rq->curr;
    17 
    18     schedule_debug(prev);
    19 
    20     if (sched_feat(HRTICK))
    21         hrtick_clear(rq);
    22 
    23     /*
    24      * Make sure that signal_pending_state()->signal_pending() below
    25      * can't be reordered with __set_current_state(TASK_INTERRUPTIBLE)
    26      * done by the caller to avoid the race with signal_wake_up().
    27      */
    28     smp_mb__before_spinlock();
    29     raw_spin_lock_irq(&rq->lock);
    30 
    31     switch_count = &prev->nivcsw;
    32      /*  如果内核态没有被抢占, 并且内核抢占有效
    33         即是否同时满足以下条件:
    34         1  该进程处于停止状态
    35         2  该进程没有在内核态被抢占 */
    36     if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
    37         if (unlikely(signal_pending_state(prev->state, prev))) {
    38             prev->state = TASK_RUNNING;
    39         } else {
    40             deactivate_task(rq, prev, DEQUEUE_SLEEP);
    41             prev->on_rq = 0;
    42 
    43             /*
    44              * If a worker went to sleep, notify and ask workqueue
    45              * whether it wants to wake up a task to maintain
    46              * concurrency.
    47              */
    48             if (prev->flags & PF_WQ_WORKER) {
    49                 struct task_struct *to_wakeup;
    50 
    51                 to_wakeup = wq_worker_sleeping(prev, cpu);
    52                 if (to_wakeup)
    53                     try_to_wake_up_local(to_wakeup);
    54             }
    55         }
    56         switch_count = &prev->nvcsw;
    57     }
    58 
    59     pre_schedule(rq, prev);
    60 
    61     if (unlikely(!rq->nr_running))
    62         idle_balance(cpu, rq);
    63     /*告诉调度器prev进程即将被调度出去*/
    64     put_prev_task(rq, prev);
    65     /*挑选下一个可运行的进程*/
    66     next = pick_next_task(rq);
    67     /*清除pre的TIF_NEED_RESCHED标志*/
    68     clear_tsk_need_resched(prev);
    69     rq->skip_clock_update = 0;
    70    /*如果next和当前进程不一致,就可以调度*/
    71     if (likely(prev != next)) {
    72         rq->nr_switches++;
    73         /*设置当前调度进程为next*/
    74         rq->curr = next;
    75         ++*switch_count;
    76         /*切换进程上下文*/
    77         context_switch(rq, prev, next); /* unlocks the rq */
    78         /*
    79          * The context switch have flipped the stack from under us
    80          * and restored the local variables which were saved when
    81          * this task called schedule() in the past. prev == current
    82          * is still correct, but it can be moved to another cpu/rq.
    83          */
    84         cpu = smp_processor_id();
    85         rq = cpu_rq(cpu);
    86     } else
    87         raw_spin_unlock_irq(&rq->lock);
    88 
    89     post_schedule(rq);
    90   
    91     sched_preempt_enable_no_resched();
    92     if (need_resched())
    93         goto need_resched;
    94 }

    调度器运行期间是要禁止内核抢占的,从级别上来讲,LInux中的调度器不见得比其他进程的级别高,但是肯定不会低于普通进程,即调度器运行期间会禁止内核抢占。相比之下,windows中使用中断请求级别的概念,普通进程运行在passive level,而调度器运行在DPC level,调度器运行期间只有硬件中断可以打断。从函数代码来看,这里首先调用了preempt_disable函数设置了preempt_count禁止内核抢占,然后获取当前CPU的就绪队列结构rq,prev保存当前任务,下面的prev->state && !(preempt_count() & PREEMPT_ACTIVE)是对有些进行移除运行队列。具体就是如果当前进程是阻塞并且PREEMPT_ACTIVE没有被设置,就有了移除就绪队列的条件,然后判断是否又挂起的信号,如果有,那么暂时不移除队列,否则就执行deactivate_task函数移除队列,并设置prev->on_rq=0,表明该进程不在就绪队列中。下面的if是判断如果当前进程是一个工作线程,那么就通知工作队列,看是否需要唤醒另一个worker。

    出了if就调用了pre_schedule,该函数在CFS中没有实现,而在实时调度器中实现了,具体什么作用不太清楚

    下面的一个if判断当前CPU就绪队列是否存在可运行的进程,如果不存在即没有进程可以运行就调用idle_balance从其他的CPU平衡一下任务。当然这种情况极少见。

    接下来就要进行正式工作了,调用put_prev_task预处理下,具体是调用对应调度器类的实现函数:prev->sched_class->put_prev_task(rq, prev);主要任务是把当前任务重新加入就绪队列。当然在此之前如果当前任务还在就绪队列(或者说是当前任务是否是可运行状态),就调用update_curr更新下其进程时间,包括vruntime等。

    重要的是下面的pick_next_task,它使用对应的调度器类选择一个具体的任务作为下一个占用CPU的任务,选定好之后就调用clear_tsk_need_resched清楚prev的重调度标识。

    之后进行if判断,如果prev不是我们选择的下一个进程,就执行进程的切换。具体 先设置就绪队列的切换计数nr_switches,然后设置rq->curr=next,这里就从就绪队列而言,已经标识next为当前进程了。然后就调用context_switch函数切换上下文,主要包含两部分:切换地址空间、切换寄存器域。之后就开启内核抢占,这样一个进程切换就完成了。最后会判断是否又有新设置的高优先级进程,有的话再次执行调度。

    前面大致过程如前所述,但是具体而言,下一个进程的执行是从替换了进程的EIP开始执行的,即调度器函数不到结束就开始运行另一个进程了,而待下次这个进程重新获得控制权时,就从之前保存的状态开始运行。在__schedule函数的最后仍然会判断是否被设置了重调度位,如果被设置了,那么很不幸,又要被调度出去了,但是这种几率很小,只是以防万一而已。这样出了调度函数,正常运行了。

    核心函数实现分析:

    pick_next_task

     1 static inline struct task_struct *
     2 pick_next_task(struct rq *rq)
     3 {
     4     const struct sched_class *class;
     5     struct task_struct *p;
     6 
     7     /*
     8      * Optimization: we know that if all tasks are in
     9      * the fair class we can call that function directly:
    10      */
    11      /*如果所有任务都处于完全公平调度类,则可以直接选择下一个任务*/
    12     if (likely(rq->nr_running == rq->cfs.h_nr_running)) {
    13         p = fair_sched_class.pick_next_task(rq);
    14         if (likely(p))
    15             return p;
    16     }
    17     /*从优先级最高的调度器类开始遍历,顺序为stop_sched_class->rt_scheduled_class->fair_schedled_class->idle_sched_class*/
    18     /*
    19     #define for_each_class(class) 
    20    for (class = sched_class_highest; class; class = class->next)
    21     */
    22     for_each_class(class) {
    23         p = class->pick_next_task(rq);
    24         if (p)
    25             return p;
    26     }
    27 
    28     BUG(); /* the idle class will always have a runnable task */
    29 }

    该函数还是处于主调度器的层面,没有涉及到核心逻辑,所以还比较好理解。首先判断当前CPu就绪队列上的可运行进程数和CFS就绪队列上的可运行进程数是否一致,如果一致就说明当前主就绪队列上没有只有CFS调度类的进程,那么这样直接调用CFS调度类的方法挑选下一个进程即可。否则还需要从最高级的调度类,层层选择。下面的for_each_class便是实现这个功能。它按照stop_sched_class->rt_scheduled_class->fair_schedled_class->idle_sched_class这个顺序,依次调用其pick函数,只有前一个调度类没有找到可运行的进程,才会查找后一个调度类。我们这里值看CFS的实现:

    在fair.c中,对应的函数是pick_next_task_fair

     1 static struct task_struct *pick_next_task_fair(struct rq *rq)
     2 {
     3     struct task_struct *p;
     4     /*从CPU 的就绪队列找到公平调度队列*/
     5     struct cfs_rq *cfs_rq = &rq->cfs;
     6     struct sched_entity *se;
     7     /*如果公平调度类没有可运行的进程,直接返回*/
     8     if (!cfs_rq->nr_running)
     9         return NULL;
    10      /*如果调度的是一组进程,则需要进行循环设置,否则执行一次就退出了*/
    11     do {
    12         /*从公平调度类中找到一个可运行的实体*/
    13         se = pick_next_entity(cfs_rq);
    14         /*设置红黑树中下一个实体,并标记cfs_rq->curr为se*/
    15         set_next_entity(cfs_rq, se);
    16         cfs_rq = group_cfs_rq(se);
    17     } while (cfs_rq);
    18     /*获取到具体的task_struct*/
    19     p = task_of(se);
    20     if (hrtick_enabled(rq))
    21         hrtick_start_fair(rq, p);
    22 
    23     return p;
    24 }

    代码也是比较简单的,核心在下面的那个do循环中,从这里看该循环做了两个事情,调用pick_next_entity从CFS就绪队列中选择一个调度实体,然后调用set_next_entity设置下一个可以调度的任务,由于CFS的调度实体通过红黑树维护,所以这里实际上是调整红黑树的过程。而使用循环时应用与组调度的场合,这里我们暂且忽略。

    看下pick_next_entity

     1 static struct sched_entity *pick_next_entity(struct cfs_rq *cfs_rq)
     2 {
     3     /*从红黑树中找到最左边即等待时间最长的那个实体*/
     4     struct sched_entity *se = __pick_first_entity(cfs_rq);
     5     struct sched_entity *left = se;
     6 
     7     /*
     8      * Avoid running the skip buddy, if running something else can
     9      * be done without getting too unfair.
    10      */
    11     if (cfs_rq->skip == se) {
    12         struct sched_entity *second = __pick_next_entity(se);
    13         if (second && wakeup_preempt_entity(second, left) < 1)
    14             se = second;
    15     }
    16 
    17     /*
    18      * Prefer last buddy, try to return the CPU to a preempted task.
    19      */
    20     if (cfs_rq->last && wakeup_preempt_entity(cfs_rq->last, left) < 1)
    21         se = cfs_rq->last;
    22 
    23     /*
    24      * Someone really wants this to run. If it's not unfair, run it.
    25      */
    26     if (cfs_rq->next && wakeup_preempt_entity(cfs_rq->next, left) < 1)
    27         se = cfs_rq->next;
    28 
    29     clear_buddies(cfs_rq, se);
    30 
    31     return se;
    32 }

    该函数核心在__pick_first_entity,其本身也是很简答的,不妨看下代码:

    1 struct sched_entity *__pick_first_entity(struct cfs_rq *cfs_rq)
    2 {
    3     struct rb_node *left = cfs_rq->rb_leftmost;
    4 
    5     if (!left)
    6         return NULL;
    7 
    8     return rb_entry(left, struct sched_entity, run_node);
    9 }

    以为每次选择后都会设置好下一个应该选择的,所以这里仅仅获取下cfs_rq->rb_leftmost就可以了,然后就进行了三个if判断,但是都使用了同一个函数wakeup_preempt_entity

    然后返回相应的调用实体。last表示最后一个调用唤醒操作的进程,next表示最后一个被唤醒的进程。

     1 static int
     2 wakeup_preempt_entity(struct sched_entity *curr, struct sched_entity *se)
     3 {
     4     s64 gran, vdiff = curr->vruntime - se->vruntime;
     5 
     6     if (vdiff <= 0)
     7         return -1;
     8 
     9     gran = wakeup_gran(curr, se);
    10     if (vdiff > gran)
    11         return 1;
    12 
    13     return 0;
    14 }

    这里是对比两个实体的vruntime,如果curr->vruntime - se->vruntime大于一个固定值,那么就返回1.这个值一般是sysctl_sched_wakeup_granularity。

    所以这里逻辑当选定一个实体后,判断该实体是否是cfs_rq指定跳过的实体,如果是就选择下一个实体,判断该实体和上一个实体的vruntime的差距,只要不大于阈值,就可以接收从而选择后者。

    在设定好初始实体后,判断cfs_rq->last和left的vruntime,如果在可接受的范围内,则选择cfs_rq->last,然后接着判断cfs_rq->next,如果仍然在可接收的范围内,就选择cfs_rq->next作为最终选定的调度实体。NEXT_BUDDY表示在cfs选择next sched_entity的时候会优先选择最后一个唤醒的sched_entity,而 LAST_BUDDY表示在cfs选择next sched_entity的时候会优先选择最后一个执行唤醒操作的那个sched_entity,这两种调度策略都有助于提高cpu cache的命中率。从代码来看,next比last优先级更高!

     1 static void
     2 set_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
     3 {
     4     /* 'current' is not kept within the tree. */
     5     /*如果该实体处于就绪态,就可以被调度*/
     6     if (se->on_rq) {
     7         /*
     8          * Any task has to be enqueued before it get to execute on
     9          * a CPU. So account for the time it spent waiting on the
    10          * runqueue.
    11          */
    12         update_stats_wait_end(cfs_rq, se);
    13         /*把se出队列,然后选取一个实体设置到红黑树的最左边*/
    14         __dequeue_entity(cfs_rq, se);
    15     }
    16 
    17     update_stats_curr_start(cfs_rq, se);
    18     cfs_rq->curr = se;
    19 #ifdef CONFIG_SCHEDSTATS
    20     /*
    21      * Track our maximum slice length, if the CPU's load is at
    22      * least twice that of our own weight (i.e. dont track it
    23      * when there are only lesser-weight tasks around):
    24      */
    25     if (rq_of(cfs_rq)->load.weight >= 2*se->load.weight) {
    26         se->statistics.slice_max = max(se->statistics.slice_max,
    27             se->sum_exec_runtime - se->prev_sum_exec_runtime);
    28     }
    29 #endif
    30     se->prev_sum_exec_runtime = se->sum_exec_runtime;
    31 }

    该函数首先判断选定的实体是否可运行,如果可以就调用调度器的出队函数,把进程出队,然后调整红黑树,这里为何用个if笔者不是很懂,进程被调用前都要入队,所以这里选定的se,其on_rq肯定为1呀。难道不是么?之后设置se->exec_start记录开始运行的时间,然后设置cfs—>curr=se。之后就是设置时间等操作。然后就执行返回了。回到pick_next_task_fair函数中,这里接下来就返回了实体对应的task_struct。

     context_switch函数

     1 static inline void
     2 context_switch(struct rq *rq, struct task_struct *prev,
     3            struct task_struct *next)
     4 {
     5     struct mm_struct *mm, *oldmm;
     6     /*进程切换准备工作,需要枷锁和关中断,最后需要调用finish_task_switch*/
     7     prepare_task_switch(rq, prev, next);
     8     
     9     mm = next->mm;
    10     oldmm = prev->active_mm;
    11     /*
    12      * For paravirt, this is coupled with an exit in switch_to to
    13      * combine the page table reload and the switch backend into
    14      * one hypercall.
    15      */
    16     arch_start_context_switch(prev);
    17     /*如果将要执行的是内核线程*/
    18     if (!mm) {
    19         next->active_mm = oldmm;
    20         atomic_inc(&oldmm->mm_count);
    21         enter_lazy_tlb(oldmm, next);
    22     } else
    23         switch_mm(oldmm, mm, next);
    24     /*如果被调度的是内核线程*/
    25     if (!prev->mm) {
    26         prev->active_mm = NULL;
    27         rq->prev_mm = oldmm;
    28     }
    29     /*
    30      * Since the runqueue lock will be released by the next
    31      * task (which is an invalid locking op but in the case
    32      * of the scheduler it's an obvious special-case), so we
    33      * do an early lockdep release here:
    34      */
    35 #ifndef __ARCH_WANT_UNLOCKED_CTXSW
    36     spin_release(&rq->lock.dep_map, 1, _THIS_IP_);
    37 #endif
    38 
    39     context_tracking_task_switch(prev, next);
    40     /* Here we just switch the register state and the stack. */
    41     /*切换寄存器域和栈*/
    42     switch_to(prev, next, prev);
    43 
    44     barrier();
    45     /*
    46      * this_rq must be evaluated again because prev may have moved
    47      * CPUs since it called schedule(), thus the 'rq' on its stack
    48      * frame will be invalid.
    49      */
    50     finish_task_switch(this_rq(), prev);
    51 }

     这部分内容主要做了两件事情:切换地址空间、切换寄存器域和栈空间。整个切换过程需要加锁和关中断,首先切换的是地址空间,mm 和active_mm分别代表调度和被调度的进程的 mm_struct,如果mm为空,则表明next是内核线程,内核线程没有自己独立的地址空间,所以其mm为null,运行的时候使用prev的active_mm即可。如果非空,则是用户进程,那么可以直接切换,这里调用

    switch_mm函数进行切换;如果prev为内核线程,由于其没有独立地址空间,所以需要设置其active_mm为null。

    接下来就要调用switch_to来切换寄存器域和栈了。这也是进程切换的最后部分。

     1 #define switch_to(prev, next, last)                    
     2 do {                                    
     3     /*                                
     4      * Context-switching clobbers all registers, so we clobber    
     5      * them explicitly, via unused output variables.        
     6      * (EAX and EBP is not listed because EBP is saved/restored    
     7      * explicitly for wchan access and EAX is the return value of    
     8      * __switch_to())                        
     9      */                                
    10     unsigned long ebx, ecx, edx, esi, edi;                
    11                                     
    12     asm volatile("pushfl
    	"        /* save    flags */    
    13              "pushl %%ebp
    	"        /* save    EBP   */    
    14              "movl %%esp,%[prev_sp]
    	"    /* save    ESP   */ 
    15              "movl %[next_sp],%%esp
    	"    /* restore ESP   */ 
    16              "movl $1f,%[prev_ip]
    	"    /* save    EIP   */    
    17              "pushl %[next_ip]
    	"    /* restore EIP   */    
    18              __switch_canary                    
    19              "jmp __switch_to
    "    /* regparm call  */    
    20              "1:	"                        
    21              "popl %%ebp
    	"        /* restore EBP   */    
    22              "popfl
    "            /* restore flags */    
    23                                     
    24              /* output parameters */                
    25              : [prev_sp] "=m" (prev->thread.sp),        
    26                [prev_ip] "=m" (prev->thread.ip),        
    27                "=a" (last),                    
    28                                     
    29                /* clobbered output registers: */        
    30                "=b" (ebx), "=c" (ecx), "=d" (edx),        
    31                "=S" (esi), "=D" (edi)                
    32                                            
    33                __switch_canary_oparam                
    34                                     
    35                /* input parameters: */                
    36              : [next_sp]  "m" (next->thread.sp),        
    37                [next_ip]  "m" (next->thread.ip),        
    38                                            
    39                /* regparm parameters for __switch_to(): */    
    40                [prev]     "a" (prev),                
    41                [next]     "d" (next)                
    42                                     
    43                __switch_canary_iparam                
    44                                     
    45              : /* reloaded segment registers */            
    46             "memory");                    
    47 } while (0)

    实际上switch_to是一个宏,由一大串的汇编代码实现,这段代码稍微有点复杂,首先把标识寄存器压栈,然后ebp入栈,接着保存当前esp指针到prev->thread.sp变量中,第15行就把next->thread.sp设置到当前esp寄存器了,也就是说从现在开始使用的是next进程的栈,但是EBP 还没有切换,所以还可以使用prev进程的变量。接下来movl $1f,%[prev_ip]是把标号1的地址保存到prev->thread.ip,这个就是下次prev进程被调度的时候,开始执行的IP。到目前为止,prev进程的状态域就保存好了,接下来pushl  %[next_ip]是把next进程的起始EIP压栈,因为后面直接调用一个函数,所以在函数返回后执行执行ret指令,就直接从栈中取出地址放到EIP开始执行,next进程正是从此处开始执行即代码中标号1的位置。第19行直接使用jmp跳转到目标函数__switch_to,主要是使用call会自动push eip,这样函数返回后又从原位置开始,要执行next还需要手动切换EIP,比较麻烦。

    切换到next后,就从next进程的标号1位置开始,即popl %%ebp,popfl.需要注意的是,__switch_to的参数在最下方的部分

    [prev] "a" (prev),
    [next] "d" (next)

    被放到eax和edx中,不明白的人可能怎么也发现不了,涉及到AT&T汇编语法,这里就不在多说,具体可以参考相关文档。

    到这里进程便切换过来了,但是细心的人可能会注意到,这里switch_to本来仅仅是切换两个进程,却传递进去三个参数,这是为何?

    具体来说,这是为了让被调度到的进程知道在他之前运行的实际进程,为何这么说呢?

    下面三个调度,在switch_to执行的时候,状态如下:

    1、A——>B  prev=A next=B

    2、B——>C  prev=B next=C

    3、C——>A  prev=C next=A

    看第三次调度的时候,这个时候A重新获得控制权,恢复了A的栈状态,即在A的进程空间,prev=A next=B,而A并不知道在他之前实际运行的是C,所以需要一种方式告知A,在他之前实际运行的进程是C。

    参考资料:

    1. http://blog.chinaunix.net/uid-27767798-id-3548384.html
    2. linux内核3.11.1源码
  • 相关阅读:
    编译原理 实例
    lex yacc flex bison
    图解tensorflow 源码分析
    PostgreSQL 179个场景 案例集锦
    github view source
    Java 微服务实践
    Linux kernel AIO
    Lex与Yacc学习
    OpenResty 通过 Lua 扩展 NGINX 实现的可伸缩的 Web 平台
    nginx Architecture
  • 原文地址:https://www.cnblogs.com/ck1020/p/6089970.html
Copyright © 2011-2022 走看看