分类: LINUX
2012-05-27 18:59:38
讲到中断的硬件环境,我们先从著名的IRQ信号谈起。每个能够发出中断请求的硬件设备控制器都有一条名为IRQ的输出线。所有现有的IRQ线都与一个名为可编程中断控制器(PIC)的硬件电路的输入引脚相连,可编程中断控制器执行下列动作:
1. 监视IRQ线,检查产生的信号。如果有条或两条以上的IRQ线上产生信号,就选择引脚编号较小的IRQ线。
2. 如果一个引发信号出现在IRQ线上:
a) 把接收到的引发信号转换成对应的向量号(参见上一篇博文)。
b) 把这个向量存放在中断控制器的一个I/O端口(0x20、0x21),从而允许CPU通过数据总线读此向量。
c) 把引发信号发送到处理器的INTR引脚,即产生一个中断。
d) 等待,直到CPU通过把这个中断信号写进可编程中断控制器的一个I/O端口来确认它;当这种情况发生时,清INTR线。
3. 返回第1步。
可以有选择地禁止某条IRQ线。因此,可以对PIC编程从而禁止IRQ,也就是说,可以告诉PIC停止对给定的IRQ线发布中断,或者激活它们。这个特点被大多数中断处理程序使用,因为这允许中断处理程序逐次地处理同一类型的IRQ。
这里要注意一下,有选择地激活/禁止IRQ线不同于上一篇博文中提到的可屏蔽中断的全局屏蔽/非屏蔽,一个是对PIC编程,一个是对CPU编程(汇编)。
当eflags寄存器的IF标志被清0时,由PIC发布的每个可屏蔽中断都由CPU暂时忽略。cli和sti汇编指令分别清除和设置该标志。
传统的PIC是由两片8259A风格的外部芯片以“级联”的方式连接在一起的。每个芯片可以处理多达8个不同的IRQ输入线。因为从PIC的INT输出线连接到主PIC的IRQ2引脚,因此,可用IRQ线的个数为15。
从P3开始,Intel引入了一种I/O高级可编程控制器(APIC),以替代老式8259A可编程中断控制器。首先,为了支持老式操作系统,新系统将两
个级联的老式PIC
8259A保留了下来。其次,新的80x86CPU的控制单元都含有一个本地的APIC。这不奇怪,为的就是支持多CPU或者多核的体系。每个本地
APIC都有32位寄存器,一个内部时钟,一个本地定时设备以及为本地APIC中断保留的两条额外IRQ线LINT0和LINT1。所有本地APIC都连
接到同一个外部I/O
APIC,形成一个多APIC的系统。对于APIC,其本质跟传统8259A其实是一样的,只是多了一层在多CPU上分发外部中断请求的I/O
APIC,这里不在话下。
我们前边谈到,中断系统的主要目的就是改变程序的执行顺序。具体如何改变?也就是一个调用中断处理程序的过程,这里我们就要引出中断描述符表了。中断描述
符表(Interrupt Descriptor
Table,IDT)是一个系统表,它与每一个中断或异常向量相联系,每一个向量在表中有相应的中断或异常处理程序的入口地址。内核在允许中断发生前,必
须适当地初始化IDT。
IDT是一个向量表,表中的每一项对应一个中断或异常向量,每个向量由8个字节组成。因此,最多需要256 ×
8=2048字节来存放IDT,正好是半个页面。那么,这个IDT到底放在什么位置呢?有了idtr
CPU寄存器,IDT就可以位于内存的任何地方。idtr寄存器指定IDT的线性基地址及其限制(最大长度)。在允许中断之前,必须用lidt汇编指令初
始化idtr。
IDT包含三种类型的描述符,下图显示了每种描述符中的64位(8个字节)的含义。尤其值得注意的是,在40~43位的Type字段的值表示三种描述符的类型。
这些描述符是:
任务门(task gate):当中断信号发生时,必须取代当前进程的那个进程的TSS选择符存放在任务门中。
中断门(interrupt gate):包含段选择符和中断或异常处理程序的段内偏移量。当控制权转移到一个适当的段时,处理器清IF标志,从而关闭将来会发生的可屏蔽中断。
陷阱门(Trap gate):与中断门相似,只是控制权传递到一个适当的段时处理器不修改IF标志。
这里再联系上一篇博文强调一下,Linux利用中断门处理中断,利用陷阱门处理异常
。
好了,有了前面的基本概念后,我们现在开始分析CPU控制单元如何处理中断和异常。我们假定内核已被初始化,因此,CPU在保护模式下运行。当执行了一条
指令后,CS和eip这对寄存器包含下一条将要执行的指令的逻辑地址。在处理那条指令之前,控制单元会检查在运行前一条指令时是否已经发生了一个中断或异
常。如果发生了一个中断或异常,那么控制单元执行下列操作:
1. 确定与中断或异常关联的向量i (0 ≤ i ≤ 255)。
2. 读由idtr寄存器指向的 IDT表中的第i项(在下面的分析中,我们假定IDT表项中包含的是一个中断门或一个陷阱门)。
3. 从gdtr寄存器获得GDT的基地址,并在GDT中查找,以读取IDT表项中的选择符所标识的段描述符。这个描述符指定中断或异常处理程序所在段的基地址。
4.
确信中断是由授权的(中断)发生源发出的。首先将当前特权级CPL(存放在cs寄存器的低两位)与段描述符(存放在GDT中)的描述符特权级DPL比较,
如果CPL小于DPL,就产生一个“General
protection”异常,因为中断处理程序的特权不能低于引起中断的程序的特权。对于编程异常,则做进一步的安全检查:比较CPL与处于IDT中的门
描述符的DPL,如果DPL小于CPL,就产生一个“General
protection”异常。这最后一个检查可以避免用户应用程序访问特殊的陷阱门或中断门。
5. 检查是否发生了特权级的变化,也就是说,CPL是否不同于所选择的段描述符的DPL。如果是,控制单元必须开始使用与新的特权级相关的栈。通过执行以下步骤来做到这点:
i. 读tr寄存器,以访问运行进程的TSS段。
ii. 用与新特权级相关的栈段和栈指针的正确值装载ss和esp寄存器。这些值可以在TSS中找到(参见第三章的“任务状态段”一节)
iii. 在新的栈中保存ss和esp以前的值,这些值定义了与旧特权级相关的栈的逻辑地址。
6. 如果故障已发生,用引起异常的指令地址装载CS和eip寄存器,从而使得这条指令能再次被执行。
7. 在栈中保存eflags、CS及eip的内容。
8. 如果异常产生了一个硬件出错码,则将它保存在栈中。
9. 装载cs和eip寄存器,其值分别是IDT表中第i项门描述符的段选择符和偏移量字段。这些值给出了中断或者异常处理程序的第一条指令的逻辑地址。
控制单元所执行的最后一步就是跳转到中断或者异常处理程序。换句话说,处理完中断信号后,控制单元所执行的指令就是被选中处理程序的第一条指令。
中断或异常被处理完后,相应的处理程序必须产生一条iret指令,把控制权转交给被中断的进程,这将迫使控制单元:
1. 用保存在栈中的值装载CS、eip或eflags寄存器。如果一个硬件出错码曾被压入栈中,并且在eip内容的上面,那么,执行iret指令前必须先弹出这个硬件出错码。
2. 检查处理程序的CPL是否等于CS中最低两位的值(这意味着被中断的进程与处理程序运行在同一特权级)。如果是,iret终止执行;否则,转入下一步。
3. 从栈中装载ss和esp寄存器,因此,返回到与旧特权级相关的栈。
4.
检查ds、es、fs及gs段寄存器的内容,如果其中一个寄存器包含的选择符是一个段描述符,并且其DPL值小于CPL,那么,清相应的段寄存器。控制单
元这么做是为了禁止用户态的程序(CPL=3)利用内核以前所用的段寄存器(DPL=0)。如果不清这些寄存器,怀有恶意的用户态程序就可能利用它们来访
问内核地址空间。