本文为<x86汇编语言:从实模式到保护模式> 第15章笔记
由两种基本的任务切换方式, 一种是协同式额, 从一个任务切换到另一个任务, 需要当前任务主动地请求暂时放弃执行权, 或者在通过调用门请求操作系统服务时, 由操作系统"趁机"将控制转移到另一个任务. 这种方式依赖于每个任务的"自律"性, 当一个任务失控时, 其他任务可能得不到执行的机会. 另一种是抢占式的, 在这种方式下, 可以安装一个定时器中断, 并在中断服务程序中实施任务切换. 硬件中断信号总会定时出现, 不管处理器当时在做什么, 中断都会适时地发生, 而任务切换也就能够顺利进行. 在这种情况下, 每个任务都能获得平等的执行机会. 而且, 即使一个任务失控, 也不会导致其他任务没有机会执行.
任务切换概述
如上图所示( - -画的比较挫), 所有任务共享一个全局空间, 这是内核或者操作系统的, 包括了系统服务程序和数据; 同时, 每个任务还有自己的局部空间, 每个人物的功能都不一样, 所以, 局部空间包含的是一个任务区别于其他任务的私有代码和数据.
任务切换是以任务为单位的, 是指离开一个任务, 转到另一个任务中去执行. 要执行任务切换, 系统中必须至少有两个任务, 而且已经有一个正在执行中.
任务切换的方法
第一种任务切换的方法是借助中断, 这也是现代抢占式多任务的基础. 原因很简单, 只要中断没有被屏蔽, 它就能随时发生. 特别是定时器中断, 能够以准确的时间间隔发生, 可以用来强制实施任务切换. 毕竟, 没有哪个人物愿意交出处理器控制权, 也没有哪个任务能精确地把握交出控制权的时机.
在实模式下, 内存最低地址端1KB是中断向量表, 保存着256个中断处理程序的段地址和偏移地址. 当中断发生时, 处理器把中断号乘以4, 作为表内索引号访问中断向量表, 从相应的位置取出中断处理过程的段地址和偏移地址, 并转移到那里执行. 在保护模式下, 中断向量表不再使用, 取而代之的是中断描述符表. 它和GDT, LDT是一样的, 用于保存描述符. 唯一不同的地方是, 它保存的是门描述符, 包括中断门, 陷阱门和任务门. 当中断发生时, 处理器用中断号乘以8(因为每个描述符8字节), 作为索引访问中断描述符表, 取出门描述符. 门描述符中有中断处理过程的代码段选择子和段内偏移量, 这和调用门是一样的. 接着, 转移到相应的位置去执行.
一般的中断处理可以使用中断门和陷阱门. 回忆一下调用门的工作原理, 它只是从任务的局部空间转移到更高特权级的全局空间去执行, 本质上是一种任务内的控制转移行为. 与此相同, 中断门和陷阱门允许在任务内实施中断处理, 转到全局空间去执行一些系统级的管理工作, 本质上, 也是任务内的控制转移行为.
但是, 当中断发生时, 如果该中断号对应的门是任务门, 那么, 性质就截然不同了, 必须进行任务切换, 即, 要中断当前任务的执行, 保护当前任务的现场, 并转换到另一个任务去执行.
如上图所示, 这是任务门描述符的格式. 相对与其他描述符, 任务门描述符中的多数区域没有使用.
任务门描述符中的主要成分是任务的TSS选择子. 任务门用于在中断发生时执行任务切换, 而执行任务切换时必须找到新任务的任务状态段(TSS). 所以, 任务门应当指向任务的TSS. 为了指向任务的TSS, 只需要在任务门描述符中给出任务的TSS选择子就可以了.
任务门描述符的P位指示该门是否有效, 当P位为0时, 不允许通过门实施任务切换; DPL是任务门描述符的特权级, 但是对于因中断而发起的任务切换不起作用, 处理器不按特权级是假任何保护. 但是, 这并不意味着DPL字段没有用处, 当以非中断的方式通过任务门实施任务切换时, 它就有用了.
当中断发生时, 处理器用中断号乘以8作为索引访问中断描述符表. 当它发现这是一个任务门(描述符)时, 就知道应当发起任务切换. 于是, 它取出任务门描述符; 再从任务门描述符中取出新任务的TSS选择子; 接着, 再用TSS选择子访问GDT, 取出新任务的TSS描述符. 在转到新任务执行前, 处理器要先把当前任务的状态保存起来. 当前任务的TSS是由任务寄存器TR的当前内容指向的, 所以, 处理器把每个寄存器的"快照"保存到由TR指向的TSS中. 然后, 处理器访问新任务的TSS, 从中恢复各个寄存器的内容, 包括通用寄存器, 标志寄存器, 段寄存器, 指令指针寄存器EIP, 栈指针寄存器ESP, 以及局部描述符表寄存器LDTR等. 最终, 任务寄存器TR指向新任务的TSS, 而处理器随即开始执行新的任务. 一旦新任务开始执行, 处理器固件会自动将其TSS描述符的B位置1, 表示该任务的状态为忙.
中断发生时, 可以执行常规的中断处理过程, 也可以进行任务切换. 尽管性质不同, 但它们都要使用iret指令返回. 前者是返回到同一任务内的不同代码段; 后者是返回到被中断的那个任务. 处理器根据标志寄存器的NT位(位14)来区分, 意思是嵌套任务标志. 每个任务的TSS中都有一个任务链接域(指向前一个人物的指针), 可以填写为前一个任务的TSS描述符选择子. 如果当前任务EFLAGS寄存器的NT位是1, 则表示当前正在执行的任务嵌套与其他任务内, 并且能够通过TSS任务链接域的指针返回到前一个任务.
因中断而引发任务切换时, 取决于当前任务(旧任务)是否嵌套于其他任务内, 其EFLAGS的NT位可能是0, 也可能是1. 不过这无关紧要, 因为处理器不会改变它, 而是和其他寄存器一道, 写入TSS中保护起来. 另外, 当前任务(旧任务)肯定处于"忙"的状态, 其TSS描述符的B位一定是"1", 在任务切换后同样保持不变.
对新任务的处理是, 要把老任务的TSS选择子填写到新任务TSS中的任务链接域, 同时, 将新任务EFLAGS的NT位置1, 以允许返回到前一个任务(老任务)继续执行, 同时, 还要把新任务TSS描述符的B位置1(忙).
可以使用iret指令从当前任务返回到前一个任务, 前提是EFLAGS的NT位为1. 无论任务处理器碰到iret指令, 它都要检查NT位, 如果为0, 表示是一般的中断过程, 按一般的中断返回处理, 即, 中断返回是任务内的(中断处理过程虽然属于操作系统, 但属于任务的全局空间); 如果此位为1, 则表明当前任务之所以能够正在执行, 是因为中断了别的任务. 因此, 应当返回原先被中断的任务继续执行. 此时, 由处理器固件把当前任务EFLAGS的NT位置0, 并把TSS描述符的B位置0(非忙). 在保存了当前任务的状态之后, 接着, 用新任务(被中断的任务)的TSS恢复现场.
除了中断引发的任务切换外, 还可以用远过程调用指令call, 或者远跳转指令jmp直接发起任务切换. 在这两种情况下, call和jmp指令的操作数是任务的TSS描述符选择子或任务门. 以下是两个例子
call 0x0010:0x00000000 jmp 0x0010:0x00000000
当处理器执行这两条指令时, 首先用指令中给出的描述符选择子访问GDT, 分析他的描述符类型, 如果是一般的代码段描述符, 就按普通的段间转移规则执行; 如果是调用门, 按调用门的规则执行; 如果是TSS描述符, 或者任务门, 则执行任务切换. 此时指令中给出的32位偏移被忽略, 原因是执行任务切换时, 所有处理器的状态都可以从TSS中获得. 注意, 任务门描述符可以安装在中断描述符表中, 也可以安装在GDT或者LDT中.
如果用于发起任务切换, call指令和jmp指令也有不同之处. 使用call发起的任务切换类似于因中断发起的任务切换. 这就是, 由call发起的任务切换是嵌套的, 当前任务(旧任务)TSS描述符的B位保持原来的1不变, EFLAGS寄存器的NT位也不发生变化; 新任务TSS描述符的B位置1, EFLAGS的NT位也置1, 表示此任务嵌套与其他任务中. 同时, TSS任务链接域的内容改为旧任务的TSS描述符选择子.
如上图所示(- -画的较挫), 假设任务1是整个系统中的第一个任务. 当任务1开始执行时, 其TSS描述符的B位是1, EFLAGS的NT位是0, 不嵌套于其他任务. 当从任务1转换到任务2后, 任务1任然为忙, eflags的nt位不变(在其TSS中); 任务2也变为忙, eflags的nt位为1, 表示嵌套与任务1中, 同时, 任务1的TSS描述符选择子也被复制到任务2的TSS中(任务链接域). 最后是从任务2转换到任务3执行, 和从前一样, 任务2保持忙状态, eflags的nt位不变(在其TSS中); 任务3成为当前任务, 其TSS描述符的B位变成1, eflags寄存器的nt位也变成1, 同时, 其TSS的任务链接域指向任务2.
用call指令发起的任务切换, 可以通过iret返回前一个任务. 此时, 旧任务(当前任务)TSS描述符的B位, 以及eflags的nt位都被恢复到0, 并保存到它的TSS中.
用jmp指令发起的任务切换, 不会形成任务之间的嵌套关系. 执行任务切换时, 当前任务(旧任务)TSS的B位清0, eflags的nt位不变; 新任务TSS描述符的B位置1, 进入忙的状态, eflags的nt位保持从TSS中加载时的状态不变.
任务是不可重入的. 任务不可重入的本质是, 执行任务切换是, 新任务的状态不能为忙. 这里有两个典型的情形:
- 执行任务切换时, 新任务不能是当前任务自己. 试想一下, 如果允许这种情况发生, 处理器该如何执行现场保护和恢复操作?
- 如上图所示, 不允许使用call指令从任务3切换到任务2和任务1上. 如果不禁止这种情况的话, 任务之间的嵌套关系将会因为TSS任务链接域的破坏而错乱.
处理器是通过TSS描述符的B位来检测重入的. 因中断, iret, call和jmp指令发起任务切换时, 处理器固件会检测新任务TSS描述符的B位, 如果为1, 则不允许执行这样的切换.
处理器在实施任务切换时的操作
处理器用一下4种方法将控制转移到其他任务:
- 当前程序, 任务或着过程执行一个将控制转移到GDT内某个TSS描述符的jmp或者call指令.
- 当前程序, 任务或者过程执行一个将控制转移到GDT或者当前LDT内某个任务门描述符的jmp或者call指令.
- 一个异常或者中断发生时, 中断号指向中断描述符表内的任务门.
- 在EFLAGS寄存器的NT位置位的情况下, 当前任务执行了一个iret指令.
在任务切换是, 处理器执行以下操作:
- 从jmp或者call指令的操作数, 任务门或者当前任务的TSS任务链接域取的新任务的TSS描述符选择子. 最后一种方法适用于以iret发起的任务切换.
- 检查是否允许从当前任务(旧任务)切换到新任务. 数据访问的特权级检查规则适用于jmp和call指令, 当前(旧)任务的CPL和新任务段选择子的RPL必须在数值上小于或者等于目标TSS或者任务门的DPL. 异常, 中断(除了int n指令引发的中断)和iret指令引起的任务切换忽略目标任务门或者TSS描述符的DPL. 对于以int n指令产生的中断, 要检查DPL.
- 检查新任务的TSS描述符是否已经标记为有效(P = 1), 并且界限值也有效(大于过着等于0x67, 十进制103).
- 检查新任务是否可用, 不忙(B = 0, 对于以call, jmp, 异常或者中断发起的任务切换)或者忙(B = 1, 对于以iret发起的任务切换).
- 检查当前任务(旧任务)和新任务的TSS, 以及所有任务切换时用到的段描述符已经安拍到系统内存中.
- 如果任务切换是由jmp或者iret发起的, 处理器清除当前(旧)任务的忙(B)标志; 如果是由call指令, 异常或者中断发起的, 忙标志保持原来的置为状态.
- 如果任务切换是由iret指令发起的, 处理器建立eflags寄存器的一个临时副本并清除其NT标志; 如果是由call, jmp, 异常或者中断发起的, 副本中的NT标志不变.
- 保存当前(旧)任务的状态到它的TSS中. 处理器从任务寄存器中找到当前TSS的基地址, 然后将一下寄存器的状态复制到它的TSS中: 所有通用寄存器, 段寄存器中的段选择子, 刚才那个eflags寄存器的副本, 以及指令指针寄存器EIP.
- 如果任务切换是由call指令, 异常或者中断发起的, 处理器把从新任务加载的eflags的nt标志置位; 如果是由iret或者jmp指令发起的, nt标志位的状态对应着从新任务加载的eflags的nt位.
- 如果任务切换是由call, jmp, 异常或者中断发起的, 处理器将新任务TSS描述符中的B位置位; 如果是由iret指令发起的, B位保持原先的置位状态不变.
- 用新任务的TSS选择子和TSS描述符加载任务寄存器TR.
- 新任务的TSS状态数据被加载到处理器. 者包括LDTR, PBDR(控制寄存器CR3), eflags, eip, 通过用寄存器, 以及段选择子. 载入状态期间只要发生一个故障, 架构状态就会被破坏(因为有些寄存器的内容已被改变, 而且无法撤销和回退). 所谓架构, 是指处理器对外公开的那一部分的规格和构造; 所谓架构状态, 是指处理器内部的各种构件, 在不同的条件下, 所建立起来的确定状态. 当处理器处于某种状态时, 再施加另一种确定的条件, 可以进入另一种确定的状态, 这应当是严格的, 众所周知的, 可预见的. 否则, 就意味着架构状态遭到破坏.
- 与段选择子相对应的描述符在经过验证后也被加载. 与加载和验证新任务环境有关的任何错误都将破坏架构状态. 注意, 如果所有的检查和保护工作都已经成功实施, 处理器提交任务切换. 如果在第11步的过程中发生了不可恢复性的错误, 处理器不能完成任务切换, 并确保处理器返回到执行发起任务切换的那条指令前的状态. 如果在第12步发生了不可恢复的错误, 架构状态被破坏; 如果在提交点(第13步)之后发生了不可恢复性的错误, 处理器完成任务切换并在开始执行新任务之前产生一个相应的异常.
- 开始执行新任务.
任务切换时, 当前任务的状态总要保存起来, 在恢复执行时, 处理器从eip寄存器的保存值所指向的那条指令开始执行, 这个寄存器的值是在当初任务切换被挂起时保存的.
任务切换时, 新任务的特权级并不是从那个被挂起的任务继承来的. 新任务的特权级别是由其段寄存器CS的低2位决定的.
任务状态段TSS的任务链接域和eflags的nt位用于返回前一个任务执行, 当前eflags寄存器的nt位是1表明当前任务嵌套与其他任务中. 无论如何, 新任务的TSS描述符的B位都会被置位, 旧任务的B位取决于任务切换的方法, 如下图所示