zoukankan      html  css  js  c++  java
  • Linux内核分析——第八周学习笔记

    实验作业:进程调度时机跟踪分析进程调度与进程切换的过程

    20135313吴子怡.北京电子科技学院 

    【第一部分】理解Linux系统中进程调度的时机

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

    2.Linux调度时机主要有:

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

    【第二部分】使用gdb跟踪分析一个schedule()函数 ,验证对Linux系统进程调度与进程切换过程的理解

    使用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

    【第三部分】分析switch_to中的汇编代码,理解进程上下文的切换机制,以及与中断上下文切换的关系

    1.关键函数的调用关系:

    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()

    该函数包含了一些:

    针对抢占的处理
    自旋锁(raw_spin_lock_irq(&rq->lock);)
    检查prev的状态,并且重设state的状态
    进程调度算法(next = pick_next_task(rq, prev);)
    更新就绪队列的时钟
    进程上下文切换(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中包含了:

    ①判断是否为内核线程,即是否需要上下文切换

    如果next是一个普通进程,schedule( )函数用next的地址空间替换prev的地址空间
    如果prev是内核线程或正在退出的进程,context_switch()函数就把指向prev内存描述符的指针保存到运行队列的prev_mm字段中,然后重新设置prev->active_mm

    ②切换堆栈和寄存器(switch_to(prev, next, prev);)

    注意:宏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
    	"       /* save    flags */
                 "pushl %%ebp
    	"      /* save    EBP   */
                 "movl %%esp,%[prev_sp]
    	"    /* save    ESP   */
                 "movl %[next_sp],%%esp
    	"    /* restore ESP   */
                 "movl $1f,%[prev_ip]
    	"  /* save    EIP   */
                 "pushl %[next_ip]
    	" /* restore EIP   */
                 __switch_canary
                 "jmp __switch_to
    "    /* regparm call  */
                 "1:	"
                 "popl %%ebp
    	"       /* restore EBP   */
                 "popfl
    "          /* 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进程就成为当前进程而真正开始执行。
    • __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;
    }

    【第四部分】堆栈状态、CPU寄存器状态分析

    1.内核堆栈情况:

    stack1.png
    stack2.png
    stack3.png
     

    【第五部分】总结

    对“Linux系统一般执行过程”的理解

    1.在调度时机方面,内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度。

    2.schedule()函数实现进程调度,context_ switch完成进程上下文切换,switch_ to完成寄存器的切换。

    3.用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。

    【第六部分】附录

    作者:吴子怡

    学号:20135313

    原创作品转载请注明出处

    《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

  • 相关阅读:
    wordpress ImetaWeblog
    日期替换,正则
    大文本编辑程序
    [C#]使用 Bing Sharp 來做 Bing 翻譯[转]
    uc密码产生方式。
    运行时出现 “child”不是此父级的子控件。
    太犯傻了。。。。
    mysql中使用rand函数得到随机整数
    混合模式程序集是针对“v2.0.50727”版的运行时生成的,在没有配置其他信息的情况下,无法在 4.0 运行时中加载该程序集。
    获取 httponly 的 cookie
  • 原文地址:https://www.cnblogs.com/paperfish/p/5380361.html
Copyright © 2011-2022 走看看