Linux内核分析(八)Linux中的进程调度与进程切换

本文将包括以下内容:

1. Linux中进程调度的时机

2. Linux的进程调度函数schedule()处理过程分析

3. 进程上下文切换过程分析

一、Linux中进程调度的时机

    进程调度函数schedule在Linux的源代码文件中有非常多的地方会调用,包括各种设备驱动程序(网络设备,文件系统,声卡等等)中,用cscope可以找到500+处调用。而我们今天将只关注内核部分,也就是kernel目录下的代码中调用schedule的地方。一共找到53处,如下面两截图所示:

至于在这些地方进行进程调度的原因,我用了一个取巧的办法就是去查看schedule函数的注释,发现注释写的还真是非常详细,对理解进程调度非常有帮助。

/*

 * __schedule() is the main scheduler function.

 * __schedule()函数是主要的进程调度函数

 * The main means of driving the scheduler and thus entering this function are:

 * 主要的意思是进程调度的驱动器,所以,在下面几种情况下会调用该函数

 *   1. Explicit blocking: mutex, semaphore, waitqueue, etc.

 *  1. 显式的阻塞,如被同步锁,信号量,等待队列等所阻塞的时候

 *   2. TIF_NEED_RESCHED flag is checked on interrupt and userspace return

 *      paths. For example, see arch/x86/entry_64.S.

 *      To drive preemption between tasks, the scheduler sets the flag in timer

 *      interrupt handler scheduler_tick().

 * 2. TIF_NEED_RESCHED标记被中断处理程序和用户态返回处理的过程中被设置

 *    为了在进程之间实现抢占优先调度,调度器在定时器中断处理函数scheduler_tick()函数中设置该标志

 *

 *   3. Wakeups don't really cause entry into schedule(). They add a

 *      task to the run-queue and that's it.

 *  3. 唤醒一个进程的时候并不实际调用schedule()函数,而只是在运行队列中添加一条任务。

 *      Now, if the new task added to the run-queue preempts the current

 *      task, then the wakeup sets TIF_NEED_RESCHED and schedule() gets

 *      called on the nearest possible occasion:

 *  现在,如果新添加到运行队列中的任务要抢占当前的任务,唤醒函数会设置TIF_NEED_RESCHED标志,所以,调度器会在下一次被调用时运行这个进程。调度时机包括:

 *       - If the kernel is preemptible (CONFIG_PREEMPT=y):

 *      - 内核被配置成抢占式的

 *         - in syscall or exception context, at the next outmost

 *           preempt_enable(). (this might be as soon as the wake_up()'s

 *           spin_unlock()!)

 *      - 

 *         - in IRQ context, return from interrupt-handler to

 *           preemptible context

 *

 *       - If the kernel is not preemptible (CONFIG_PREEMPT is not set)

 *         then at the next:

 * - 如果内核没有被配置成可抢占式的,则在下列情况下也会执行进程调度

 *          - cond_resched() call                                            // cond_resched()被调用

 *          - explicit schedule() call                                        // schedule函数被显式调用

 *          - return from syscall or exception to user-space // 从系统调用或异常处理中返回用户态

 *          - return from interrupt-handler to user-space    // 从终端处理程序中返回用户态

 */

二、进程调度函数schedule()处理过程分析

schedule()函数的实现在core.c文件中,如下:

asmlinkage __visible void __sched schedule(void)

{

        struct task_struct *tsk = current;

        sched_submit_work(tsk);    // 提交IO请求用于防止死锁

        __schedule();    // 主要的调度处理

}

__schedule()函数的实现和解释如下,关键处理的注释做了加粗并标注成了蓝色:

static 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);                   // 获取当前正在CPU上运行的进程信息

        rcu_note_context_switch(cpu);           

        prev = rq->curr;                     // 将当前的进程保存为新的prev进程

        schedule_debug(prev);         // 调试进程调度函数的额外信息

        if (sched_feat(HRTICK))

                hrtick_clear(rq);

        smp_mb__before_spinlock();  // 一些精细的特殊处理,防止死锁的

        raw_spin_lock_irq(&rq->lock);  // 在要操作的rq结构上加锁

        switch_count = &prev->nivcsw;    

        if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {

                if (unlikely(signal_pending_state(prev->state, prev))) {

                        prev->state = TASK_RUNNING;

                } else {

                        deactivate_task(rq, prev, DEQUEUE_SLEEP);

                        prev->on_rq = 0;         // 将当前进程挂起

                        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;  

        }

        if (task_on_rq_queued(prev) || rq->skip_clock_update < 0)

                update_rq_clock(rq);

        next = pick_next_task(rq, prev);    // 调用具体的调度算法,从进程队列中取出下一个要运行的进程

        clear_tsk_need_resched(prev);

        clear_preempt_need_resched();   // 清除一些调度标志

        rq->skip_clock_update = 0;

        if (likely(prev != next)) {

                rq->nr_switches++;

                rq->curr = next;

                ++*switch_count;

                context_switch(rq, prev, next); /* 执行进程切换 */

                cpu = smp_processor_id();

                rq = cpu_rq(cpu);                     /*重新获得当前正在运行的进程信息,因为我们已经切换到新进程上了*/

        } else

                raw_spin_unlock_irq(&rq->lock);

        post_schedule(rq);

        sched_preempt_enable_no_resched();

        if (need_resched())

                goto need_resched;

}

