深入理解Linux内核(第三版)- 进程切换

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换(process switch)、任务切换(task switch)或上下文切换(context switch)。

硬件上下文

尽管每个进程可以拥有自己的地址空间,但所有进程必须共享CPU寄存器。因此,在恢复一个进程的执行之前,内核必须确保每个寄存器装入了挂起进程时的值。

进程恢复执行前必须装入寄存器的一组数据称为硬件上下文(hardware context)。硬件上下文是进程可执行上下文的一个子集,因为可执行上下文包含进程执行时需要的所有信息。在Linux中,进程硬件上下文的一部分存放在TSS段,而剩余部分存放在内核态堆栈中。

在下面的描述中,我们假定用prev局部变量表示切换出的进程的描述符,next表示切换进的进程的描述符。因此,我们把进程切换定义为这样的行为:保存prev硬件上下文,用next硬件上下文代替prev。

进程切换只发生在内核态。在执行进程切换之前,用户态进程使用的所有寄存器内容都已经保存在内核态堆栈上,这也包括ss和esp这对寄存器的内容(存储用户态堆栈指针的地址)。

任务状态段

80x86体系结构包括了一个特殊的段类型,叫任务状态段(Task State Segment,TSS)来存放硬件上下文。尽管Linux并不使用硬件上下文切换,但是强制它为系统中每个不同的CPU创建一个TSS。这样做出于两个目的:

  • 当80x86的一个CPU从用户态切换到内核态时,它就从TSS中获取内核态堆栈的地址。
  • 当用户态试图通过in或out指令访问一个I/O端口时,CPU需要访问存放在TSS中的I/O许可权位图(Permission Bitmap)以检查该进程是否有访问端口的权力。

更确切的说,当进程在用户态下执行in或out指令时,控制单元执行下列操作:

  1. 它检查eflags寄存器中的2位IOPL字段。如果该字段值为3,控制单元就执行I/O指令。否则,执行下一个检查。
  2. 访问tr寄存器以确定当前的TSS和相应的I/O许可权位图。
  3. 检查I/O指令中指定端口在I/O许可权位图中对应的位。如果该位清零,这条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标志位被清零,以表示相应的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的地址。

thread字段 

在每次进程切换时,被替换进程的硬件上下文必须保存在别处。不能向Intel原始设计那样把它保存在TSS中,因为Linux为每个处理器而不是为每个进程使用TSS。

因此,每个进程描述符包含一个类型为thread_struct的thread字段,只要进程被切换出去,内核就把其硬件上下文保存在这个结构中。这个数据结构包含的字段涉及大部分CPU寄存器,但不包括诸如eax、ebx等等这些通用寄存器,它们的值保存在内核堆栈中。

执行进程切换

进程切换可能只发生在精心定义的点:schedule()函数。这里,我们仅关注内核如何执行一个进程切换。

从本质上说,进程切换由两步组成:

1、切换页全局目录以安装一个新的地址空间

2、切换内核态堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息,包含CPU寄存器。

switch_to 宏

进程切换的第二步由switch_to宏执行。它是内核中与硬件关系最密切的例程之一。

首先,该宏有三个参数,它们是prev,next和last。我们很容易猜到prev和next的作用:它们仅是局部变量prev和next的占位符,即它们是输入参数,分别表示被替换进程和新进程描述符的地址在内存中的位置。

那第三个参数last呢?在任何进程切换中,涉及到三个进程而不是两个。假设内核决定暂停进程A而激活进程B。在schedule( )函数中,prev指向A的描述符而next指向B的描述符。switch_to宏一但使A暂停,A的执行流就冻结。

随后,当内核想再次此激活A,就必须暂停另一个进程C(这通常不同于B),于是就要用prev指向C而next指向A来执行另一个switch_to宏。当A恢复它的执行流时,就会找到它原来的内核栈,于是prev局部变量还是指向A的描述符而next 指向B的描述符。此时,代表进程A执行的内核就失去了对C的任何引用。但是,事实表明这个引用对于完成进程切换是很有用的。

switch_to宏的最后一个参数是输出参数,它表示宏把进程C的描述符地址写在内存的什么位置了(当然,这是在A恢复执行之后完成的)。在进程切换之前,宏把第一个输入参数prev(即在A的内核堆栈中分配的prev局部变量)表示的变量的内容存入CPU的eax寄存器。在完成进程切换,A已经恢复执行时,宏把CPU的eax寄存器的内容写入由第三个输出参数——last所指示的A在内存中的位置。因为CPU寄存器不会在切换点发生变化,所以C的描述符地址也存在内存的这个位置。在schedule()执行过程中,参数last 指向A的局部变量prev,所以prev被C的地址覆盖。

