Sk8er Boi
正己而不求于人,则无怨。

导航

 

原链接:http://www.newsmth.net/bbsanc.php?path=%2Fgroups%2Fcomp.faq%2FKernelTech%2Fprocess_manage%2FM.1169660559.K0

分析进程切换宏 switch_to

feiy@smth, KernelTech

switch_to这个宏用来切换进程,它被schedule调用。有很多资料对这个宏作出了一些
常见的分析,下面的分析基于个人的理解略做阐释。

这个宏在2.4.0内核版本中的定义如下(略).

有一些如下说明:

1.  它是一个宏定义,不是一个函数。

    1) 它不是一个函数

       所以它的“参数”--prev, next, last不是象函数的行参那样在传递的时候
       是作值拷贝的,而是直接的作为变量表达式被代入。

    2) 它只是一个宏定义

       所以它的“参数”--prev, next, last本身是它的调用者的内部变量或常量
       在这里,它的调用者是schedule(),它的这三个参数是shedule()的“局部变
       量”。

    3) “局部变量”被保存在寄存器 ebp 指向的堆栈中,在汇编语言格式下,可以用
       “ %x(ebp) ” 的形式来表示。

       我们知道, gcc利用堆栈来保存函数体内的“局部变量”,即,在汇编代码的
       函数体内部,局部变量采用“ %x(ebp) ” 的形式。它是一种基于ebp的逻辑
       地址的表示方式。由于Linux的逻辑地址等同于线性地址,同时,进程在内核
       态采用相同的页面表,因此,只要进程采用不同的ebp,则某段代码的某些变
       量在不同的进程下,即使具备相同的变量名称,但将指向不同的物理地址;
       反过来,如果某个进程在某种特殊的情况下短暂的使用和另外一个进程相同的
       ebp的值,那么对于同一段代码的局部变量,即使是不同的进程,但是它们依
       然指向同一个物理地址。

       特别指出,这里是ebp而不是esp,尽管ebp常常都是esp进入函数体时的备份。
       esp在一段函数体内总是尽量保持不变,而esp则随着堆栈的操作不断变化。
       esp可用来标识当前正在内核态执行的进程,而ebp可用来标识代码段中的局部
       变量当前正指向那个进程的内核堆栈。

       记住这一点,对于我们区别这段代码中prev, next, last具体对应于那个进程
       的局部变量很重要。

    4)  局部变量的逻辑形式 “ %x(ebp) ” 由编译器决定。

        但是对于一个特定的变量,编译器会分配一个确定的x值;对于一段编译完成
        的代码,这个x是确定的,并不会因为执行这段代码的进程不同而不同。
        换句话说,对于一段代码,如果执行它的代码的进程使用的ebp不相同,那么
        局部变量就会指向不同的物理地址。反过来,不同的进程也可能短暂的使用
        相同的 ebp (因为区别进程的本质区别在于esp指向的内核态堆栈,但是ebp
        的滞后备份,无论是否属于故意,带来了一个结果,就是ebp并不一定总是指
        向由esp所确定的进程的内核堆栈的某个地址),这种情况就发生在了进程切
        换的时候。

    5)  综上分析,如果我们要确定当前正在执行的内核态进程,应该从当前esp指向
        的内核堆栈来判断(current宏的定义就是依照esp来计算),而如果要判断一
        个局部变量位于哪个进程的堆栈中,却要根据当前的ebp来判断。

2. 这个宏会跳转到一个 __switch_to 函数

    1)  这个函数是FASTCALL的,即函数的前三个行参是通过寄存器传递的,即在这
        个函数体内,prev从eax中获得,next从edx中获得。

    2)  在跳转到这个函数之前,有一个压栈操作,就是将函数的返回地址保存进堆
        栈。

        这里的堆栈指的是next的堆栈,后面的分析会详细谈到。

    3)   在调用这个函数之前,原宏定义的汇编代码的输入域指示prev的“值”被复
         制到了eax中,next的“值”被复制到了edx中,因此这里传递给这个函数的
         “值”就是进入这个宏前schedule()函数中局部变量prev和next的“值”。

    4)  区别是值而不是变量表达式,这一点很重要,因为复制过去的“值”可以视
        作一个独立于ebp的“常数”,而变量表达式在会随着ebp而改变。