context_switch函数的实现如下,为了能更清楚的看到整体的结构,删掉了一些大段的注释,并对关键步骤做了加粗标注:

static inline void context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next)

{

        struct mm_struct *mm, *oldmm;

        prepare_task_switch(rq, prev, next);

        mm = next->mm;

        oldmm = prev->active_mm;

        arch_start_context_switch(prev);

        if (!mm) {

                next->active_mm = oldmm;

                atomic_inc(&oldmm->mm_count);

                enter_lazy_tlb(oldmm, next);

        } else

                switch_mm(oldmm, mm, next);

        if (!prev->mm) {

                prev->active_mm = NULL;

                rq->prev_mm = oldmm;

        }

        spin_release(&rq->lock.dep_map, 1, _THIS_IP_);

        context_tracking_task_switch(prev, next);

        switch_to(prev, next, prev);        // 具体处理过程见第三部分

        barrier();

        finish_task_switch(this_rq(), prev);

}

跟踪schedule函数执行过程的方法也非常简单,因为我们有那么多地方都会调用schedule函数,所以用之前的方法启动内核之后,只需要在函数schedule处设置一个断点,内核就会在下一次调用schedule函数的时候停在断点的位置:

三、上下文切换宏switch_to解析

上面进程切换的最关键部分swtich_to实现了不同进程的CPU寄存器内容的切换,是硬件相关的,我们找到32位X86平台的实现代码来分析。(整洁期间,删掉了源文件中的大段注释)

#define switch_to(prev, next, last)                                     \

do {                                                                    \

        unsigned long ebx, ecx, edx, esi, edi;                          \

        asm volatile("pushfl\n\t"               /* save    flags */     \         // 保存状态寄存器

                     "pushl %%ebp\n\t"          /* save    EBP   */     \      // 保存栈底指针EBP到栈上

                     "movl %%esp,%[prev_sp]\n\t"        /* save    ESP   */ \   // 把ESP保存到进程结构的sp字段中

                     "movl %[next_sp],%%esp\n\t"        /* restore ESP   */ \  // 将要调入的进程的ESP值设置给ESP寄存器

                     "movl $1f,%[prev_ip]\n\t"  /* save    EIP   */  \  // 将标号1的代码地址保存到换出的进程结构的IP字段

                     "pushl %[next_ip]\n\t"     /* restore EIP   */     \    // 将要调入的进程曾经保存的IP值设置给EIP

                     __switch_canary                                    \            // 64位X86上有些额外的事情做,32位X86该宏是空

                     "jmp __switch_to\n"        /* regparm call  */     \   // 跳到__switch_to函数,将正式跳入新进程去执行

                     "1:\t"                                             \                        // 这是某进程被换入时将开始执行的地方

                     "popl %%ebp\n\t"           /* restore EBP   */     \   // 恢复EBP

                     "popfl\n"                  /* restore flags */     \           // 恢复状态寄存器,随后CPU将继续执行上次调用

// schedule函数的下面的代码,也就是上次被挂起的进程继续执行

                     /* output parameters */                            \     

                     : [prev_sp] "=m" (prev->thread.sp),                \

                       [prev_ip] "=m" (prev->thread.ip),                \

                       "=a" (last),                                     \

                                                                        \

                       /* clobbered output registers: */                \

                       "=b" (ebx), "=c" (ecx), "=d" (edx),              \

                       "=S" (esi), "=D" (edi)                           \               

                                                                        \

                       __switch_canary_oparam                           \     //  , [stack_canary] "=m" (stack_canary.canary)

                                                                        \

                       /* 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          \ //, [task_canary] "i" (offsetof(struct task_struct, stack_canary))

                                                                        \

                     : /* reloaded segment registers */                 \

                        "memory");                                      \  //  上面都是嵌入式汇编用到的变量

} while (0)

猜你喜欢

转载自blog.csdn.net/yubo112002/article/details/82527196