下图1显示了进程A,B,C内核堆栈的内容以及eax寄存器的内容。必须注意的是:图中显示的是在被eax寄存器的内容覆盖以前的prev局部变量的值。

 图1:通过一个进程切换保留对进程C的引用

由于switch_to宏采用扩展的内联汇编语言编码,所以可读性比较差:实际上这段代码通过特殊位置记数法使用寄存器,而实际使用的通用寄存器由编译器自由选择。我们将采用标准汇编语言而不是麻烦的内联汇编语言来描述switch_to宏在80x86微处理器上所完成的典型工作。

1、在eax和edx寄存器中分别保存prev和next的值:

movl prev, %eax
movl next, %edx

2、把eflags和ebp寄存器的内容保存在prev内核栈中。必须保存它们的原因是编译器认为在switch_to结束之前它们的值应当保持不变。

pushfl
pushl %ebp

3、把esp的内容保存到prev->thread.esp中以使该字段指向prev内核栈的栈顶:

movl %esp, 484(%eax)

484(%eax)操作数表示内存单元的地址为eax内容加上484。

4、把next->thread.esp装入esp。此时,内核开始在next的内核栈上操作,因此这条指令实际上完成了从prev到next的切换。由于进程描述符的地址和内核栈的地址紧挨着,所以改变内核栈意味着改变当前进程。

movl 484(%edx), %esp

5、把标记为1的地址存入prev->thread.eip。当被替换的进程重新恢复执行时,进程执行被标记为1的那条指令:

movl $1f, 480 (%eax)

6、宏把next->thread.eip的值(绝大多数情况下是一个被标记为1的地址)压入next 的内核栈:

pushl 480(%edx)

7、跳到__switch_to () C函数(见下面):

jmp_ _switch_to

8、这里被进程B替换的进程A再次获得CPU:它执行一些保存eflags和ebp寄存器内容的指令,这两条指令的第一条指令被标记为1。

1:
    popl %ebp
    popfl

注意这些pop指令是怎样引用prev进程的内核栈的。当进程调度程序选择了prev作为新进程在CPU上运行时,将执行这些指令。于是,以prev作为第二个参数调用switch_to。因此,esp寄存器指向prev的内核栈。

9、拷贝eax寄存器(上面步骤1中被装载)的内容到switch_to宏的第三个参数last标识的内存区域中:

movl %eax, last

正如先前讨论的,eax寄存器指向刚被替换的进程的描述符(当前执行的schedule()函数重新使用了prev局部变量,于是汇编语言指令就是: movl %eax , prev)


参考文献:《深入理解Linux内核(第三版)》  中国电力出版社

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
深入理解Linux内核第三版)》是一本由Daniel P. Bovet和Marco Cesati合著的著作,书中详细介绍了Linux操作系统的核心部分。该书是学习Linux内核的经典教材之一,对于想要深入了解Linux内核工作原理的读者来说是一本极具价值的参考书籍。 在《深入理解Linux内核第三版)》中,作者通过详细的分析和解释,帮助读者逐步理解Linux内核的关键概念和原理。书中首先介绍了Linux内核的起源和发展历程,为读者提供了一个全面的背景了解。接着,书中深入分析了Linux内核中的进程管理、内存管理、文件系统、设备驱动等关键模块。作者通过展示源代码片段、数据结构图和流程图,帮助读者理解Linux内核的实现细节。 此外,《深入理解Linux内核第三版)》还重点讲述了Linux内核的性能优化、故障排除和安全性等方面的内容。读者可以通过学习相关章节了解如何通过对内核的配置和调整来提高系统性能,以及如何分析和解决系统故障。此外,书中还涵盖了Linux内核的安全性原理和机制,帮助读者了解如何保护系统免受各种安全威胁。 总的来说,《深入理解Linux内核第三版)》是一本全面深入地解析Linux内核的著作。通过读这本书,读者可以逐步理解Linux内核的工作原理和实现细节,对于从事Linux系统开发和运维的人员来说是一本非常有价值的参考书籍。同时,这本书也适合那些对计算机操作系统和底层原理感兴趣的读者阅读,能够帮助他们对操作系统的基本原理有更加深入的理解。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

青衫客36

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值