linux内核-X86CPU对中断的硬件支持

本博客不讨论严格意义上的中断响应全过程(比如说,怎样获得中断向量),而是着重讨论CPU在响应中断时,即在得到了中断向量以后,怎样进入相应的中断服务程序的过程。这是从操作系统的角度需要关心的问题。Intel x86 CPU支持256个不同的中断向量,这一点至今未变。可是,早期x86 CPU的中断响应机制非常原始、非常简单的。在实地址模式中,CPU把内存中从0开始的1K字节作为一个中断向量表。表中的每一个表项占四个字节,由两个字节的段地址和两个字节的位移组成。这样构成的地址便是相应中断服务程序的入口地址。这与16位实地址模式中的寻址方式也是一致的。但是,在这样的机制上是不能构筑现代意义的操作系统的,即使把16位寻址改成32位寻址,即使试下你了页式存储管理,也还是无济于事。原因在于,这个机制中并没有提供空间切换,或者说运行模式切换的手段。为了理解这一点,让我们来看看其他的CPU是怎也做的。读者也许知道,早期的UNIX是在PDP-11上实现的。PDP-11的CPU中有一个与x86的FLAGS寄存器相类似的控制状态寄存器,称为PSW(处理器状态字)。PSW中有一个位段决定了CPU的当前运行优先级和模式(系统或用户)。在用户程序中是不能通过直接修改PSW来达到调高优先级的目的的。在PDP-11的中断向量表中,每个表项由两部分组成,一部分是相应中断服务程序的入口地址,另一部分就是当CPU进入中断服务程序后的PSW。当然,中断向量表的内容只有当CPU处于系统模式时才能改变。当中断发生时,CPU从向量表中将PSW装入其控制状态寄存器,而将中断服务程序的入口地址装入程序计算器,从而达到既转入了相应的中断服务程序,又从一种运行模式切换到另一种运行模式(或优先级别)的双重目的。至于原来的PSW则随中断返回地址一起被压入堆栈,以便CPU从中断服务程序返回时能回到原来的运行模式。这样,就很自然地实现了运行状态的切换。CPU平时处于用户状态,无论是因为外部中断还是系统调用(由软件产生的中断),或是某种异常,都会通过中断向量表进入系统状态,执行完中断服务程序后返回时便又恢复原状,回到用户状态。相比之下,我们可以清楚地看到,x86实地址模式下的中断响应过程所缺少的就是类似于PDP-11对PSW的处理。

因此,Intel在实现保护模式时,对CPU的中断响应机制作了大幅度的修改。

首先,中断向量表中的表项从单纯的入口地址改成了类似于PSW加入口地址并且更为复杂的描述项,称为门(gate),意思是当中断发生时必须先通过这些门,才能进入相应的服务程序。但是,这样的门并不光是为中断而设的,只要想切换CPU的运行状态,即其优先级别,例如从用户的3级别进入系统的0级别,就都要通过一道门。而从用户态进入系统态的途径也并不只限于中断(或异常,或陷阱),还可以通过子程序调用指令CALL和转移指令JMP来达到目的。而且,当中断发生时不但可以切换CPU的运行状态并转入中断服务程序,还可以安排进行一次任务切换(所谓上下文切换),立即切换到另一个进程。因此在操作系统中可以设立一个中断服务程序(任务),每当中断发生时就切换到该进程。

按不同的用途和目的,CPU中一共有四种门,即任务门(task gate)、中断门(interrupt gate)、陷阱门(trap gate)以及调用门(call gate)。其中除任务门外其他三种门的结构基本相同,不过调用门并不是与中断向量表相联系的。

先看任务门,其大小为64位,结构如下图所示:

linux内核-X86CPU对中断的硬件支持_第1张图片

TSS段选择码的作用和段寄存器CS、DS等相似,通过GDT或LDT指向特殊的系统段中的一种,称为任务状态段(task state segment)TSS。TSS实际上是一个用来保存任务运行现场的数据结构,其中包括CPU中所有与具体进程有关的寄存器的内容(包含页面目录指针CR3),还包括了三个堆栈指针。中断发生时,CPU在中断向量表中找到相应的表项。如果此表项时一个任务门,并且通过了优先级别的检查,CPU就会将当前任务的运行现场保存在相应的TSS中,并将任务门所指向的TSS作为当前任务,将其内容装入CPU中的各个寄存器,从而完成了一次任务的切换。为此目的,CPU中又增设了一个任务寄存器TR,用来指向当前任务的TSS。在linux内核中,一个任务就是一个进程,但是进程的控制块,即task_struct结构中需要存放更多的信息。所以,从这个意义上讲,linux的进程又并不完全是Intel设计意图中的任务。读者后面就会看到,linux内核并不采用任务门作为进程切换的手段。通过任务门切换到一个新的任务并不是唯一的途径,例如在程序中也可以用CALL指令或JMP指令通过调用门达到同样的目的。DPL位段的作用后面还要讨论。

除任务门外,其余三种们的结构基本相同,每个门的大小也都是64位,见下图:

linux内核-X86CPU对中断的硬件支持_第2张图片

 三种门之间的不同之处在于3位的类型码。中断门的类型码是110,陷阱门的类型码是111,而调用门的类型码是100。与任务门相比,不同之处主要在于:在任务门中不需要使用段内位移,因为任务门并不指向某一个子程序的入口,TSS本身是作为一个段来对待的,而中断门、陷阱门和调用门则都要指向一个子程序,所以必须结合使用段选择码和段内位移。此外,任务门中相对于D标志位的位置上永远是0。

