进程调度时机跟踪分析进程调度与进程切换的过程

罗冲 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

1. 实验准备

2. 代码分析

2.1 进程调度前

当进程发生切换的时候,从堆栈中可以看到
进程调度时机跟踪分析进程调度与进程切换的过程_第1张图片
查看此处代码:

work_resched:
    call schedule      ---第597行代码
    LOCKDEP_SYS_EXIT

当系统接收到中断的时候(一般是时间中断),系统进入中断程序,然后调用schedule函数

2.2 进程的

系统调用schedule()函数后,其最后会调用__schedule()

    static void __sched __schedule(void)
    {
        ... ... 

        // 获取下一个调度实体
        next = pick_next_task(rq, prev);
        ... ... 

            //进程的上下文切换
            context_switch(rq, prev, next); /* unlocks the rq */
        ... ... 
    }

对于调度来说,可以简化为两个步骤。进程的选择,与进程上下文件的切换

2.2.1 进程的选择

进程调度时机跟踪分析进程调度与进程切换的过程_第2张图片
(原图:http://blog.chinaunix.net/uid-26772321-id-4847184.html
从这个图中,可以看出,CPU对应包含一个运行队列结构(struct rq),而每个运行队列又包含有实时运行队列(struct rt_rq)、普通进程运行队列(struct cfs_rq)与dl队列。我们可以理解成对于每个CPU而言,它都会管理一个运行队列。当我们创建一个进程的时候,都会将创建的进程加入到这个队列中。
其简单的过程:

2.2.1.1 新进程的加入

回忆一下进程的创建。通过系统调调用sys_clone —> do_fork(),在这个do_fork()中,系统调用copy_process()函数,将父进程copy一份给子进程。而在这个copy_process()中会调用函数

 /* Perform scheduler related setup. Assign this task to a CPU. */ retval = sched_fork(clone_flags, p);

查看这个函数的实现:

    int sched_fork(unsigned long clone_flags, struct task_struct *p)
    {
        unsigned long flags;
        /* 获取当前CPU,并且禁止抢占 */
        int cpu = get_cpu();

        /* 初始化跟调度相关的值,比如调度实体,运行时间等 */
        __sched_fork(clone_flags, p);
        /*
         *将进程标志为running状态,但是此时没有实际运行。 在它被插入到运行队列之前,没有任务信号或外部事件能够唤醒它。
         */
        p->state = TASK_RUNNING;

        /*
         * 根据父进程的运行优先级设置设置进程的优先级
         */
        p->prio = current->normal_prio;
        .... .... 


        } else if (rt_prio(p->prio)) {
            /* 根据优先级判断,如果是实时进程,设置其调度类为rt_sched_class */
            p->sched_class = &rt_sched_class;
        } else {
            /* 如果是普通进程,设置其调度类为fair_sched_class .注意这个函数实现:
             *
             *const struct sched_class fair_sched_class = {
          *     .next           = &idle_sched_class,
         *    .enqueue_task     = enqueue_task_fair,
            */
            p->sched_class = &fair_sched_class;
        }
        ... ... 
        /* 设置新进程的CPU为当前CPU */
        set_task_cpu(p, cpu);

        ... ... 
        return 0;
    }

经过此函数后,进程所对应的CPU、调度策略都已经设置完成。但此时进程还不能被调度。直到do_fork()函数中的

long do_fork(...)
{
      ... ... 

    p = copy_process(clone_flags, stack_start, stack_size,
             child_tidptr, NULL, trace);
       ... ....

        wake_up_new_task(p); //新进程被加入到调度队列中

       ... ....
}

查看一下wake_up_new_task的代码

void wake_up_new_task(struct task_struct *p)
{
  ... ...
  //将新进程放入到队列中去
    activate_task(rq, p, 0);
  ... ... 
}

activate_task()函数比较简单,它最后会调到enqueue_task(rq, p, flags)中

static void enqueue_task(struct rq *rq, struct task_struct *p, int flags)
{
    update_rq_clock(rq);
    sched_info_queued(rq, p);
    p->sched_class->enqueue_task(rq, p, flags);
}

而此时p->sched_class->enqueue_task的赋值,可以看到,它是

             const struct sched_class fair_sched_class = {
               .next            = &idle_sched_class,
             .enqueue_task      = enqueue_task_fair,
             ... ... 
     }

正是通过enqueue_task_fair()将新进程加入到队列中去

2.2.1.2 进程的选择

