zoukankan      html  css  js  c++  java
  • lab8:理解进程调度时机跟踪分析进程调度与进程切换的过程

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

    一.实验原理

    1.操作系统的基本概念

    任何计算机系统都包含一个基本的程序集合,称为操作系统。

    – 内核(进程管理,进程调度,进程间通讯机制,内存管理,中断异常处理,文件系统,I/O系统,网络部分)

    – 其他程序(例如函数库、shell程序、系统程序等等)

    操作系统的目的

    – 与硬件交互,管理所有的硬件资源

    – 为用户程序(应用程序)提供一个良好的执行环境

    2.典型的Linux操作系统的结构

    3.站在CPU执行指令的角度

    4.从内存的角度来看

    5.linux内核进程切换

     首先简单提一下这个宏和函数的被调用关系:
        schedule() --> context_switch() --> switch_to --> __switch_to() 
        这里面,schedule是主调度函数,涉及到一些调度算法,这里不讨论。当schedule()需要暂停A进程的执行而继续B进程的执行时,就发生了进程之间的切换。进程切换主要有两部分:1、切换全局页表项;2、切换内核堆栈和硬件上下文。这个切换工作由context_switch()完成。其中switch_to__switch_to()主要完成第二部分。更详细的,__switch_to()主要完成硬件上下文切换,switch_to主要完成内核堆栈切换。
          阅读switch_to时请注意:这是一个宏,不是函数,它的参数prev, next, last不是值拷贝,而是它的调用者context_switch()的局部变量。局部变量是通过%ebp寄存器来索引的,也就是通过n(%ebp),n是编译时决定的,在不同的进程的同一段代码中,同一局部变量的n是相同的。在switch_to中,发生了堆栈的切换,即ebp发生了改变,所以要格外留意在任一时刻的局部变量属于哪一个进程。关于__switch_to()这个函数的调用,并不是通过普通的call来实现,而是直接jmp,函数参数也并不是通过堆栈来传递,而是通过寄存器来传递。
         在下文中提到一些局部变量和寄存器值,为了不引起混淆,在名字后面加上_X,表示是X进程的成员。如esp_A表示进程A的esp的值,prev_B,表示进程B中的prev变量,等等。

        switch_to切换主要有以下三部分:

    进程切换

    即esp的切换

    由于从esp可以找到进程的描述符

    硬件上下文切换

    __switch_to()

    以前通过x86硬件支持,现在使用软件切换

    堆栈的切换

    即ebp的切换

    ebp是栈底指针,它确定了当前变量空间属于哪个进程

                              
         上面的四个步骤中,有三个是在switch_to宏中完成,硬件上下文切换由__switch_to()函数完成。

         下面来具体看switch_to从A进程切换到B进程的步骤。

    step1:复制两个变量到寄存器:
        [prev] "a" (prev)
        [next] "d" (next)
        即:
        eax <== prev_A 或eax <==%p(%ebp_A)
        edx <== next_A 或edx <==%n(%ebp_A)
        这里prev和next都是A进程的局部变量。

    step2:保存进程A的ebp和eflags
        pushfl
        pushl %ebp
        注意,因为现在esp还在A的堆栈中,所以这两个东西被保存到A进程的内核堆栈中。

    step3:保存当前esp到A进程内核描述符中:
        "movl %%esp,%[prev_sp]/n/t"    /*save    ESP   */
        它可以表示成:prev_A->thread.sp <== esp_A
        在调用switch_to时,prev是指向A进程自己的进程描述符的。

    step4:从next(进程B)的描述符中取出之前从B切换出去时保存的esp_B。

       "movl%[next_sp],%%esp/n/t" /* restore ESP */
        它可以表示成:esp_B<== next_A->thread.sp
        注意,在A进程中的next是指向B的进程描述符的。
        从这个时候开始,CPU当前执行的进程已经是B进程了,因为esp已经指向B的内核堆栈。但是,现在的ebp仍然指向A进程的内核堆栈,所以所有局部变量仍然是A中的局部变量,比如next实质上是%n(%ebp_A),也就是next_A,即指向B的进程描述符。


    step5:把标号为1的指令地址保存到A进程描述符的ip域:
        "movl $1f,%[prev_ip]/n/t"    /*save    EIP   */
        它可以表示成:prev_A->thread.ip<== %1f,当A进程下次被switch_to回来时,会从这条指令开始执行。具体方法看后面被切换回来的B的下一条指令。

    step6:将返回地址保存到堆栈,然后调用__switch_to()函数,__switch_to()函数完成硬件上下文切换。
        "pushl %[next_ip]/n/t"    /*restore EIP   */
        "jmp __switch_to/n"    /* regparmcall  */
        这里,如果之前B也被switch_to出去过,那么[next_ip]里存的就是下面这个1f的标号,但如果进程B刚刚被创建,之前没有被switch_to出去过,那么[next_ip]里存的将是ret_ftom_fork(参看copy_thread()函数)。这就是这里为什么不用call__switch_to而用jmp,因为call会导致自动把下面这句话的地址(也就是1:)压栈,然后__switch_to()就必然只能ret到这里,而无法根据需要ret到ret_from_fork。
        另外请注意,这里__switch_to()返回时,将返回值prev_A又写入了%eax,这就使得在switch_to宏里面eax寄存器始终保存的是prev_A的内容,或者,更准确的说,是指向A进程描述符的“指针”。这是有用的,下面step8中将会看到。

    step7:从__switch_to()返回后继续从1:标号后面开始执行,修改ebp到B的内核堆栈,恢复B的eflags:

        "popl %%ebp/n/t"       /* restore EBP   */    
        "popfl/n"           /* restore flags */
        如果从__switch_to()返回后从这里继续运行,那么说明在此之前B肯定被switch_to调出过,因此此前肯定备份了ebp_B和flags_B,这里执行恢复操作。
         注意,这时候ebp已经指向了B的内核堆栈,所以上面的prev,next等局部变量已经不是A进程堆栈中的了,而是B进程堆栈中的(B上次被切换出去之前也有这两个变量,所以代表B堆栈中prev、next的值了),因为prev == %p(%ebp_B),而在B上次被切换出去之前,该位置保存的是B进程的描述符地址。如果这个时候就结束switch_to的话,在后面的代码中(即 context_switch()函数中switch_to之后的代码)的prev变量是指向B进程的,因此,进程B就不知道是从哪个进程切换回来。而从context_switch()中switch_to之后的代码中,我们看到finish_task_switch(this_rq(), prev)中需要知道之前是从哪个进程切换过来的,因此,我们必须想办法保存A进程的描述符到B的堆栈中,这就是last的作用。

    step8:将eax写入last,以在B的堆栈中保存正确的prev信息。
        "=a" (last)  即 last_B <== %eax
        而从context_switch()中看到的调用switch_to的方法是:
        switch_to(prev, next, prev);
        所以,这里面的last实质上就是prev,因此在switch_to宏执行完之后,prev_B就是正确的A的进程描述符了。   
        这里,last的作用相当于把进程A堆栈中的A进程描述符地址复制到了进程B的堆栈中。

         至此,switch_to已经执行完成,A停止运行,而开始了B。在以后,可能在某一次调度中,进程A得到调度,就会出现switch_to(C, A)这样的调用,这时,A再次得到调度,得到调度后,A进程从context_switch()中switch_to后面的代码开始执行,这时候,它看到的prev_A将指向C的进程描述符。

         注意,这里有两个堆栈,在这个过程中,有一个时期esp和ebp并不在同一个堆栈上,要格外注意这个时期里所有涉及堆栈的操作分别是在哪个堆栈上进行的。记住一个简单的原则即可,pop/push这样的操作,都是对esp所指向的堆栈进行的,这些操作同时也会改变esp本身,除此之外,其它关于变量的引用,都是对ebp所指向的堆栈进行的。
        
        下面我们从switch_to被调用的情况来看一下这个执行过程。

         这里,为了便于理解,我们首先忽略switch_to中的具体细节,仅仅把它当作一个普通的指令。对A进程来说,它始终没有感觉到自己被打断过,它认为自己一直是不间断执行的。switch_to这条“指令”,除了改变了A进程中的prev变量外,对A没有其它任何影响。在系统中任何进程看到的都是这个样子,所有进程都认为自己在不间断的独立运行。然而,实际上switch_to的执行并不是一瞬间完成的,switch_to执行花了很长很长的时间,但是,在执行完switch_to之后,这段时间被从A的记忆中抹除,所以A并没有觉察到自己被打断过。
         接着,我们再来看这个“神奇”的switch_to。switch_to是从A进程到B进程的过渡,我们可以认为在switch_to这个点上,A进程被切出,B进程被切入。但是,如果把粒度放小到switch_to里面的单个汇编语句,这个界限就不明显了。进入switch_to的宏里面之后,首先 pushfl和pushl ebp肯定仍然属于进程A,之后把esp指向了B的堆栈,严格的说,从此时开始的指令流都属于B进程了。但是,这个时候B进程还没有完全准备好继续运行,因为ebp、硬件上下文等内容还没有切换成B的,剩下的部分宏代码就是完成这些事情。
         另外需要格外强调的是,这部分代码是内核代码,它们跟用户代码不在同一个代码段,所有进程在内核态共用这一段内核代码。这里涉及到的所有堆栈都是内核堆栈,而不涉及用户堆栈。进程切换时需要的页表项的切换不是在这里面做的。

        我们现在再向“上“看,从一个高级语言程序员的角度看,内核态的东西就好比这里的switch_to一样,对高级语言程序员是透明的。高级语言程序员始终认为自己的进程在不间断连续执行,而调度点的语句以及调度点之后的整个过程对该程序是完全没有影响的。

     

    二.实验步骤

    1.使用gdb跟踪schedule函数,单步进入__schedule()函数,如下图所示:

    2.跟踪pick_next_task()函数,如下图所示:

    3.跟踪context_switch()函数,如下图所示:

    4.跟踪switch_to函数,如下图所示:

    三.实验总结

    1.Linux系统中进程调度的时机

      • 中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();

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

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

    2.仔细分析switch_to中的汇编代码,理解进程上下文的切换机制,以及与中断上下文切换的关系

    • 为了控制进程的执行,内核必须有能力挂起正在CPU上执行的进程,并恢复以前挂起的某个进程的执行,这叫做进程切换、任务切换、上下文切换;

    • 挂起正在CPU上执行的进程,与中断时保存现场是不同的,中断前后是在同一个进程上下文中,只是由用户态转向内核态执行;

    • 进程上下文包含了进程执行需要的所有信息

      • 用户地址空间: 包括程序代码,数据,用户堆栈等

      • 控制信息 :进程描述符,内核堆栈等

      • 硬件上下文(注意中断也要保存硬件上下文只是保存的方法不同)

    • schedule()函数选择一个新的进程来运行,并调用context_switch进行上下文的切换,这个宏调用switch_to来进行关键上下文切换

      • next = pick_next_task(rq, prev);//进程调度算法都封装这个函数内部
      • context_switch(rq, prev, next);//进程上下文切换
      • switch_to利用了prev和next两个参数:prev指向当前进程,next指向被调度的进程

    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                      "jmp __switch_to
    "        /* regparm call  */     
    19                      "1:	"                                             
    20                      "popl %%ebp
    	"           /* restore EBP   */     
    21                      "popfl
    "                  /* restore flags */     
    22                                                                         
    23                      /* output parameters */                            
    24                      : [prev_sp] "=m" (prev->thread.sp),                
    25                        [prev_ip] "=m" (prev->thread.ip),                
    26                        "=a" (last),                                     
    27                                                                         
    28                        /* clobbered output registers: */                
    29                        "=b" (ebx), "=c" (ecx), "=d" (edx),              
    30                        "=S" (esi), "=D" (edi)                           
    31                                                                         
    32                        /* input parameters: */                          
    33                      : [next_sp]  "m" (next->thread.sp),                
    34                        [next_ip]  "m" (next->thread.ip),                
    35                                                                         
    36                        /* regparm parameters for __switch_to(): */      
    37                        [prev]     "a" (prev),                           
    38                        [next]     "d" (next));                          
    39 } while (0)

    结语:本次实验让我想起了第一次实验的那个小的进程切换的例子,但是这次实验更加复杂。一路走来,这门课程让我学会了很多,为了更好的理解实验基本上需要查阅很多资料。如今课程的实验结束了让我感慨万千,但愿自己能将课程所学全部融会贯通。

  • 相关阅读:
    清除所有缓存命令
    web前端工程师面试技巧 常见问题解答
    Web前端开发面试技巧
    JavaScript
    JavaScript Cookie
    JavaScript 计时事件
    javascript 弹窗
    JavaScript Window Navigator
    JavaScript Window History
    JavaScript Window Location
  • 原文地址:https://www.cnblogs.com/crowpurple/p/5402364.html
Copyright © 2011-2022 走看看