《深入理解Linux内核3rd》学习笔记——进程切换(上):相关知识

   进程切换(process switch),作为抢占式多任务OS中重要的一个功能,其实质就是OS内核挂起正在运行的进程A,然后将先前被挂起的另一个进程B恢复运行。

 

硬件上下文

  每个进程都有自己的地址空间,但是所有进程在物理上共享着CPU的寄存器,因此,当恢复一个进程执行前,OS内核必须要将挂起该进程时寄存器的值装入CPU寄存器。进程恢复执行前必须装入寄存器的一组数据就叫做“硬件上下文”(hardware context),它是进程执行上下文的子集,后者是进程执行时需要的所有信息(如地址空间中的数据等)。

  Linux中,TSS保存着部分的进程的硬件上下文(如ss、esp等寄存器的值),剩余部分保存在内核堆栈中(如eax、ebx等通用数据寄存器的值)。

  进程切换只发生在内核态,在进程切换之前,用户态使用的寄存器内容都已保存在内核堆栈上,如ss、esp等。

 

任务状态段(TSS)

  80x86体系结构中有个特殊的段——TSS,用来存放硬件上下文。Linux为每个CPU分配一个TSS。这样,当一个CPU从用户态切换到内核态时,就从TSS中得到内核态的堆栈地址,如果用户态程序试图用in或out指令访问I/O设备时,CPU就可以访问在TSS中的I/O许可位图(I/O Permission Bitmap)来检查该操作是否合法。

  tss_struct结构描述TSS的格式。系统中有一个全局数组——init_tss,里面保存着每个不同CPU的TSS(n个CPU就有n个TSS)。由此可见,TSS表示了CPU上当前进程的信息,没有必要为每个进程都分配TSS。

  Linux创建的TSSD(任务状态段描述符)存放在GDT中,GDT的基地址保存在每个CPU的gdtr寄存器里。每个CPU的tr寄存器里有相应的TSS的TSSD的选择子,这可以用来在GDT中定位TSSD,从而得到TSS。CPU中有两个不可编程的寄存器,存放TSSD的Base字段和Limit字段,这样CPU可以快速地对TSS寻址,而不需经过GDT。

  因为Linux为每个CPU分配TSS,而不是每个进程分配TSS,因此,被替换的进程的硬件上下文必须保存在别处,不能存在TSS中。每个进程描述符中有一个字段thread——一个thread_struct类型的字段,使用它可以保存部分硬件上下文。该结构中包含了大部分的CPU寄存器(如esp、eip等),但不包含eax、ebx之类的通用寄存器,因为它们保存在进程内核堆栈中。

 

执行进程切换

  进程切换发生在schedule()函数中。进程切换分为两个步骤:

  1. 切换页全局目录(Page Global Directory)来加载一个新的地址空间,实际上就是加载新进程的cr3寄存器值。
  2. 切换内核堆栈和硬件上下文,这些包含了内核执行一个新进程的所有信息,包含了CPU寄存器。

  现在假设prev表示即将被替换的进程的描述符,next表示即将执行的进程的描述符。其实,prev和next都是schedule()函数的局部变量。

 

switch_to宏

  这里讨论进程切换的第2个步骤,该步骤通过switch_to宏来实现。

  switch_to宏它有三个参数:prev、next、last。prev和next不需要解释,last参数是干什么的呢?实际上,任何进程切换涉及3个进程,不仅仅是2个。

  假设内核决定将进程A挂起,执行进程B,那么在schedule()函数中,prev就是进程A的描述符地址,next就是进程B的描述符地址,一旦switch_to挂起A,那么进程A就冻结了。后来,当内核想重新执行进程A,它必须通过switch_to宏来挂起进程C(通常不是进程B),此时prev代表C、next代表A。当A恢复执行,它得到它原来的内核堆栈,在这个原来的内核堆栈里,prev代表A,next代表B。此时,代表进程A的内核代码失去了对进程C的引用,就找不到进程C了。事实证明,这个引用对于完成进程切换是有用的。

  switch_to的last参数是一个输出参数,表示宏把进程C的描述符地址写在内存的某个地址(这是在A恢复执行后完成的)。在进程切换之前,switch_to把prev的值写入eax。在A恢复执行后,此时还是在switch_to宏代码中,A得到它原来的内核堆栈,prev是A的描述符地址,注意,因为CPU内eax寄存器的值不会因为切换而变化,因此,eax里存的是进程C的描述符地址,switch_to会将eax的值写入到last中,原来last指向进程A的prev就被C的描述符地址覆盖了。

  关于switch_to宏的分析,请见下一篇。

 