3.  基于上述2点前提,下面举出一个例子对进程切换的过程做一个简要的分析

    假设A进程调用shedule而切换到B进程,过一段时间后C进程调用schedule而切换
    到进程A。

    为了便于表达,采用ebp_y, esp_y, %x(ebp_y)的形式来表示一些容易混淆的变
    量和指针,比如

             esp == esp_A  表示当前esp指向A进程的内核堆栈

             ebp == ebp_A  表示当前ebp指向A进程的内核堆栈

             %p(ebp_A) 、%n(ebp_A)和%l(ebp_A)

    依次表示进程A的内核堆栈中保存的prev、next和last变量,简称进程A下prev、
    next和last变量,或者

             prev_A, next_A, last_A

    依次表示进程A下的prev、next和last变量

              A <== B        表示B的“值”被复制到变量A



    首先,进程A要切换到进程B:

       step1:汇编代码的输入域将局部变量的值复制到三个寄存器。

                "a" (prev), "d" (next), "b" (prev));

         可表示成

                eax <== %p(ebp_A)  或 eax <== prev_A

                edx <== %n(ebp_A)  或 edx <== next_A

                ebx <== %l(ebp_A)  或 ebx <== last_A

       step2:esi, edi, ebp 压入进程A的内核堆栈

                "pushl %%esi\n\t"
                "pushl %%edi\n\t"
                "pushl %%ebp\n\t"

         这时 esp == esp_A,所以当前运行的进程是A进程,这些寄存器的值被保存
         到了进程A的内核堆栈。

       step 3:  保存当前 esp 的到 A的进程描述符中

                "movl %%esp,%0\n\t"  /* save ESP */

          可表示成

                 prev_A ->thread.esp  <== esp_A

          在这里,%0指的是prev->thread.esp,prev == prev_A。

          目前,esp和ebp都指向A进程的内核堆栈,因此,prev == prev_A。

          当然,shedule开始的prev=current,让它指向A进程自己本身的进程描述
          符。

       step 4:  从next指向的进程描述符中取出esp

                "movl %3,%%esp\n\t" /* restore ESP */

          可表示成

                 esp_B  <== next_A ->thread.esp

          在这里,

              1)  从这个时候起,CPU 当前执行的当前进程已经换成了进程B;

                  因为esp已经更改指向了进程B的内核堆栈

              2)  但这个时候的局部变量依然指向进程A的堆栈。

                  因为到ebp 这个时候没有改变,依然 ebp == ebp_A,所以这个
                  时候依然有next==%n(ebp_A)==next_A, 这保证了这个时候next
                  正确地指向进程B的进程描述符号,不会因为内核堆栈的改变而
                  现错误。如果这个时候ebp被改变成了bp_B,那么
                  next==%n(ebp_B) 显然不再是原先进程A下的那个next所指向的物
                  理地址,而可能是一个垃圾数据。可以参见后面ebx复制prev的分
                  析。

        在这里,我们可以略做一点总结,分析一下进程A的运行环境和状态。

        在进程A的堆栈中,保存了进程A下的esp,edi,ebp。结合下面的step 5,同时
        在进程A的描述符下的thread.eip保存了一个标号为1的地址。


    接着,进程B占有CPU资源而成为当前进程,来继续完成进程切换的后续工作

       step 5:  把标号为1的地址保存到prev_A->thread.eip中。

                "movl $1f,%1\n\t"

          可表示成

                prev_A->thread.eip <== $1f.

          保存这个地址,是为了保证进程A再次被切换到执行状态的时候可以正确的
          找到返回地址。参看下面针对被切换进来的进程B的下一条指令
          "pushl %4\n\t"。

      step 6:  把以前保存在thread.eip中的地址取出来作为返回地址保存到堆栈中
               并跳转到函数 __switch_to 开始执行。

                "pushl %4\n\t"
                "jmp __switch_to\n"

           在这里,%4 指的是 next_A->thread.eip。当然,next_A指向的就是进
           程B。

           所以上述两条指令可以表示为

                 (esp_B++) <== next_A->thread.eip
                 jmp  __switch_to

           结合step 5,很容易产生一个疑惑,那就是,既然进程在以前被切换出来
           而退出执行的时候已经将标号1这个地址保存在了thread.eip中,而后者
           将成为__switch_to的返回地址,既然标号为1的这个地址紧接在
           __switch_to函数的后面,为什么不直接call __switch_to,却要多此一
           举将这个标号先备份到thread.eip然后再从这里面压栈并跳转呢?显然后
           者要比前者多出一步备份操作。

           其实,这里有一个前提,那就是在取出thread.eip的时候,如果它总
           是等于标号1指向的这个地址,那么这样简化是可行的而且是应该的。
           这个前提发生在进程B以前被调度退出过的情况下。如果B进程是一个新创
           建不久的进程而首次被调度进来,那么它以前就没有执行过这个宏开始的
           三个压栈操作,显然它就不应该执行标号1后面的3个出栈操作。这也就是
           为什么,在进程创建的时候,需要如下指定一下thread.eip的原因:

               p->thread.eip = (unsigned long) ret_from_fork

           换句话说,进程B若是首次被切换进来执行,那么它在执行了__switch_to
           后直接跳转到ret_from_fork处执行,而不是回到标号1处。

           当然,到目前为止, ebp == ebp_A 没有改变,所以在这里prev==prev_A
           这也保证了prev依然正确的指向了切换前的进程A。同样,也保证了next
           是从进程A的堆栈中取出,即next_A,从而保证了next是正确的指向了进
           程B。

       step 7:  在函数体__switch_to内的执行

           再次,函数的参数是通过值拷贝来传递,和普通函数的区别在于这里拷贝
           到的目标地址是寄存器而不是堆栈。所以,在函数体内如果prev和next没
           有被修改,即使ebp 发生了改变,prev依然始终指向进程A的进程描述符
           而next指向进程B的进程描述符。而由于是值复制,即使在函数体内被修
           改了,从函数返回后在函数__switch_to 外面的prev和next是不受影响的
           除非ebp被修改了而让局部变量指向了其他的物理地址。参见后面分析。

       step 8: 进程B在函数__switch_to中返回

          从函数__switch_to中返回后程序会跳到哪里执行,这取决于进程是否属
          于首次被调度。如果是首次被调度,则跳到ret_from_fork处执行,如果
          不是,则跳到标号1处执行。下面继续分析标号1处的代码。

       step 9: 进程B在在标号1处继续执行

           "popl %%ebp\n\t"     \
           "popl %%edi\n\t"     \
           "popl %%esi\n\t"

          进程在标号1处继续执行,表明它不是首次被调度,换句话说,以前因调度
          而放弃CPU资源的时候,执行了宏switch_to开始的一些压栈代码,即把它
          自己的esi, edi, ebp压入了自己的内核堆栈(参考上面的进程A的调度)

          所以,"popl %%ebp\n\t" 的结果是 ebp==ebp_B。从这个时候起,变量
          prev == %p(ebp_B),next == %n(ebp_B),它们和prev_A以及next_B指向
          不同的物理地址。由于进程B是被切换进来的进程,它并没有执行schedule
          中位于宏定义之前的代码,因此prev_B和next_B是没有被初始化的,所以
          它们是一些无意义的垃圾值,它们不再一定分别指向进程A或者B。

       step 10:  汇编的输出域"=b" (last)会把ebx的值复制到last。

          在调用这个宏定义的时候,last用prev替代。所以其实是把ebx保存到了
          prev中,而这个时候ebp==ebp_B,prev==prev_B==%p(ebp_B), 所以,上述
          这个输出域等效为

                  %p(ebp_B) <== ebx
          或者
                  prev_B  <== ebx。

          我们注意到,在开始执行这个宏的时候,我们把prev_A保存到了ebx中,
          换句话说,ebx指向了进程A的进程描述符。所以这条输出域的含义
          就是把就是让prev_B指向了进程A。

          这个时候,局部变量prev的物理地址和进入宏前的prev的物理地址是不相
          同的,在调用宏之前, prev==prev_A==%p(ebp_A),而在调用这个宏之后
          prev==prev_B==%p(ebp_B)。

          这个输出域的作用,就是把进程A的进程描述符的地址复制到了prev_B指向
          的物理地址中。这样保证了宏调用前后,局部变量prev的值不会因为
          prev的物理地址发生改变而变化,而依然指向进程A的进程描述符。

          这里有人或许会想,那我在调用这个宏之前,把prev备份到另外一个中间
          的局部变量pre_backup中如何?或者改宏定义为函数调用从而使用值拷贝
          的传递方式如何?实际上这两种方法都是不可以保证prev或prev的值在调
          用宏前后不发生改变,因为这种改变是由于该变量的物理地址随着ebp的
          更改而发生了改变。但寄存器不会随ebp而改变,所以在这里采用了ebx备
          份的方式。

    到这里为止,进程B已经是当前进程而占有CPU资源并继续占有,而进程A进入非执
    行状态。那么进程A的再次执行什么时候开始,从哪里开始?

    例如在另外某个进程C的运行schedule的时候,进程A可能成为最good的进程而被
    再次调度进来进入执行这个时候,进程A就占据了前面分析中进程B的位置,而进
    程C就占据了上面分析中A的位置。所以,进程A从 "movl %3,%%esp\n\t"处开始
    把CPU资源交给进程B,而再次被C调度进来执行的时候,也是从这个地址开始成为
    当前进程占有CPU资源。

    但是,进程A的内核堆栈中的临时变量依然会被切换进来的进程B所访问,直到标
    号1处的 "popl %%ebp\n\t"执行结束后将ebp更改成指向了自己的堆栈。但是期间
    进程B在函数体内新开辟的变量,比如在__switch_to函数体内,这些变量依然会
    在进程B的内核空间,这是由于进入函数体后会临时更新ebp的,而在退出函数体时
    又会恢复ebp的缘故。

posted on 2013-07-03 16:49  Sk8er-boi  阅读(200)  评论(0)    收藏  举报