3.4 进程切换
目录1.进程切换2.硬件上下文3.任务状态段3.1 thread字段1.进程切换为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换(process switch)、任务切换(task switch)或上下文切换(context switch)。下面几节描述在 Linux 中进行进程切换的主要内容。2.硬件上下文一、尽管每个进程可以拥
1.进程切换
- 为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换(process switch)、任务切换(task switch)或上下文切换(context switch)。下面几节描述在 Linux 中进行进程切换的主要内容。
2.硬件上下文
- 一、尽管每个进程可以拥有属于自己的地址空间,但所有进程必须共享CPU寄存器。因此,在恢复一个进程的执行之前,内核必须确保每个寄存器装入了挂起进程时的值。
- 二、进程恢复执行前必须装入寄存器的一组数据称为硬件上下文(hardware context)。硬件上下文是进程可执行上下文的一个子集,因为可执行上下文包含进程执行时需要的所有信息。在Linux中,进程硬件上下文的一部分存放在TSS段,而剩余部分存放在内核态堆栈中。
- 三、在下面的描述中,我们假定用prev局部变量表示切换出的进程的描述符,next表示切换进的进程的描述符。因此,我们把进程切换定义为这样的行为:
保存prev硬件上下文,用next 硬件上下文代替prev。因为进程切换经常发生,因此减少保存和装入硬件上下文所花费的时间是非常重要的。 - 四、早期的Linux版本利用80x86体系结构所提供的硬件支持,并通过far jmp指令(far jmp指今既修改cs寄存器,也修改eip寄存器,而简单的 jmp指今只修改eip寄存器。)跳到next进程TSS描述符的选择符来执行进程切换。当执行这条指令时,CPU通过自动保存原来的硬件上下文,装入新的硬件上下文来执行硬件上下文切换。但基于以下原因,Linux2.6使用软件执行进程切换:
- 1. 通过一组mov指令逐步执行切换,这样能较好地控制所装入数据的合法性。尤其是,这使检查ds和es段寄存器的值成为可能,这些值有可能被恶意用户伪造。当用单独的far imp 指令时,不可能进行这类检查。
- 2.旧方法和新方法所需时间大致相同。然而,尽管当前的切换代码还有改进的余地却不能对硬件上下文切换进行优化。进程切换只发生在内核态。在执行进程切换之前,用户态进程使用的所有寄存器内容都已保存在内核态堆栈上(参见中断和异常),这也包括ss和esp这对寄存器的内容(存储用户态堆栈指针的地址)。
3.任务状态段
-
一、80x86 体系结构包括了一个特殊的段类型,叫任务状态段(Task State Segment , TSS)来存放硬件上下文。尽管
Linux并不使用硬件上下文切换,但是强制它为系统中每个不同的CPU创建一个TSS。这样做的两个主要理由为: -
1.当80x86的一个CPU从用户态切换到内核态时,它就从TSS中获取内核态堆栈的地址”(参见“中断和异常的硬件处理“和"通过sysenter 指令发送系统调用”一节)。
-
2.当用户态进程试图通过in或out指令访问一个I/0端口时。CPU需要访问存放在TSS中的I/O许可权位图(Permission Bitmap)以检查该进程是否有访问端口的权力。更确切地说,当进程在用户态下执行in或out 指令时,控制单元执行下列操作:
- a.它检查eflags寄存器中的2位IOPL字段。如果该字段值为3,控制单元就执行 I/O 指令。否则,执行下一个检查。
- b.访问tr寄存器以确定当前的TSS和相应的1/O许可权位图。
- c.检查I/0 指令中指定的 I/O端口在 I/0 许可权位图中对应的位。如果该位清0.这条I/O指令就执行,否则控制单元产生一个“General protection”异常。
-
二、tss_struct结构描述TSS的格式。正如之前所提到的,init_tss数组为系统上每个不同的CPU存放一个TSS。在每次进程切换时,内核都更新TSS的某些字段以便相应的 CPU控制单元可以安全地检索到它需要的信息。因此,TSS 反映了CPU上的当前进程的特权级,但不必为没有在运行的进程保留TSS。
-
三、每个TSS有它自己8字节的任务状态段描述符(Task State Segment Descriptor,TSSD)。这个描述符包括指向TSS起始地址的32位Base字段, 20位Limit字段。TSSD的S标志位被清0,以表示相应的TSS是系统段的事实(参见“段描述符”)。
-
四、Type字段置为11或9以表示这个段实际上是一个TSS 在Intel的原始设计中,系统中的每个进程都应当指向自己的TSS:Type字段的第二个有效位叫做Busy位:如果进程正由CPU执行,则该位置1,否则置0。在Linux的设计中,每个CPU只有一个TSS.因此,Busy位总置为1。
-
五、由Linux创建的TSSD存放在全局描述符表(GDT)中,GDT的基地址存放在每个CPU的gdtr寄存器中。每个CPU的tr寄存器包含相应TSS的TSSD选择符,也包含了两个隐藏的非编程字段:TSSD的Base字段和Limit字段。这样,处理器就能直接对TSS寻址而不用从GDT中检索 TSS的地址。
3.1 thread字段
- 一、在每次进程切换时,被替换进程的硬件上下文必须保存在别处。不能像Intel原始设计那样把它保存在TSS中,因为Linux 为每个处理器而不是为每个进程使用TSS。
- 二、因此,每个进程描述符包含一个类型为thread_struct的thread字段,只要进程被切换出去,内核就把其硬件上下文保存在这个结构中。随后我们会看到,这个数据结构包含的字段涉及大部分CPU寄存器,但不包括诸如eax、ebx等等这些通用寄存器,它们的值保留在内核堆栈中。
4.执行进程切换
-
一、进程切换可能只发生在精心定义的点:schedule()函数(以后将专门讨论shcedule函数)。这里,我们仅关注内核如何执行一个进程切换。
-
二、从本质上说,每个进程切换由两步组成:
- 1.切换页全局目录以安装一个新的地址空间。
- 2.切换内核态堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息,包含CPU寄存器。
-
三、我们又一次假定prev指向被替换进程的描述符,而next 指向被激活进程的描述符。prev和next是schedule()函数的局部变量。
4.1 switch_to(prev, next, last)宏
- 一、进程切换的第二步由switch_to宏执行。它是内核中与硬件关系最密切的例程之一
(跟架构有关每种架构的实现不一样),要理解它到底做了些什么我们必须下些功夫。 - 二、首先,该宏有三个参数,它们是prev,next和last。你可能很容易猜到prev和next的作用:它们
仅是局部变量prev和next 的占位符,即它们是输入参数,分别表示被替换进程和新进程描述符的地址在内存中的位置。 - 三、那第三个参数last呢?
在任何进程切换中涉及到三个进程而不是两个。假设内核决定暂停进程A而激活进程B。在schedule()函数中,prev指向A的描述符而next指向B的描述符。switch_to宏一但使A暂停,A的执行流就冻结。 - 四、switch_to宏的最后一个参数是输出参数,它表示宏把
进程C的描述符地址写在内存的什么位置了(当然,这是在A恢复执行之后完成的)。在进程切换之前,宏把第一个输入参数prev(即在A的内核堆栈中分配的prev局部变量)表示的变量的内容存入CPU的eax寄存器。在完成进程切换,A已经恢复执行时,宏把CPU的eax寄存器的内容写入由第三个输出参数——last所指示的A在内存中的位置。因为CPU寄存器不会在切换点发生变化,所以C的描述符地址也存在内存的这个位置。在schedule()执行过程中,参数last指向A的局部变量prev,所以prev被C的地址覆盖。 - 五、下图显示了进程A,B,C内核堆栈的内容以及eax寄存器的内容。必须注意的是:图中显示的是在被eax寄存器的内容覆盖以前的prev局部变量的值。

4.2 switch_to代码分析
- 一、switch_to宏是内核进程切换的核心函数,其采用内联汇编语言编码,所以可读性比较差。想要了解其中的过程,最后是自己画出来进程间切换时堆栈的变化情况。
- 二、15_02_一个简单的操作系统内核源代码。这是之前分析进程间切换时写的switch_to的堆栈图解过程(my_schedule函数即使简化版的switch_to)。
- 三、总结一下switch_to的作用:
- 1.将当前内核栈底指针esp保存到prev进程的栈底指针prev->thread.esp中—>即
movl %esp, prev->thread.esp,保存进程上文。 - 2.将next进程的栈底指针next->thread.esp赋值给当前内核栈底指针esp——>即
movl next->thread.esp, %esp,完成进程间堆栈切换。 - 3.将标记为1的地址赋值给prev进程的eip——>即
movl $1f, prev->thread.eip,为恢复进程下文并执行做准备。 - 4.将next进程的eip压栈——>即
pushl next->thread.eip,为执行next进程做准备。 - 5.jmp __switch_to该函数忽略细节可简化为汇编指令
ret,——>即出栈并将出栈值(此时指next->thread.eip)赋给eip。如果next进程第一次执行而以前从未被挂起(next->thread.eip未被重新赋值,next->thread.eip第一次赋值发生在copy_thread()中),则__switch_to就找到ret_from_fork(),否则next进程肯定已经走了步骤3,则开始运行next进程。
- 1.将当前内核栈底指针esp保存到prev进程的栈底指针prev->thread.esp中—>即
更多推荐

所有评论(0)