美文网首页
进程调度跟踪分析

进程调度跟踪分析

作者: uglyyouth | 来源:发表于2015-04-26 22:46 被阅读0次

    此文仅用于MOOCLinux内核分析作业

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


    进程调度

    Linux的调度程序是一个叫schedule()的函数,这个函数被调用的频率很高,由它来决定是否要进行进程的切换,如果要切换的话,切换到哪个进程等等。

    Linux调度时机主要有:

    1. 中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule()
    2. 内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;
    3. 用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。

    代码分析

    关键函数的调用关系:

    schedule() --> context_switch() --> switch_to --> __switch_to()
    
    • schedule()

    这里调用__schedule(),tsk为当前进程.

    asmlinkage __visible void __sched schedule(void)
    {
        struct task_struct *tsk = current;
    
        sched_submit_work(tsk);
        __schedule();
    }
    
    • __schedule();

    该函数包含了一些:

    1. 针对抢占的处理
    2. 自旋锁(raw_spin_lock_irq(&rq->lock);)
    3. 检查prev的状态,并且重设state的状态
    4. 进程调度算法(next = pick_next_task(rq, prev);)
    5. 更新就绪队列的时钟
    6. 进程上下文切换(context_switch(rq, prev, next);)
    static void __sched __schedule(void)
    {
        struct task_struct *prev, *next;
        unsigned long *switch_count;
        struct rq *rq;
        int cpu;
    
    ...
    //调度算法
        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

    在挑选得到了下一个即将被调度进来的进程之后,如果被选中的进程不是当前正在运行的进程,那么需要进行上下文切换以执行被选中的进程即context_switch.

    context_switch中包含了:

    1. 判断是否为内核线程,即是否需要上下文切换(mm)
      • 如果next是一个普通进程,schedule( )函数用next的地址空间替换prev的地址空间
      • 如果prev是内核线程或正在退出的进程,context_switch()函数就把指向prev内存描述符的指针保存到运行队列的prev_mm字段中,然后重新设置prev->active_mm
    2. 切换堆栈和寄存器(switch_to(prev, next, prev);)

    ps:宏switch_to用来进行关键上下文切换

    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);
        /* Here we just switch the register state and the stack. */
        switch_to(prev, next, prev);
    
        barrier();
    
        finish_task_switch(this_rq(), prev);
    }
    

    • 宏switch_to
    
    #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   */
                 "movl %%esp,%[prev_sp]\n\t"    /* save    ESP   */
                 "movl %[next_sp],%%esp\n\t"    /* restore ESP   */
                 "movl $1f,%[prev_ip]\n\t"  /* save    EIP   */
                 "pushl %[next_ip]\n\t" /* restore EIP   */
                 __switch_canary
                 "jmp __switch_to\n"    /* regparm call  */
                 "1:\t"
                 "popl %%ebp\n\t"       /* restore EBP   */
                 "popfl\n"          /* restore flags */
    
                 /* 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
    
                   /* 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");
    } while (0)
    
    

    这个宏实现了进程之间的真正切换:

    • 首先在当前进程prev的内核栈中保存esi,edi及ebp寄存器的内容。
    • 然后将prev的内核堆栈指针ebp存入prev->thread.esp中。
    • 把将要运行进程next的内核栈指针next->thread.esp置入esp寄存器中
    • 将popl指令所在的地址保存在prev->thread.eip中,这个地址就是prev下一次被调度
    • 通过jmp指令(而不是call指令)转入一个函数__switch_to()
    • 恢复next上次被调离时推进堆栈的内容。从现在开始,next进程就成为当前进程而真正开始执行。

    内核堆栈情况:

    stack1.png stack2.png stack3.png
    • __switch_to函数

    在宏switch_to中,用jmp跳转到该函数运行.

    该函数主要进行一些针对TSS的操作,不再赘述

    __visible __notrace_funcgraph struct task_struct *
    __switch_to(struct task_struct *prev_p, struct task_struct *next_p)
    {
        struct thread_struct *prev = &prev_p->thread,
                     *next = &next_p->thread;
        int cpu = smp_processor_id();
        struct tss_struct *tss = &per_cpu(init_tss, cpu);
        fpu_switch_t fpu;
    
    
        fpu = switch_fpu_prepare(prev_p, next_p, cpu);
    
    
        load_sp0(tss, next);
    
    
        lazy_save_gs(prev->gs);
    
    
        load_TLS(next, cpu);
    
    
        if (get_kernel_rpl() && unlikely(prev->iopl != next->iopl))
            set_iopl_mask(next->iopl);
    
    
        task_thread_info(prev_p)->saved_preempt_count = this_cpu_read(__preempt_count);
        this_cpu_write(__preempt_count, task_thread_info(next_p)->saved_preempt_count);
    
    
        if (unlikely(task_thread_info(prev_p)->flags & _TIF_WORK_CTXSW_PREV ||
                 task_thread_info(next_p)->flags & _TIF_WORK_CTXSW_NEXT))
            __switch_to_xtra(prev_p, next_p, tss);
    
    
        arch_end_context_switch(next_p);
    
        this_cpu_write(kernel_stack,
              (unsigned long)task_stack_page(next_p) +
              THREAD_SIZE - KERNEL_STACK_OFFSET);
    
    
        if (prev->gs | next->gs)
            lazy_load_gs(next->gs);
    
        switch_fpu_finish(next_p, fpu);
    
        this_cpu_write(current_task, next_p);
    
        return prev_p;
    }
    

    GDB调试

    使用MenuOS进行调试,并设置合适的断点.

    • 首先在schedule处停下来:
    process1.png
    • 查看当前进程tsk,观察到该进程pid=1,stack=0xC7858000
    process2.png
    • 继续执行,到__schedule中的关键函数pick_next_task停下
    process3.png
    • 查看队列rq
    process4.png
    • context_switch
    process5.png
    • switch_to宏&__switch_to函数
    process6.png
    • 在这里查看切换的进程prev&next,prev就是最开始tsk
    process7.png process8.png

    总结

    1. Linux的调度程序是一个叫schedule()的函数,这个函数被调用的频率很高,由它来决定是否要进行进程的切换,如果要切换的话,切换到哪个进程等等。
    2. Linux系统的一般执行过程主要在进程X切换到进程Y
      • 正在运行的用户态进程X
      • 发生中断
      • SAVE_ALL
      • 中断处理过程中或中断返回前调用了schedule()
      • 开始运行用户态进程Y
      • restore_all
      • iret
      • 继续运行用户态进程Y
    3. 内核线程主动调用schedule(),只有进程上下文的切换
    4. switch_to实现了进程之间的真正切换

    参考

    1. 进程管理之schedule->context_switch()
    2. 深入分析Linux内核源码
    3. Context switch in Linux

    相关文章

      网友评论

          本文标题:进程调度跟踪分析

          本文链接:https://www.haomeiwen.com/subject/zobmfttx.html