通过上面介绍,中断描述符表IDT已被初始化,并具有相应的内容;对于外部中断,还要建立中断请求队列和执行中断处理程序。
一、中断和异常的硬件处理
从硬件角度来看CPU如何处理中断和异常。假定已初始化内核,CPU已从实模式切换到保护模式。
当CPU执行当前指令后,寄存器CS和EIP中包含的内容是下一条将要执行指令的虚地址。对下一条指令执行前,CPU先要判断在执行当前指令的过程中是否发生中断或异常。如果发生一个中断或异常,那么CPU做如下事情。
(1)确定发生中断或异常的向量i(在0~255之间)。
(2)通过IDTR寄存器找到IDT,读取IDT表的第i项(或第i个门)。
(3)分两步进行有效性检查:首先是段级检查,将CPU的当前特权级CPL(存放在CS寄存器的最低两位)与IDT表中第i项段选择符中的DPL比较,如果DPL(3)大于CPL(0),产生一个“通用保护”异常,因为中断处理程序的特权级大于等于产生中断的进程的特权级。这种情况发生的可能性不大,因为中断处理程序一般运行在内核态,特权级为0。再次是门级检查,把CPL与IDT中第i个门的DPL比较,如果CPL(0)小于DPL(3),那么CPU不能穿过这个门,则产生一个“通用保护”异常,这是为了避免用户应用程序访问特殊的中断门或陷阱门。
备注:门级检查针对一般的用户程序,不包括外部I/O产生的中断或CPU内部产生的异常,如果产生中断或异常,就免去门级检查。
(4)检查特权级是否发生改变。当中断发生在用户态(特权级为3),而中断处理程序运行在内核态(特权级为0),特权级发生变化,引起堆栈切换,从用户态堆栈切换到内核态堆栈。当中断发生在内核态时,即CPU运行在内核中时,则不会切换堆栈,如图5.4所示。
总结如下:
从图5.4看出,当从用户态堆栈切换到内核态堆栈时,先把用户态堆栈的值压入中断处理程序的内核态堆栈中,同时把EFLAGS寄存器自动压入堆栈,然后把中断进程的返回地址压入堆栈。如果异常产生一个硬错误码,则将这个硬错误码也保存在堆栈中。如果特权级没有发生变化,压入堆栈的内容如图5.4(b)所示。此时,CS:EIP的值是IDT表中第i项门描述符的段选择符和偏移量的值,CPU跳转到中断或异常处理程序。
二、中断请求队列的建立
由于硬件限制,很多外部设备必须共享中断线,例如,一些PC(个人电脑)配置可以把同一条中断线分配给网卡和图形卡。由此看来,让每个中断源都必须占用一条中断线不现实。所以,仅仅用中段描述符表IDT并不能提供中断产生的所有信息,内核必须对中断线给出进一步的描述。在Linux中,为每个中断请求IRQ设置一个队列,即中断请求队列。
1、中断服务程序与中断处理程序
这里提到的中断服务程序与中断处理程序概念不同。在Linux中15条中断线对应15个中断处理程序,其名字依次为IRQ0x00_interrupt(),IRQ0x01_interrupt(),......,IRQ0x0f_interrupt()。具体来说,中断处理程序相当于某个中断向量的总处理程序,例如IRQ0x05_interrupt()是中断号为5的总处理程序,如果5号中断由网卡和图形卡共享,则网卡和图形卡分别有其对应的中断服务程序。
2、中断共享的数据结构
为让多个设备能共享一条中断线,内核定义一个irqaction的数据结构:
typedef irqreturn_t (*irq_handler_t)(int, void *); // 定义函数指针类型
struct irqaction {
irq_handler_t handler; //用户注册的中断服务程序,中断发生时会执行这个中断服务程序
unsigned long flags; //中断标志,注册中断时设置,比如上升沿中断,下降沿中断等
cpuma