5.4.1 硬件支持
Intel i386 体系结构包括了一个特殊的段类型,叫任务状态段(TSS),如图5.4所示。每个任务包含有它自己最小长度为104字节的TSS段,在/include/ i386/processor.h 中定义为tss_struct结构:
struct tss_struct {
unsigned short back_link,__blh;
unsigned long esp0;
unsigned short ss0,__ss0h;/*0级堆栈指针,即Linux中的内核级 */
unsigned long esp1;
unsigned short ss1,__ss1h; /* 1级堆栈指针,未用*/
unsigned long esp2;
unsigned short ss2,__ss2h; /* 2级堆栈指针,未用*/
unsigned long __cr3;
unsigned long eip;
unsigned long eflags;
unsigned long eax,ecx,edx,ebx;
unsigned long esp;
unsigned long ebp;
unsigned long esi;
unsigned long edi;
unsigned short es, __esh;
unsigned short cs, __csh;
unsigned short ss, __ssh;
unsigned short ds, __dsh;
unsigned short fs, __fsh;
unsigned short gs, __gsh;
unsigned short ldt, __ldth;
unsigned short trace, bitmap;
unsigned long io_bitmap[IO_BITMAP_SIZE+1];
/*
* pads the TSS to be cacheline-aligned (size is 0x100)
*/
unsigned long __cacheline_filler[5];
};
每个TSS有它自己 8字节的任务段描述符(Task State Segment Descriptor ,简称TSSD)。这个描述符包括指向TSS起始地址的32位基地址域,20位界限域,界限域值不能小于十进制104(由TSS段的最小长度决定)。TSS描述符存放在GDT中,它是GDT中的一个表项。
图5.4 Intel i386任务状态段及TR寄存器
后面将会看到,Linux在进程切换时,只用到TSS中少量的信息,因此Linux内核定义了另外一个数据结构,这就是thread_struct 结构:
struct thread_struct {
unsigned long esp0;
unsigned long eip;
unsigned long esp;
unsigned long fs;
unsigned long gs;
/* Hardware debugging registers */
unsigned long debugreg[8]; /* %%db0-7 debug registers */
/* fault info */
unsigned long cr2, trap_no, error_code;
/* floating point info */
union i387_union i387;
/* virtual 86 mode info */
struct vm86_struct * vm86_info;
unsigned long screen_bitmap;
unsigned long v86flags, v86mask, v86mode, saved_esp0;
/* IO permissions */
int ioperm;
unsigned long io_bitmap[IO_BITMAP_SIZE+1];
};
用这个数据结构来保存cr2寄存器、浮点寄存器、调试寄存器及指定给Intel 80x86处理器的其他各种各样的信息。需要位图是因为ioperm( ) 及 iopl( )系统调用可以允许用户态的进程直接访问特殊的I/O端口。尤其是,如果把eflag寄存器中的IOPL 域设置为3,就允许用户态的进程访问对应的I/O访问权位图位为0的任何一个I/O端口。
那么,进程到底是怎样进行切换的?
从第三章我们知道,在中断描述符表(IDT)中,除中断门、陷阱门和调用门外,还有一种“任务们”。任务门中包含有TSS段的选择符。当CPU因中断而穿过一个任务门时,就会将任务门中的段选择符自动装入TR寄存器,使TR指向新的TSS,并完成任务切换。CPU还可以通过JMP或CALL指令实现任务切换,当跳转或调用的目标段(代码段)实际上指向GDT表中的一个TSS描述符项时,就会引起一次任务切换。
Intel的这种设计确实很周到,也为任务切换提供了一个非常简洁的机制。但是,由于i386的系统结构基本上是CISC的,通过JMP指令或CALL(或中断)完成任务的过程实际上是“复杂指令”的执行过程,其执行过程长达300多个CPU周期(一个POP指令占12个CPU周期),因此,Linux内核并不完全使用i386CPU提供的任务切换机制。
由于i386CPU要求软件设置TR及TSS,Linux内核只不过“走过场”地设置TR及TSS,以满足CPU的要求。但是,内核并不使用任务门,也不使用JMP或CALL指令实施任务切换。内核只是在初始化阶段设置TR,使之指向一个TSS,从此以后再不改变TR的内容了。也就是说,每个CPU(如果有多个CPU)在初始化以后的全部运行过程中永远使用那个初始的TSS。同时,内核也不完全依靠TSS保存每个进程切换时的寄存器副本,而是将这些寄存器副本保存在各个进程自己的内核栈中(参见上一章task_struct结构的存放)。
这样以来,TSS中的绝大部分内容就失去了原来的意义。那么,当进行任务切换时,怎样自动更换堆栈?我们知道,新任务的内核栈指针(SS0和ESP0)应当取自当前任务的TSS,可是,Linux中并不是每个任务就有一个TSS,而是每个CPU只有一个TSS。Intel原来的意图是让TR的内容(即TSS)随着任务的切换而走马灯似地换,而在Linux内核中却成了只更换TSS中的SS0和ESP0,而不更换TSS本身,也就是根本不更换TR的内容。这是因为,改变TSS中SS0和ESP0所化的开销比通过装入TR以更换一个TSS要小得多。因此,在Linux内核中,TSS并不是属于某个进程的资源,而是全局性的公共资源。在多处理机的情况下,尽管内核中确实有多个TSS,但是每个CPU仍旧只有一个TSS。