在之前我们实现的线程都是在内核线程,从中断发生到中断结束,都没有涉及到特权级的变化。而我们要实现用户进程,则整个任务运行的过程中,则必会发生特权级转移过程。
任务状态段TSS
单核CPU要想实现多任务,唯一的方案就是多个任务共享同一个CPU。这其中必须要做的就是保护任务的上下文。
为了实现这个,Intel 的建议是给每个任务关联一个任务状态段TSS(Task State Segment),用它来表示任务。
从上图可见,TSS 中的字段基本上全是寄存器,这些寄存器就是任务运行中的最新状态,所以TSS就是用来保存任务的快照。
TSS中有三组栈:SS0和esp0,SS1和esp1,SS2和esp2。这三组栈仅仅是CPU用来由低特权级进入高特权级时用(除了中断和调用门返回外,CPU不允许从高特权级(内核)转入低特权级(用户)),CPU并不会主动在TSS中更新相应特权级的栈指针,不管进入高特权级后进行了多次压栈操作,下次重新进入该特权级时,该特权级别的栈指针依然是TSS中的最初值,除非认为改写,否则这三组栈指针将一成不变。
TSS和其他段一样,本质上一片存储数据的内存区域,所以它也是如同其他段那样,用某个描述符结构来描述符它,这就是TSS描述符,它同样在GDT中注册。
TYPE
值为10B1,B表示busy位。
除此之外,在CPU中有一个专门存储TSS信息的寄存器,这就是TR寄寄存器
ltr "16位通用寄存器" 或 "16位内存单元" ; 不管是寄存器还是内存,必须是描述符在GDT中选择子。
有了这些基础之后,任务在被换下CPU时,由CPU自动地把当前任务的资源状态,保存到该任务对应的TSS中(由寄存器TR指定)。
CPU原生支持的任务切换方式
进行任务切换的方式有 "中断+任务门","call 或 jmp+任务门" 和 irted。
通过"中断+任务门"进行任务切换
最直接实现任务切换的是任务门:
任务门描述符中的是TSS选择子。(中断描述符表中可以是中断门,陷阱门,任务门)
当中断发生后,处理器通过向量号在IDT中找到对应描述符后,通过分析描述符中字段S
和TYPE
字段,判断描述符类型。
若发现此中断对应的描述符对应的是中断门描述符,则转而去执行此中断门描述符中指定的中断处理例程,最后通过iretd
指令返回到被中断任务的中断前代码。
iretd
指令用于从中断处理例程返回,这只是属于它的一个功能。它一共有2个功能:
- 从中断返回当前任务的中断前代码
- 当前任务是被嵌套调用时,它会调用自己TSS中的"上一个任务的TSS指针"的任务,也就是返回到上一个任务。
所以,当调用一个新任务时,CPU做了2件事:
【1】自动将新任务eflags中的NT位置1
【2】随后处理器将旧任务的 TSS 选择子写入新任务 "TSS 的上一个任务的TSS指针" 字段中
所以执行iretd
指令时,始终要判断NT位,如果NT位为1,表示嵌套调用,于是CPU把当前TSS中"上一个任务的TSS指针"获取旧任务的TSS,转而去执行旧任务。如果NT位等于0,表示要回到当前任务中断前的指令部分。
-
综上,通过任务门进行任务切换的过程如下:
【1】从该任务门描述符中中取出新任务的TSS选择子。
【2】用新任务TSS选择子在GDT中索引TSS描述符
【3】判断该TSS描述符P位是否为1,为1表示该TSS描述符对应的TSS已经位于内存中。
【4】从寄存器TR中取出旧任务(当前任务)的TSS位置,保存旧任务(当前任务)的状态到
【5】把新任务的TSS中的值加载到响应寄存器中
【6】使寄存器TR指向新任务的TSS
【7】将新任务(当前任务)的TSS描述符中的B位置1
【8】将新任务标志寄存器eflags的NT位置1
【9】将旧任务的TSS选择子写入新任务TSS中的"上一个任务的TSS指针"
【10】开始执行新任务在执行新任务之前,旧任务是当前的任务,因此旧任务TSS描述符中的B位为1,在调用新任务后,也不会修改,因为它尚未执行玩,属于嵌套调用别的任务,并不是单独的任务。
当新任务执行完成后,调用iretd指令返回到旧任务,此时处理器检查NT位,其值为1,便进行返回工作:
【1】将当前任务(新任务)标志寄存器中eflags的NT位置0
【2】将当前任务的状态信息写入TR指向的TSS
【3】获取当前任务TSS中的 "上一个任务的TSS指针" 字段的值,将其加载到TR中,恢复上一个任务的状态。
【4】执行上一个任务
通过call,jmp切换任务
任务门描述符除了在IDT中注册,也可以在GDT中注册。
其次,任务以TSS为代表,只要包括TSS选择子的对象都可以作为任务切换的操作数。
call 任务门选择子或者TSS选择子
jmp 任务门选择子或者TSS选择子
call是有去有回的指令,jmp是一去不返的指令。它们在调用新任务的时的区别也在于此。
call 指令以任务嵌套的方式调用新任务,当以call指令调用新任务时。比如
call 0x0018 : 0x1234
,其步骤如下:
【1】CPU忽略偏移量0x1234,拿选择子0x0018在GDT中索引到第3个描述符。
【2】检查描述符中的P位。若P位为0,表示该段不存在,引发异常,同时检查该描述符中的 S与TYPE的值,判断其类型,如果是TSS描述符,检查该描述符的B位,B位若为1,抛出异常,表示调用不可重入。
【3】进行特权级检查,数值上 "CPL和TSS选择子中的RPL"都要小于等于TSS描述符DPL。
【4】特权检查完后,将当前任务的状态保存到寄存器TR指向的TSS中。
【5】加载新任务TSS选择子到TR寄存器中。
【6】将新任务TSS中的寄存器数据载入到相应寄存器中
【7】CPU把新任务的eflags寄存器中的NT位置1
【8】将旧任务的TSS选择子写入新任务(当前任务)TSS中的"上一个任务的TSS指针"
【9】将新任务(当前任务)的TSS描述符中的B位置1(旧任务的B位不变)
【10】开始执行新任务,完成任务切换jmp 指令以非嵌套的方式调用新任务,新任务和旧任务之间不会形成链式关系。当以 jmp 指令调用新任务时,新任务的TSS描述符中的B位会被CPU置1,旧任务的TSS描述符中的B位置0。
现代操作系统采用的任务切换方式
在上面我们介绍了Intel原生的任务切换方式,但现在CPU并没有采取这一方案,因为过程繁琐,效率低下。
Linux 对TSS的操作是一次性加载到TR,之后不断修改同一个TSS的内容不再进行重复加载操作。
同时,在TSS中只初始化了ss0,esp0 和 I/O位图字段,除此之外TSS便没用。
那任务的状态信息保存在哪里?
当CPU由低特权级(用户态)进入高特权级(内核态)时,CPU自动从当前任务的TSS中获取 SS0 和 esp0字段的值作为0特权级的栈,然后执行一系列的push将任务保存在0特权级栈中。
最后,Linux中任务切换也不使用call 和 jmp 指令。