当系统调用schedule()函数时,会调用pick_next_task函数来选择一个新进程。

static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev)
{
    const struct sched_class *class = &fair_sched_class;

    ... ....
//一般来说都是使用fair_sched调用,开始选择进程了
    p = fair_sched_class.pick_next_task(rq, prev);
  ..... 
}

通过fair_sched_class的定义,可以查到pick_next_task的函数定义:

const struct sched_class fair_sched_class = {
    .next           = &idle_sched_class,
    .enqueue_task       = enqueue_task_fair,
        ... ... 

    .pick_next_task     = pick_next_task_fair,
    .put_prev_task      = put_prev_task_fair,

查看pick_next_task_fail的代码

static struct task_struct *
pick_next_task_fair(struct rq *rq, struct task_struct *prev)
{
  //从cfs队列中查找
    struct cfs_rq *cfs_rq = &rq->cfs;
     ... ....


    do {

        if (curr && curr->on_rq)
            update_curr(cfs_rq);
        else
            curr = NULL;

    //链表中查找。
        se = pick_next_entity(cfs_rq, curr);
        cfs_rq = group_cfs_rq(se);
    } while (cfs_rq);

    p = task_of(se);


}

2.2.2 进程上下文的切换

当进程被选出来之后,就会调用context_switch()函数进行进程切换,而这其中,切换的实际动作是由函数

static inline void
context_switch(struct rq *rq, struct task_struct *prev,
           struct task_struct *next)
{
        ... ... 
    /* Here we just switch the register state and the stack. */
    switch_to(prev, next, prev);
        ... ... 
}

switch_to的实现是switch_to.h通过汇编实现

volatile("pushfl\n\t"       /* save flags */ \//保存当前进程的flags
   "pushl %%ebp\n\t"        /* save EBP */ \//把当前进程的堆栈的基址压栈
   "movl %%esp,%[prev_sp]\n\t"  /* save ESP */ \//把当前的栈顶保存到prev->thread.sp
   "movl %[next_sp],%%esp\n\t"  /* restore ESP */ \//把下一进程的thread.sp保存到esp中,此时完成内核堆栈的切换。 从此句开始,所有的动作都是在next内核堆栈里面完成了。
   "movl $1f,%[prev_ip]\n\t"   /* save EIP */ \//保存当前进程的eip
   "pushl %[next_ip]\n\t"   /* restore EIP */ \//把next进程的thread->ip保存到next进程的堆栈中
   __switch_canary                  \
   "jmp __switch_to\n"  /* regparm call */ \//使用寄存器传递参数。 即下面的a与d。 当函数__switch_to不是通过call来调用,因此,不会保存eip。当函数返回的时候,它还是会pop eip,此eip就是 $1f,即从下一条开始执行
   "1:\t"                       \    //从此处开始,被认为是next进程开始执行了
   "popl %%ebp\n\t"     /* restore EBP */ \
   "popfl\n"            /* restore flags */ \
                        \
   /* output parameters */              \
   : [prev_sp] "=m" (prev->thread.sp),      \   //当前进程是在内核态,此时sp为内核堆栈
     [prev_ip] "=m" (prev->thread.ip),      \//当前进程的eip
     "=a" (last),                   \
                        \
     /* clobbered output registers: */      \
     "=b" (ebx), "=c" (ecx), "=d" (edx),        \
     "=S" (esi), "=D" (edi)             \
                                \
     __switch_canary_oparam             \
                        \
     /* input parameters: */                \
   : [next_sp]  "m" (next->thread.sp),      \//下一进程的内核堆栈的栈底
     [next_ip]  "m" (next->thread.ip),      \//下一个进程的执行的起点
                                \
     /* regparm parameters for __switch_to(): */    \
     [prev]     "a" (prev),             \
     [next]     "d" (next)              \
                        \
     __switch_canary_iparam             \
                        \
   : /* reloaded segment registers */           \
"memory");                  \

3. 总结

  1. 系统一般通过时钟中断,调用entry_32.S中的work_resched,实现调度的切换
  2. 每个CPU会保存一个进程队列,当发生调度时,cpu会从此队列中获取一个进程执行
  3. 当发生调度时,系统会保存当前进程的flags、eip、ebp。 当下次此进程被重新调度时,会通过ebp、eip、flags恢复执行

4. 参考文档

  1. http://blog.chinaunix.net/uid-26772321-id-4888185.html

你可能感兴趣的:(进程调度时机跟踪分析进程调度与进程切换的过程)