【学习&理解】基于TSS和基于内核栈两种机制的进程切换
在Linux0.11中,进程的切换是基于intel提供的TSS机制的,要从一个进程切换到别的进程,就是切换TSS这个结构。但是,这样的切换方式效率太低,所以后来Linux和Windows都改成采用基于内核栈来切换这种方式。由于TSS机制比较简单,所以大部分精力用于讲解基于内核栈机制。
以下的基于内核栈方式是通过修改Linux0.11的基于TSS方式实现的。
TSS机制
以下是从网上找到的TSS机制切换的流程图:
就这张图来讲解,TR(task register)是用于在GDT中索引当前任务的TSS描述符,是一个选择子,切换任务的话需要把TR也修改。
首先我们在切换任务之前,需要把当前任务的环境保存一下(即把CPU中的寄存器的值保存到“当前TSS”相应的位置eax对eax,ebx对ebx这样)。
然后就是找到“目标TSS”,把其中的恢复(即把CPU中的寄存器的值设置成“目标TSS”中保存的)。
环境恢复之后,就需要把TR设置成“目标TSS”的选择子。
其中,GDT表再详细点是如下图:
每个进程的LDT选择子都被保存于相应TSS中。由于该机制花费了很多时间在保存与赋值,而这两种操作是比较低效率的,所以该机制已不在Linux和Windows中使用。
内核栈机制
要讲切换,首先讲一下一个进程的结构。
一个进程的创建,是由其父进程调用系统调用来创建的。
每次使用fork()创建一个进程,都会申请一页的空间(4kb),低地址空间base用来存放进程的PCB,而base+PAGE_SIZE则是作为该进程的内核栈的栈底。
该栈用于存放父进程的各种寄存器值,毕竟fork出来的子进程其实跟父进程是完全一样的(除非调用了exec类函数等)。子进程的寄存器基本都是用父进程相应寄存器来赋值(eax除外,其为fork的返回值,子进程的为0)。
父进程在调用系统调用创建子进程时,会把自己的ss、esp、EFLAGS、cs、eip压入栈,这对于调用copy_process函数就相当于传参(其实函数中的参数都是从堆栈中获取的),而在中断处理函数中还会压入父进程的其他寄存器。这时,copy_process(int nr, long ebp, long edi, long esi, long gs, long none,long ebx, long ecx, long edx,long fs, long es, long ds,long eip, long cs, long eflags, long esp, long ss)函数就获得了所有需要的参数(父进程的寄存器)。
压入cs:eip指向的是fork函数的下一个指令的地址,所以子进程被调用的话,第一条指令是调用fork函数完后下面的第一条指令。如if(fork() == 0),这里的所谓的“第一条指令”就是用fork函数的返回值与0做比较。
然后就是switch_to函数用于切换进程,该函数传入两个参数:子进程的PCB地址和其ldt描述符的索引。该函数由汇编代码组成,首先是保存父进程一系列的寄存器到父进程内核栈,还有保存esp在自身PCB相应字段,把子进程的内核栈指针写到全局TSS中esp0字段,然后就是再次把子进程的内核栈指针(在PCB中)写到现在的esp寄存器,这样就可以用ss:esp进行压栈和出栈了。接着就是切换LDT了,使用lldt指令就可以了。然后设置fs寄存器,使其保存指向用户数据空间的局部描述符(0x17 = 0001 0111b)。最后就是连续的出栈指令,把之前创建子进程时保存到子进程内核栈的都出栈到相应的寄存器(这时的内核栈已经切换为子进程的了)。
到这里,切换就完成了。
欢迎各位发现错误后指出,本人一定及时改正并致以最诚恳的感谢!