实现用户进程一

在之前我们实现的线程都是在内核线程,从中断发生到中断结束,都没有涉及到特权级的变化。而我们要实现用户进程,则整个任务运行的过程中,则必会发生特权级转移过程。

任务状态段TSS

单核CPU要想实现多任务,唯一的方案就是多个任务共享同一个CPU。这其中必须要做的就是保护任务的上下文。

为了实现这个,Intel 的建议是给每个任务关联一个任务状态段TSS(Task State Segment),用它来表示任务。

实现用户进程一_第1张图片
TSS描述符.png

从上图可见,TSS 中的字段基本上全是寄存器,这些寄存器就是任务运行中的最新状态,所以TSS就是用来保存任务的快照。

TSS中有三组栈:SS0和esp0,SS1和esp1,SS2和esp2。这三组栈仅仅是CPU用来由低特权级进入高特权级时用(除了中断和调用门返回外,CPU不允许从高特权级(内核)转入低特权级(用户)),CPU并不会主动在TSS中更新相应特权级的栈指针,不管进入高特权级后进行了多次压栈操作,下次重新进入该特权级时,该特权级别的栈指针依然是TSS中的最初值,除非认为改写,否则这三组栈指针将一成不变。

TSS和其他段一样,本质上一片存储数据的内存区域,所以它也是如同其他段那样,用某个描述符结构来描述符它,这就是TSS描述符,它同样在GDT中注册。

实现用户进程一_第2张图片
TSS描述符格式.png

TYPE值为10B1,B表示busy位。

除此之外,在CPU中有一个专门存储TSS信息的寄存器,这就是TR寄寄存器

TR寄存器.png
ltr "16位通用寄存器" 或 "16位内存单元"    ; 不管是寄存器还是内存,必须是描述符在GDT中选择子。

有了这些基础之后,任务在被换下CPU时,由CPU自动地把当前任务的资源状态,保存到该任务对应的TSS中(由寄存器TR指定)。

CPU原生支持的任务切换方式

进行任务切换的方式有 "中断+任务门","call 或 jmp+任务门" 和 irted。

通过"中断+任务门"进行任务切换

最直接实现任务切换的是任务门:


实现用户进程一_第3张图片
任务门.png

任务门描述符中的是TSS选择子。(中断描述符表中可以是中断门,陷阱门,任务门)

当中断发生后,处理器通过向量号在IDT中找到对应描述符后,通过分析描述符中字段STYPE字段,判断描述符类型。

若发现此中断对应的描述符对应的是中断门描述符,则转而去执行此中断门描述符中指定的中断处理例程,最后通过iretd指令返回到被中断任务的中断前代码。

iretd指令用于从中断处理例程返回,这只是属于它的一个功能。它一共有2个功能:

  • 从中断返回当前任务的中断前代码
  • 当前任务是被嵌套调用时,它会调用自己TSS中的"上一个任务的TSS指针"的任务,也就是返回到上一个任务。

所以,当调用一个新任务时,CPU做了2件事:
【1】自动将新任务eflags中的NT位置1
【2】随后处理器将旧任务的 TSS 选择子写入新任务 "TSS 的上一个任务的TSS指针" 字段中

实现用户进程一_第4张图片
TSS链.png

所以执行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 指令。

你可能感兴趣的:(实现用户进程一)