1.中断和异常的硬件处理
首先,从硬件的角度来看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)(CPU当前执行权限和中断处理程序权限比较),就产生一个“通用保护”异常(中断向量13),因为中断处理程序的特权级不能低于引起中断的程序的特权级。这种情况发生的可能性不大,因为中断处理程序一般运行在内核态,其特权级为0。然后是“门”级检查,把CPL与IDT中第i个门的DPL相比较,如果CPL大于DPL,也就是当前特权级(3)小于这个门的特权级(0)(CPU当前执行权限与门的执行权限比较),CPU就不能“穿过”这个门,于是产生一个“通用保护”异常,这是为了避免用户应用程序访问特殊的陷阱门或中断门。但是请注意,这种“门”级检查是针对一般的用户程序,而不包括外部I/O产生的中断或因CPU内部异常而产生的异常,也就是说,如果产生了中断或异常,就免去了“门”级检查。
(4)检查是否发生了特权级的变化。当中断发生在用户态(特权级为3),而中断处理程序运行在内核态(特权级为0),特权级发生了变化,所以会引起堆栈的更换。也就是说,从用户堆栈切换到内核堆栈。而当中断发生在内核态时,即CPU在内核中运行时,则不会更换堆栈。
2.中断请求队列的建立
由于硬件的限制,很多外部设备不得不共享中断线,例如,一些PC配置可以把同一条中短线分配给网卡和图形卡。由此来看,让每个中断源都必须占用一条中段线是不现实的。所以,仅仅用中断描述符表并不能提供中断产生的所有信息,内核必须对中断线给出进一步的描述。在linux设计中,专门为每个中断请求IRQ设置了一个队列,这就是所谓的中断请求队列。
(1)中断服务程序与中断处理程序
中断服务程序与中断处理程序并不同,在Linux中,15条中断线对应15个终端处理程序,其名依次是IRQ0x00_interrupt()~IRQ0x0f_interruption(),中断处理程序相当于某个中断向量的总处理程序,比如IRQ0x05_interrupt()对应 中断号为5(向量为32+5)的总处理程序,如过这个5号中断由网卡和图形卡共享,则他们分别有自己的中断服务程序。
(2)中断线共享的数据结构
为了让多个设备可以共享一条中断线,内核设置了一个叫irqaction的数据结构:
typedef irqreturn_t (*irq_handler_t)(int , void *);
struct irqaction {
irq_handler_t handler; //指向一个中断服务程序,该函数有两个参数,第一个参数为中段号IRQ,第二个参数一般为dev_id(唯一的标识某个设备的设备号)
unsigned long flags; //用一组标志描述中断线与I/O设备之间的关系
cpumask_t mask;
const char *name; //I/O设备名
void *dev_id; //指定I/O设备的主设备号和次设备号
struct irqaction *next; //指向irqaction描述符链表的下一个元素,前提是flags为IRQF_SHARED标志(允许其他设备共享中段线)。
int irq;
...
};
共享同一中断线的每个硬件设备都有其对应的中断服务程序,链表中的每个元素就是对相应设备及中断服务程序的描述。
一条中断线对应一条由该结构体组成的链表,若共享则链表存储了每个设备的中断服务程序,若不共享则只有一个结构体。
(3)注册中断服务程序
在IDT表初始化完成之初,每个中断服务队列还为空。此时,即使打开中断且某个中断发生了,也得不到实际的服务。因为此时中断服务程序还没有挂入中断请求队列。所以,在设备驱动程序的初始化阶段,必须通过request_irq()函数将相对应中断服务程序挂入中断请求队列,也就是对其进行注册
int request_irq(unsigned int irq, void (*handler)(int, void *, struct pt_regs *), unsigned long irqflags, const char * devname, void *dev_id)
第一个参数irq表示要分配的中断号。
第二个参数handler是一个指针,指向处理这个中断的实际中断服务程序。
第三个参数irqflags对应irqaction中的flags。
第四个参数devname是与中断相关的设备名字。这些名字会被/proc/irq和/proc/interrupt文件使用。
第五个参数dev_id主要用于共享中断线。如两个设备共享中断线则需要dev_id作为唯一标识信息,以便从众多的中断服务程序中找到指定的那一个。
这里要说明的是,在驱动程序初始化或者在设备第一次打开的时候,事先要调用request_irq()函数,以申请使用参数中指定的中断请求号irq,另一参数handler指的是要挂入到中断请求队列中的中断服务程序。
注意,request_irq()函数可能会睡眠,因此,不能在中断上下文或其他不允许阻塞的代码中调用该函数。在睡眠不安全的上下文中调用request_irq()函数是一种常见的错误。
(4)注销中断函数
卸载驱动程序时,需要注销相应的中断处理服务程序,并释放中断线。可以调用void free_irq(unsigned int irq, void * dev_id)来释放中断线。
如果指定的中断线不是共享的,那么,该函数删除处理程序的同时将禁用这条中断线。如果中断线是共享的,则仅删除dev_id所对应的服务程序,而这条中断线本身只有在删除了最后一个服务程序时才会被禁用。由此可以看出为什么唯一的dev_id如此重要。对于共享的中断线,需要一个唯一的信息来区分其上面的多个服务程序,并让free_irq()仅仅删除指定的服务程序。
3.中断处理程序的制定
假定外设的驱动程序都已完成了初始化工作,并且已把相应的中断服务例程挂入到特定的中断请求队列。 又假定当前进程正在用户空间运行(随时可以接受中断),且外设已产生了一次中断请求。当这个中断请求通过中断控制器8259A到达CPU的中断请求引线INTR时,CPU就在执行完当前指令后来响应该中断。
CPU从中断控制器的一个端口取得中断向量I,然后根据I从中断描述符表IDT中找到相应的表项,也就是找到相应的中断门。因为这是外部中断,不需要进行“门级”检查,CPU就可以从这个中断门获得中断处理程序的入口地址,假定为IRQ0x05_interrupt。因为这里假定中断发生时CPU运行在用户空间(CPL=3),而中断处理程序属于内核(DPL=0),因此,要进行堆栈的切换。也就是说,CPU从TSS中取出内核栈指针,并切换到内核栈(此时栈还为空)。当CPU进入IRQ0x05_interrupt时,内核栈中除用户栈指针、EFLAGS的内容以及返回地址外再无其他内容。另外,由于CPU进入的是中断门(而不是陷阱门),因此,这条中断线已被禁用,直到重新启用。
这个中断处理程序实际上要调用do_IRQ(),而do_IRQ()要调用handle_IRQ_event()函数(运行挂在这条中断线上的所有中断服务程序);最后这个函数才真正地执行中断服务例程(ISR)。
(1)中断处理程序IRQn_interrupt
一个中断处理程序主要包含以下两条语句
IRQn_interrupt:
pushl $n-256
jmp common_interrupt
其中第一条语句把中断号减256的结果保存在栈中,这是每个中段处理程序唯一的不同之处。然后,所有的中断处理程序都跳到一段相同的代码common_interrupt。
common_interrupt:
SAVE_ALL
call do_IRQ
jmp ret_from_intr
SVAE_AL L宏把中断处理程序会使用的所有CPU寄存器都保存在栈中。然后,BUILD_COMMON_IRQ 宏调用do_IRQ( )函数,因为通过CALL调用这个函数,因此,该函数的返回地址被压入栈。当执行完do_IRQ( ),就跳转到ret_from_intr( )地址。
(2)do_IRQ()函数
do_IRQ()这个函数处理所有的外设的中断请求。该函数对中断请求队列的处理主要是通过handle_IRQ_event()函数完成的,handle_IRQ_event()函数的主要代码片段为:
retval = 0;
do {
status |= action->flags;
retval |= action->handler(irq, action->dev_id, regs);
action = action->next;
} while (action);
这个循环依次调用请求队列中的每个中断服务程序。中断服务程序都是在关中断的条件下进行(不包括非屏蔽中断),这也是为什么CPU再穿过中断门时自动关闭中断的原因了。但是,关中断时间绝对不能太长,否则就可能丢失其他重要的中断。也就是说,中断服务程序应该处理最紧急的事情,而把剩下的事情交给另外一部分来处理,即下半部来处理。
4.从中断返回
do_IRQ()这个函数处理所有外设的中断请求。当这个函数执行时,内核栈从栈顶到栈底包括:
· do_IRQ( )的返回地址
· 由SAVE_ALL 推进栈中的一组寄存器的值
· ORIG_EAX(即n-256)
· CPU自动保存的寄存器
内核栈顶包含的就是do_IRQ()的返回地址,这个地址指向ret_from_intr。实际上,ret_from_intr是一段汇编语言的入口点。而实际上中断,异常及系统调用的返回是放在一起实现的,因此,我们常常以函数的形式提到下面三个入口点。
(1)ret_from_intr():终止中断处理程序。
(2)ret_from_sys_call():终止系统调用,即0x80引起的异常。
(3)ret_from_exception():终止除了0x80的所有异常。