一、TSS 基本概念
1、什么是 TSS ?
TSS(Task State Segment)即任务状态段。具体的说,在设计 “Intel 架构”(即 x86 系统结构)时,每个任务(进程or线程)都对应有一个独立的 TSS ,TSS 就是内存中的一个结构体,里面包含了 几乎所有的 CPU 寄存器的映像 。
在 Linux 0.11 中,就是依靠任务状态段 TSS 的切换来真正完成进程的切换。
2、TR 寄存器
TR(Task Register)即任务寄存器,指向当前进程对应的 TSS 结构体。
TR 指向当前进程对应的 TSS 结构体,但具体是怎么 “指向” 的呢?—— 通过 GDT 表。具体实现为:TR 存储当前进程的 TSS 结构体的段选择符,通过段选择符在 GDT 表中找到对应 TSS 结构体的内存位置。
- 关于段的存储可以看看这篇文章:操作系统:段存储_DoYa~的博客-CSDN博客
二、TSS 切换
1、什么是 TSS 切换 ?
所谓的 TSS 切换 就是将 CPU 中 几乎所有的寄存器 都复制到 TR 指向的那个 TSS 结构体中保存起来,同时还要找到一个目标 TSS(即将要切换到的下一个进程所对应的 TSS),并将其中存放的寄存器映像 “扣” 在CPU 上,就完成了执行现场的切换,如下图所示。
Intel 架构不仅提供了 TSS 来实现任务的切换,而且只需要一条指令就能完成这样的切换,即图中的 ljmp 指令。
2、具体的工作过程
【1】首先用 TR 中存储的段选择符在 GDT 表中找到当前 TSS 的内存位置。即上图的(1)(2)。
由于 TSS 是一个段,所以需要用段表中的一个描述符来表示这个段,这和在系统启动时论述的内核代码段是一样的,当时的那个段使用 GDT 中的 8 对应的第 1 项来描述。此处的 TSS 也是用 GDT 中的某个表项来描述,而 TR 寄存器就是用来表示这个段用 GDT 表中的哪一项来描述,所以 TR 和 CS、DS 等寄存器的功能是完全类似的。
【2】找到了当前的 TSS 段(也就是一段内存区域)以后,将此刻 CPU 中的寄存器映像存放到这段内存区域中,即 “拍” 了一个快照。即上图的(3)。
【3】保存了当前进程的执行现场以后,接下来就要找到目标进程的现场,并将其 “扣” 在 CPU 上。即上图的(4)(5)(6)。
寻找目标 TSS 段的方法也是一样的,因为找段都要从一个描述符表中找,而描述 TSS 的描述符放在 GDT 表中,所以找 目标 TSS 段也要靠 GDT 表。当然只需要给出目标 TSS 段对应的描述符在 GDT 表中存放的位置——段选择子就可以了(仔细回想系统启动时那条著名的 jmpi 0,8 指令,这个段选择子就放在 ljmp 的参数中,实际上就是 jmpi 0,8 中的 8。
【4】一旦将 目标 TSS 中的全部寄存器映像扣在 CPU 上,就相当于切换到了目标进程的执行现场,因为那里存储着目标进程停下时的 CS:EIP,所以现在就要从目标进程停下时的那个 CS:EIP 处开始接着执行。
另外,由于完成了进程切换,目标进程现在就变成了当前进程,所以 TR 需要修改为当前进程(也就是一开始的目标进程)的 TSS 段在 GDT 表中的段描述符所在的位置,因为 TR 总是指向当前进程 TSS 段的段描述符所在的位置。即上图的(7)。
【 注 】从上面的工作过程可以看出,每个进程对应的 TSS 结构体中存储的 CPU 相关信息并不是实时的,而是存储的该进程停下时的 CPU 相关信息。所以每次进行进程切换时,要先通过 TR 寄存器更新一下当前进程的 TSS 结构体,即保存当前进程停止时的执行现场( “拍” 一个照)。
3、switch_to() 函数
上面给出的这些工作都是通过一句长跳转指令 ljmp 段选择子:段内偏移 ,在段选择子指向的段描述符是 TSS 段时 CPU 解释执行的结果。
Linux 0.11 中,进程/线程的切换是通过 switch_to() 函数实现的(在 include/linux/sched.h 中定义),而 switch_to 进行宏展开后实际上就是一句 ljmp 指令:
#define switch_to(n) {
struct{long a,b;} tmp;
__asm__(
"movw %%dx,%1"
"ljmp %0" ::"m"(*&tmp.a), "m"(*&tmp.b), "d"(TSS(n)
)
}
#define FIRST_TSS_ENTRY 4
#define TSS(n) (((unsigned long) n) << 4) + (FIRST_TSS_ENTRY << 3))
上面这段代码就是实现进程的切换(包括记录当前进程的 TSS、切换目标进程)
4、GDT 表
GDT 表的结构如下图所示:
从上图可以看出,第一个 TSS 表项,即 进程 0 的 TSS 表项在第 4 个位置上,4<<3,即 4 * 8,相当于 TSS 在 GDT 表中开始的位置。
而 TSS(n)找的是 进程 n 的 TSS 位置,所以还要再加上 n<<4,即 n * 16。因为每个进程对应有 1 个 TSS 和 1 个 LDT,而这两个描述符的长度都是 8 个字节,总共就是 16 字节,所以是 * 16。其中 LDT 的作用就是上面论述的那个映射表,关于此表的详细论述要等到内存管理一章。
因此,TSS(n) = n * 16 + 4 * 8 得到就是 进程 n(切换到的目标进程)的 TSS 选择子,将这个值存放到 dx 寄存器中,并且又放置到结构体 tmp 中 32 位长整数 b 的前 16 位。现在 64 位 tmp 中的内容是前 32 位为空,这个 32 位数字是段内偏移,也就是 jmpi 0,8 中的 0;而接下来的 16 位是 n * 16 + 4 * 8 ,这个数字是段选择子,也就就是 jmpi 0,8 中的 8,再接下来的 16 位也为空。所以 switch_to 的核心实际上就是 ljmp 空,n*16+4*8 。
到现在,我们应该可以和前面给出的基于 TSS 的进程切换联系在一起了。
三、TSS 切换的缺点
虽然 TSS 用一条指令就能完成任务切换,但条这指令的执行的时间很长,这条 ljmp 指令在实现任务切换时大概需要 200 多个时钟周期。而通过 堆栈 实现任务切换可能要更快,而且采用堆栈的切换还可以使用指令流水的并行优化技术,同时又使得 CPU 的设计变得简单。所以无论是 Linux 还是 Windows,进程/线程的切换都没有使用 Intel 提供的这种 TSS 切换手段,而都是通过堆栈实现的。