80386学习(三) 80386任务切换机制
一、80386任务切换介绍
前面提到过,80386是一款对多任务操作系统提供良好支持的CPU。多道程序功能使得在某一耗时任务执行时(例如大数据的I/O),允许其它短耗时任务并发的执行(例如接受输入的控制台命令) ,极大的提高了用户的体验。
80386作为早期的32位CPU,是单核处理器。因此80386支持的多任务系统,指的是能够在同一时间内并发运行多个互相之间独立的程序,运行中的多个程序能够在操作系统的任务管理程序下交替、相互穿插的执行。并发的多个任务或主动的让出CPU,或被动的被强制剥夺CPU的使用权,从而达到在单位时间内,不同任务都能够得到CPU资源的目的。
由于相互独立的任务之间很难彼此信任,相互间协调的工作(例如协程,在合适的时候主动的让出CPU);同时也为了减少应用程序开发的负担,主流的操作系统一般通过时钟中断这样的周期性事件,剥夺运行中任务的CPU控制权。任务之间的切换、调度完全由操作系统负责,应用程序在开发时一般不需要考虑任务调度相关的问题。
应用程序无感知的任务调度能够在任意时刻终止当前任务A的执行,并切换至另一个任务B;在接下来的某一时刻还能切换回任务A并接着运行。要做到这一点,就必须确定任务运行时的关键上下文数据,在任务换出时将当时的上下文信息像快照一样保存起来(保护现场);同时在任务被重新调度回来时,通过之前保留的上下文信息,恢复现场。
那么什么是80386中的任务上下文?80386又是如何做到在任务切换时保护现场和恢复现场的?
二、80386任务状态段
为了保证任务切换后,能够被正确的恢复,80386设计了一个专用数据结构用于保存恢复任务所必需的信息,这一特殊的数据结构被称为任务状态段TSS(Task-State Segement),大小至少为104字节,其基本内容结构图如下图所示。
任务状态段TSS中的内容,按照类型大致可以分为以下几个部分:
寄存器保存区域
位于TSS中间位置的寄存器保存区域,用于保存切换任务时,32位的通用寄存器(EAX、EBX等)、16位的段寄存器(CS、DS、ES等)、32位的标志寄存器EFLAGS、指令指针寄存器EIP的快照记录。
高特权级堆栈指针区域
在关于数据段特权级时提到过,为了避免高特权级的程序由于栈空间不足而崩溃以及不同特权级间栈数据的交叉引用。处理器在特权级发生变化时,当前任务的堆栈也必须发生变化。
具体来说就是一个任务在每个不同的特权级下,都应该拥有一个独立的堆栈。特权级一共有4个等级,同时由于特权级的限制,高特权级的任务一般无法将控制转移至低特权级的段。
特权级为0的内核任务不需要额外的栈,使用自己固有的内核栈即可。特权级为1的任务需要定义一个额外的栈,用于将控制转移至特权级0时使用。依此类推,特权级为2的任务需要定义两个额外的栈,特权级为3的应用程序应该准备3个额外的堆栈以便在不同的特权级间进行切换。
这在TSS中有所体现,任务切换时也必须把这些额外的栈选择子/栈顶指针记录下来,其中SS0、SS1、SS2分别是任务在特权级0、1、2时的栈段选择子,ESP0、ESP1、ESP2分别是任务在特权级0、1、2时的栈顶指针寄存器的值。至于当前任务固有的栈,则位于上述的寄存器保存区域中的SS、ESP中。
不同特权级切换时的额外的栈一般是由操作系统加载任务时创建的,对应用程序是透明的。
地址映射寄存器区域
80386在开启了保护模式后,内存寻址的方式发生了改变。
每个任务一般都存在一个属于任务自己的段表LDT,同时如果当前CPU还开启了分页机制的话,则还需要通过一个页表来做进一步的映射,将内存的线性地址转换为最终的物理地址。所以在TSS中,存有指向当前任务LDT首地址的LDT段选择子字段,以及在分页模式下工作的CR3寄存器(PDBR Page Directory Base Register 页表基址寄存器)的值。
LDT段选择子加能够帮助CPU定位当前任务的段表;保存的CR3能够在开启分页机制时,指向一级页表的首地址。关于80386内存分页机制的原理,将在后续的博客中展开介绍。
任务链接字段
80386允许在任务之间进行嵌套。在中断等情况下,可以暂时打断当前任务(如应用程序)并切换至更紧急的中断处理程序(如操作系统内核内存缺页中断处理程序)。
为了能够在任务切换时,新任务执行完成后再切换回之前的老任务。TSS中引入了任务链接字段,用于在嵌套任务切换时,记录当前任务的前一个任务的TSS段选择子。嵌套任务的切换工作原理,将在下文进行详细介绍。
I/O映射位图基地址
前面的博客在I/O特权级保护中提到了I/O映射位图。和LDT一样,每个任务都拥有自己的I/O映射位图,TSS中的I/O映射位图基地址用于指向当前任务的I/O映射位图,其中的值代表着I/O许可位图相对于当前TSS内存段起始位置的偏移量。这也是为什么TSS段最小是104字节的原因,因为在104字节的基础结构之后,还允许拓展的存放一个I/O映射位图数据。
在寻找TSS时,如果发现该偏移量已经超过了TSS段描述符中的段界限,CPU不会报错,而是认为当前任务不存在I/O映射位图。这种情况下,如果任务的当前特权级CPL低于IOPL的话,将无权访问任何外设端口。
任务寄存器TR
TSS和LDT一样,都被认为是一种系统内存段(S位为0),对应的段描述符必须预先加载进全局描述符表GDT中。这么设计的主要目的还是为了将TSS通过特权级的机制加以保护。
80386提供了LDTR寄存器用于指向当前任务的LDT段。同理,80386也提供了另一个专用的寄存器指向当前任务的TSS段,这个专用的寄存器被称为任务寄存器(Task Resigter TR)。TR寄存器是16位的,其中装载的是所指向的TSS段的段选择子。任务切换时,将TR中的段选择子指向新任务的TSS段即可。
TSS段描述符
TSS作为段描述符的一种,和LDT段描述符,数据段/代码段描述符的格式差不多,最大的区别在于TYPE字段。TSS的TYPE字段为10B1,其中高2位,和最低位是固定不变的,而B位是忙(Busy)位的意思,B位为0时代表任务不处于忙状态,B位为1时代表任务处于忙状态。
当任务刚刚被初始化时,B位应该被操作系统设置为0。当任务处于执行状态时,或者因为中断等原因被暂时切换为挂起状态时,B位会被CPU固件设置为1。
三、80386任务切换的方式
80386允许在4种情况下进行任务切换:
1、当前任务执行了一条引用TSS描述符的JMP或者CALL指令
使用jmp far或者call far时,如果给出的段选择子指向的是普通的代码段,那么CPU认为是普通的跳转或是过程调用,依然是同一个任务,不会发生不同任务上下文的切换。
使用jmp far或者call far给出的段选择子指向的是一个TSS系统段时,CPU将进行任务切换。
2、当前任务执行了一条引用任务门的JMP或者CALL指令
使用jmp far或者call far时,如果给出的段描述符指向的是一个任务门时,CPU将进行任务切换。
3、引用中断描述符表(IDT)中的任务门中断或异常
一般的中断处理可以使用中断门或者陷阱门进行处理,此时CPU认为这是在同一个任务内的控制转移。但是如果中断号对应的是任务门,则CPU认为这是一次任务切换。
4、当嵌套任务标志NT置位时,当前任务执行了一条IRET指令
中断发生时,可以是同一个任务内进行常规的中断处理,也可以进行任务切换,这两者都可以通过IRET中断返回指令进行返回。同一任务内的中断返回会跳转回中断触发时的代码段,当前任务继续。使用任务切换进行的任务处理将会在返回时,切换回中断发生前的任务。
IRET指令返回时,用于区分上述两种类型的方式是判断标志寄存器EFLAGS的NT位(第14位)的值,NT的意思是Nested Task,NT位是嵌套位置标识。NT位=1时,代表当前执行的任务是嵌套于另一个任务中的,并且能够在任务返回时通过TSS中的任务链接字段返回到前一个任务。
当因中断而发生任务切换的瞬间,存在两个概念:旧任务(当前被打断执行的任务)和新任务。
对于旧任务,需要保护其上下文,将包括EFLAGS标志寄存器内的各种必要的数据快照存入旧任务的TSS段中保存,此时的旧任务TSS段TYPE字段的B位一定是为1的,因为当前任务是处于执行状态被中断被迫打断执行的。
对于新任务,需要将旧任务的TSS段选择子存入自己的TSS段中的任务链接字段处,用于在新任务IRET返回时能够切换回旧任务。此时的新任务TSS段TYPE字段的B位设置为1,并且切换时的新任务EFLAGS寄存器中的NT位也
必须设置为1。
CPU在执行IRET中断返回指令时,会首先检查EFLAGS寄存器的NT位。
如果NT位为0,说明当前任务没有处于其它任务中嵌套的执行,进行普通的中断过程返回,不发生任务切换。
如果NT位为1,说明当前任务的是嵌套于其它被打断的任务中执行的,需要让被打断的任务恢复运行。此时,将当前需要返回的任务的NT位设置为0,同时当前任务对应的TSS段描述符的B位也设置为0,因为中断返回的当前任务已经返回,变得不忙(Busy)了也不嵌套于其它任务中。随后,进行当前任务的现场保护,使用保存在当前任务TSS中的任务链接字段指向的被中断任务的TSS进行现场恢复。
用CALL指令发起的任务切换,其任务新旧任务状态是嵌套的。可以使用IRET指令返回前一个任务,此时的旧任务TSS段的B位以及EFLAGS寄存器的NT位都设置为0,即不忙,非嵌套状态。
用JMP指令发起的任务切换,不会产生新旧任务的嵌套状态,此时旧任务的B位设置为0,但EFLAGS寄存器的NT位保持不变。新任务的B位设置为1,NT位和新任务恢复现场时的TSS中的快照记录保持一致。
任务的不可重入性
任务是不可以重入的,即新任务进行切换时,其本身的状态不能为忙(TSS段TYPE字段的B位不能为1)。
在嵌套的任务链中,新任务TSS段的任务链接字段指向了上一个被打断执行的任务,构成了一个单向的链表。伴随着一个个新任务的终止并返回,嵌套的处理逻辑总会执行完。但前提是嵌套链表中不能出现环路。如果出现了环路,即新任务还能够嵌套进已经在当前链条中的旧任务中,那么这个嵌套链条便成为了一个永远无法终止的死循环了。
由于当前任务被中断嵌套时,其TSS段的B位始终都是为1的,因此CPU在处理任务切换时会首先校验新任务TSS段的B位,如果为1则表示新任务也是处于嵌套链条中的,CPU会产生异常以阻止这样的任务切换。
总结
80386的任务这一概念,与操作系统中的进程、线程这些概念有着紧密关系。对任务和任务切换概念的理解,有助于更好的学习操作系统。
在这任务切换方式的介绍中,多次提到了任务门、中断门等概念。限于篇幅,关于门描述符和中断相关的内容将在后续的博客中进行详细介绍。