5.4.2    进程切换

      前面所介绍的schedule()中调用了switch_to,这个宏实现了进程之间的真正切换,其代码存放于include/ i386/system.h

1   #define switch_to(prev,next,last) do {                                  \

2         asm volatile("pushl %%esi\n\t"                                  \

3                      "pushl %%edi\n\t"                                  \

4                      "pushl %%ebp\n\t"                                  \

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

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

7                      "movl $1f,%1\n\t"          /* save EIP */          \

8                      "pushl %4\n\t"             /* restore EIP */       \

9                      "jmp __switch_to\n"                                \

10                      "1:\t"                                             \

11                     "popl %%ebp\n\t"                                   \

12                     "popl %%edi\n\t"                                   \

13                     "popl %%esi\n\t"                                   \

14                      :"=m" (prev->thread.esp),"=m" (prev->thread.eip),  \

15                       "=b" (last)                                       \

16                      :"m" (next->thread.esp),"m" (next->thread.eip),    \

17                       "a" (prev), "d" (next),                           \

18                       "b" (prev));                                      \

19 } while (0)

switch_to宏是用嵌入式汇编写成,比较难理解,为描述方便起见,我们给代码编了行号,在此我们给出具体的解释:

·   thread的类型为前面介绍的thread_struct结构。

·   输出参数有三个,表示这段代码执行后有三项数据会有变化,它们与变量及寄存器的对应关系如下:

0%与prev->thread.esp对应,1%与prev->thread.eip对应,这两个参数都存放在内存,而2%与ebx寄存器对应,同时说明last参数存放在ebx寄存器中。

·   输入参数有五个,其对应关系如下:

3%与next->thread.esp对应,4%与next->thread.eip对应,这两个参数都存放在内存,而5%,6%和7%分别与eax,edxebx相对应,同时说明prev,next以及prev三个参数分别放在这三个寄存器中。表5.1列出了这几种对应关系:

5.1

   参数类型

参数名

内存变量

寄存器

函数参数

输出参数

0

prev->thread.esp

 

 

1

prev->thread.eip

 

 

2

 

ebx

last

输入参数

3

next->thread.esp

 

 

4

next->thread.eip

 

 

5

 

eax

prev

6

 

edx

next

7

 

ebx

prev

 

·   24行就是在当前进程prev的内核中保存esi,ediebp寄存器的内容。

·   5行将prev的内核堆栈指针ebp存入prev->thread.esp中。

·   6行把将要运行进程next的内核栈指针next->thread.esp置入esp寄存器中。从现在开始,内核对next的内核进行操作,因此,这条指令执行从prevnext真正的上下文切换,因为进程描述符的地址与其内核的地址紧紧地联系在一起(参见第四章),因此,改变内核就意味着改变当前进程。如果此处引用current的话,那就已经指向nexttask_struct结构了。从这个意义上说,进程的切换在这一行指令执行完以后就已经完成。但是,构成一个进程的另一个要素是程序的执行,这方面的切换尚未完成。

·   7行将标号“1”所在的地址,也就是第一条popl指令(第11行)所在的地址保存在prev->thread.eip中,这个地址就是prev下一次被调度运行而切入时的“返回”地址。

·   8行将next->thread.eip压入next的内核。那么,next->thread.eip究竟指向那个地址?实际上,它就是 next上一次被调离时通过第7行保存的地址,也就是第11popl指令的地址。因为,每个进程被调离时都要执行这里的第7行,这就决定了每个进程(除了新创建的进程)在受到调度而恢复执行时都从这里的第11行开始。

·   9行通过jump指令(而不是 call指令)转入一个函数__switch_to()。这个函数的具体实现将在下面介绍。当CPU执行到__switch_to()函数的ret指令时,最后进入堆栈的next->thread.eip就变成了返回地址,这就是标号“1”的地址。

·   1113行恢复next上次被调离时推进堆栈的内容。从现在开始,next进程就成为当前进程而真正开始执行。

 

下面我们来讨论__switch_to()函数。

在调用__switch_to()函数之前,对其定义了fastcall :

 

extern void FASTCALL(__switch_to(struct task_struct *prev, struct task_struct *next));

 

     fastcall对函数的调用不同于一般函数的调用,因为__switch_to()从寄存器(如表5.1)取参数,而不像一般函数那样从堆栈取参数,也就是说,通过寄存器eaxedxprevnext 参数传递给__switch_to()函数。

 

void __switch_to(struct task_struct *prev_p, struct task_struct *next_p)

{

        struct thread_struct *prev = &prev_p->thread,

                                *next = &next_p->thread;

        struct tss_struct *tss = init_tss + smp_processor_id();

 

      unlazy_fpu(prev_p);* 如果数学处理器工作,则保存其寄存器的值*

 

  /* TSS中的内核级(0级)堆栈指针换成next->esp0,这就是next 进程在内核

     的指针

  

      tss->esp0 = next->esp0;

 

   * 保存fsgs,但无需保存esds,因为当处于内核时,内核段

总是保持不变*

 

       asm volatile("movl %%fs,%0":"=m" (*(int *)&prev->fs));

       asm volatile("movl %%gs,%0":"=m" (*(int *)&prev->gs));

 

     *恢复next进程的fsgs *

 

         loadsegment(fs, next->fs);

        loadsegment(gs, next->gs);

 

* 如果next挂起时使用了调试寄存器,则装载07个寄存器中的6个寄存器,其中第45个寄存器没有使用 *

 

        if (next->debugreg[7]){

                loaddebug(next, 0);

                loaddebug(next, 1);

                loaddebug(next, 2);

                loaddebug(next, 3);

                 /* no 4 and 5 */

                loaddebug(next, 6);

                loaddebug(next, 7);

        }

 

         if (prev->ioperm || next->ioperm) {

                if (next->ioperm) {

              

*next进程的I/O操作权限位图拷贝到TSS *

                       memcpy(tss->io_bitmap, next->io_bitmap,

IO_BITMAP_SIZE*sizeof(unsigned long));

 

/* io_bitmaptss中的偏移量赋给tss->bitmap *

                         tss->bitmap = IO_BITMAP_OFFSET;

                 } else

                  

*如果一个进程要使用I/O指令,但是,若位图的偏移量超出TSS的范围,

         就会产生一个可控制的SIGSEGV信号。第一次对sys_ioperm()的调用会

         建立起适当的位图  *

                        

                        tss->bitmap = INVALID_IO_BITMAP_OFFSET;

         }

}

 

 

   从上面的描述我们看到,尽管Intel本身为操作系统中的进程(任务)切换提供了硬件支持,但是Linux内核的设计者并没有完全采用这种思想,而是用软件实现了进程切换,而且,软件实现比硬件实现的效率更高,灵活性更大。