中断门和陷阱门在使用上的区别不在于中断时外部产生的或是CPU本身产生的,而是在于通过中断门进入中断服务程序时CPU会自动将中断关闭,也就是将CPU中EFLAGS寄存器的IF标志位清成0,以防嵌套中断的发生;而在通过陷阱门进入服务程序时则维持IF标志位不变。这就是中断门和陷阱门的唯一区别。

不管是什么门,都通过段选择码指向一个存储段。段选择码的作用于普通的段寄存器一样。我们在前面内存管理讲过,在保护模式下段寄存器的内容并不直接指向一个段的起始地址,而是指向由GDTR或LDTR决定的某个段描述表中的一个表项,所以才又称为段选择码,至于到底是由GDTR还是LDTR所指向的段描述表,则取决于段选择码中的一个TI标志位。在linux内核中,实际上只使用全局描述表GDT,而局部段描述表LDT只是在特殊应用中(主要是WINE)才使用。对于中断门、陷阱门和调用门来说,段描述表中的相应表项显然应该是一个代码度描述项。而任务门所指向的描述项,则是专门为TSS而设的TSS描述项。TSS描述项的结构与我们在内存管理所讲的基本上是相同的,但是bit44的s标志位为0,表示不是一般的代码段或数据段。

每个段描述项中都有一个DPL位段,即描述项优先级别位段。当CPU通过中断门找到一个代码段描述项,并进而转入相应的服务程序时,就把整个代码段描述项装入CPU中,而描述项的DPL就变成CPU的当前运行级别,称为CPL。这与我们在前面所说的PDP-11在中断时从向量表中同时装入PSW和服务程序入口地址是一致的。可是,在中断门中也有一个DPL,那是干什么用的呢?这就是要讲到i386的保护模式中对运行和访问级别进行检查对比的机制了。

Intel在i386CPU中实现了一套可谓是复杂的出奇的优先级别检验机制。我们在这里只根据linux内核的实现介绍其中一部分。由于linux内核避开了这套机制中最复杂的部分,例如不使用任务门,基本上也不使用调用门(不过为了兼容性的要求趋势支持通过调用门来进入系统调用,但不是主流),再说这里我们只关心对代码段的访问,所以剩下的部分就不太复杂了。

当通过一条INT指令进入一个中断服务程序时,在指令中给出一个中断向量。CPU先根据该向量与CPU的CPL相比,CPL必须小于DPL,也就是优先级别不低于DPL,才能穿过这扇门,不过如果中断是由外部产生或者时因CPU异常而产生的话,那就免去了这一层检验。穿过了中断门之后,还要进一步将目标代码段描述项中的DPL与CPL比较,目标段的DPL必须小于或等于CPL。也就是说,通过中断门时只允许保持或者提升CPU的运行级别,而不允许降低其运行级别。这两个环节中的任何一个失败都会产生一次全面保护异常(general protection exception)。

进入中断服务程序时,CPU要将当前EFLAGS寄存器的内容以及返回地址压入堆栈,返回地址是由段寄存器CS的内容和取指令指针EIP的内容共同组成的。如果中断是由异常引起的,则还要将一个表示异常原因的出错代码也压入堆栈。进一步,如果中断服务程序的运行级别,也就是目标代码段的DPL,与中断发生时的CPL不同,那就要引起更换堆栈。前面提到过,TSS结构中除所有常规的寄存器(包括当前的SS和ESP)外,还有三个额外的堆栈指针(SS和ESP)。这三个额外的堆栈指针分别用于当CPU在目标代码段中的运行级别为0,1以及2时。所以,CPU根据寄存器TR的内容找到当前TSS结构,并根据目标代码段的DPL,从这TSS结构中取出新的堆栈指针(SS和ESP),并装入其堆栈寄存器SS和堆栈指针(寄存器)ESP,达到更换堆栈的目的。在这种情况下,CPU不但要将EFLAGS、返回地址以及出错代码压入堆栈,还要先将原来的堆栈指针也压入堆栈(新堆栈)。下面的示意图也许有助于理解。

linux内核-X86CPU对中断的硬件支持_第3张图片

具体到linux内核。当中断发生在用户状态、也就是CPU在用户空间中运行时,由于用户态的运行级别为3,而在内核中的中断服务程序的运行级别为0,所以会引起堆栈的更换。也就是说,从用户堆栈切换到系统堆栈。而当中断发生在系统空间时,也就是CPU在内核中运行时,则不会更换堆栈。

最后,在保护模式下,中断向量表在内核中的位置也不再限于从地址0开始的地方,而是像GDT和LDT那样可以放在内存中的任何地方。为此目的,在CPU中又增设了一个寄存器IDTR,指向当前中断向量表LDT,或者说当前中段描述表。

下面的示意图说明了i386保护模式下的中断机制在采用中断门或陷阱门时的结构。

linux内核-X86CPU对中断的硬件支持_第4张图片

实际的i386系统结构中的有关机制比上面讲的还要复杂,我们略去了其中与linux内核实现无关的内容。这也从另一个角度说明,对于像linux这样的操作系统(事实证明功能最强,并且最稳定的系统之一)来说,i386系统结构中的许多内容时不必要的,甚至是画蛇添足的,难怪有些学者批评Intel将i386的系统结构过于复杂化了。当然,也有可能将来会出现一些新的技术, 从而证明Intel是有远见的,我们拭目以待。如果说,在能达到相同目标的前提下简单就是美,那么i386系统结构显然是不美的。而相比之下,linux内核的实现倒确实是一种美。当然,不管怎么说,i386的系统结构能够满足像linux这样的现代操作系统的需要,却是毫无疑义的。

你可能感兴趣的:(中断,异常和系统调用,linux内核,1024程序员节)