4 Linux的做法:
1)Linux没有为每一个进程都准备一个tss段,而是每一个cpu使用一个tss段,tr寄存器保存该段。进程切换时,只更新唯一tss段中的esp0字段到新进程的内核栈。
2)Linux的tss段中只使用esp0和iomap等字段,不用它来保存寄存器,在一个用户进程被中断进入ring0的时候,tss中取出esp0,然后切到esp0,其它的寄存器则保存在esp0指示的内核栈上而不保存在tss中。
3)结果,Linux中每一个cpu只有一个tss段,tr寄存器永远指向它。符合x86处理器的使用规范,但不遵循intel的建议,这样的后果是开销更小了,因为不必切换tr寄存器了。
5 进程切换过程总结
进程A正在RING3正常运行(ss0_A在初始化tss的时候赋值了,esp0在启动进程A的时候,设置为了进程表中进程A的s_stackframe的最高地址),这时时钟中断(用于进程切换)发生,根据1、2的介绍,CPU的硬件自动将CPU的ss,
esp指向TSS里面的ss0,
esp0,由前面我们知道,esp0是A进程的s_stackframe的最高地址,所以CPU的esp指向了A进程表的s_stackframe中的最高地址,CPU自动把进程A的ss1_A,esp1_A以及eflags1_A,
cs1_A,
eip1_A五个寄存器值压栈,这5个寄存器的值就被压入到A进程表的s_stackframe结构里面,这个结构最高地址开始的5项正好是储存ss,esp,eflags,cs,eip的。然后CPU就将控制权交给了中断处理程序或者进程调度程序。终端处理程序中,首先把esp0_A的值加4,然后继续压eax1_A,
ecx1_A, edx1_A, ebx1_A, esp1_A, esi1_A, edi1_A, ds1_A, es1_A,
fs1_A,
gs1_A这些寄存器的值,由于esp0_A还在A进程表s_stackframe中,所以这些寄存器被依次压入到了A进程表的s_stackframe中保存起来(进程表s_stackframe的结构就是按照这个顺序来安排的)。
进程调度程序然后将进程B的进程表s_stackframe结构的最低地址(对应着gs1_B)赋给esp0,然后通过和保存A进程寄存器相反的过程,弹出进程B进程表中保存的进程B的寄存器,从gs1_B一直到eax1_B,然后将esp+4(上边的弹出过程已经使esp指向了retaddr,所以为了配合下面的iretd指令,需要esp+4,指向eip1_B),最后通过iretd命令返回到进程B(ring1)(IRETD指令弹出堆栈中数据送EIP,CS,EFLAGS,ESP,
SS)。返回后,内核中的esp0由于堆栈内容全部弹出,指向了进程B的s_stackframe的最高地址,当进程B被切换进入到内核时,重复上面A的过程
看到这里,你可能会想,在内核中,esp必须指向进程表,才能够保证优先级切换,进程切换时得到正确的结果,但是如果我们要调用复杂的进程调度程序,比如进程调度程序不止一个函数,这时一定会用到堆栈操作,那么我们的进程表立刻被破坏掉。所以我们需要切换堆栈,将esp指向另外的位置。