__switch_to函数

  switch_to宏里有一句“jmp __switch_to”,即跳转到__switch_to函数开始执行。该函数完成进程切换第2步骤的大部分工作。该函数是FASTCALL调用方式(利用关键字__attribute__(regparm(3))声明),因此参数用通用数据寄存器传递——eax传递prev_p、edx传递next_p。

  关于__switch_to函数的分析,请见下一篇。

 

保存和加载FPU、MMX和XMM寄存器

  从Intel 80486DX开始,FPU(算术浮点单元)被集成到了CPU中,浮点算术功能用ESCAPE指令来执行,操纵CPU中的浮点寄存器集。显然,当一个进程正在使用ESCAPE指令,那么浮点寄存器的内容就属于它的硬件上下文。

  为了加速多媒体程序的执行,Intel在微处理器中引入了新的指令集——MMX,MMX指令也作用于FPU的浮点寄存器。这样,MMX就不能和FPU指令混用,但是OS内核就可以忽略新的MMX指令集,因为保存浮点寄存器的功能代码也能够应用于MMX的状态。

  MMX使用SIMD(单指令多数据)流水线,Pentium III增强了这种SIMD能力,引入SSE(Streaming SIMD Extensions)扩展。该功能增强了8个128位寄存器(XMM寄存器)的功能,这些寄存器不和FPU/MMX寄存器重叠,因此能够与FPU/MMX指令混用。

  Pentium IV还引入了SSE2扩展,支持高精度浮点值,SSE2和SSE使用同一个XMM寄存器组。

  80x86微处理器不在TSS中保存FPU、MMX和XMM寄存器的值,不过还是提供了一些支持,能够在需要时保存它们。cr0寄存器有一个TS(Task-Switching)标志位,每当执行硬件上下文切换时,TS置位,每当TS被置位后进程执行ESCAPE、MMX、SSE或SSE2指令,控制器就产生一个“Device not available”异常。这样,TS标志位就能够让OS内核只有在真正需要时才保存或恢复FPU、MMX和XMM寄存器。

  假设进程A使用了数学协处理器,那么当进程A被切换出去的时候,内核设置TS并将浮点寄存器的内容保存到进程A的TSS中(原著这么写,但是应该是保存到进程A描述符的一个字段中,TSS是与CPU关联的,进程没有TSS)。

  如果新进程B不使用数学协处理器,那么内核就不需要恢复浮点寄存器的内容,但是,一旦进程B执行FPU、MMX等指令,CPU就产生一个“Device not available”异常,相应的异常处理程序就会用保存在进程B中的相关值来恢复浮点寄存器。

  处理FPU、MMX和XMM寄存器的数据结构存放在进程描述符的thread字段的i387子字段中(即thread.i387),由i387_union联合体描述,其格式如下:

union i387_union {
        
struct i387_fsave_struct    fsave; /* 保存FPU、MMX寄存器的内容 */
        
struct i387_fxsave_struct   fxsave;/* 保存SSE和SSE2寄存器内容 */
        
struct i387_soft_struct     soft;  /* 由无数学协处理器的老式CPU模型使用 */
    };

  此外,进程描述符中还包含了两个附加的标志:

  • thread_info结构中status字段的TS_USEDFPU标志,表示进程当前执行过程中是否使用过FPU、MMX和XMM寄存器。
  • task_struct结构的flags字段的PF_USED_MATH标志,表示thread.i387的内容是否有意义。

  保存和加载FPU、MMX和XMM寄存器主要用到__unlazy_fpu宏,该宏在__switch_to函数中使用,下一篇会对其进行分析。

 

内核态使用FPU、MMX和XMM寄存器

  OS内核也可以使用FPU、MMX和XMM寄存器,当然,这么做的时候应该避免干扰用户态进程。因此,Linux使用如下方法来解决:

  • 在内核使用协处理器之前,如果用户态进程使用了FPU(TS_USEDFPU标志为1),内核就要调用kernel_fpu_begin()函数,该函数里又调用save_init_fpu()来保存寄存器内容,然后重新设置cr0寄存器的TS标志。
  • 使用完协处理器之后,内核调用kernel_fpu_end宏设置cr0寄存器的TS标志。
  • 当用户态进程恢复执行时,math_state_restore()函数将恢复FPU、MMX和XMM寄存器的内容。

  需要注意的是,如果当前用户态进程有在用数学协处理器时,kernel_fpu_begin()函数的执行时间比较长,甚至无法通过FPU、MMX或XMM达到加速的目的。因此,内核只在有限的场合使用FPU、MMX或XMM指令,比如移动或清除大内存区字段、计算校验和等。

 

posted on 2010-05-25 09:34  小虎无忧  阅读(6326)  评论(0编辑  收藏  举报

导航