本文基于2.6.11内核简单介绍了中断处理的过程。本文是一个概述性质的整理,可能没有对每段代码有详细的分析,但希望读者看完之后对整个过程有大致了解。
整个中断大致的过程(注:本处并不仅仅指中断处理程序)可以描述如下:
硬件中断==>
CPU在指令周期的最后检测到有中断==>
中断应答时序提供8位矢量(中断号)==>
根据IDTR找到IDT表(idt_table)==>
再根据中断号在IDT表中找到对应的描述符(irq_desc_t)==>
根据描述符,调用对应的IRQ0xNN_interrupt,实际上就是调用common_interrupt,实际上就是调用do_IRQ函数。
下面是详细的步骤:
(1)中断向量表的第一次初始化
内核初始化时,用arch/i386/kernel/head.S中的setup_idt宏用同样的中断门(中断门包含段选择符和中断处理程序的段内偏移量,即可以简单认为就是中断处理程序的地址。在系统第一次初始化时,这个中断门是一个宏ignore_int的地址)初始化中断向量表idt_table。初始化完成后,256个表项都一样。在2.6.29内核中该文件为arch/x86/kernel/head_32.S。
(2)产生interrupt[]数组
在arch/i386/kernel/entry.S中建立一个跳转表:interrupt数组,该数组有NR_IRQS个元素。
void (*interrupt[NR_IRQS])(void) = {
IRQLIST_16(0x0),
#ifdef CONFIG_X86_IO_APIC
IRQLIST_16(0x1), IRQLIST_16(0x2), IRQLIST_16(0x3),
IRQLIST_16(0x4), IRQLIST_16(0x5), IRQLIST_16(0x6), IRQLIST_16(0x7),
IRQLIST_16(0x8), IRQLIST_16(0x9), IRQLIST_16(0xa), IRQLIST_16(0xb),
IRQLIST_16(0xc), IRQLIST_16(0xd)
#endif
#define IRQ(x,y) \
IRQ##x##y##_interrupt
#define IRQLIST_16(x) \
IRQ(x,0), IRQ(x,1), IRQ(x,2), IRQ(x,3), \
IRQ(x,4), IRQ(x,5), IRQ(x,6), IRQ(x,7), \
IRQ(x,8), IRQ(x,9), IRQ(x,a), IRQ(x,b), \
IRQ(x,c), IRQ(x,d), IRQ(x,e), IRQ(x,f)
从定义可以看出:IRQLIST_16是一个含有24个中断号的表(16进制下的16实际上是24)。如果定义了高级的中断控制器APIC,那么会有224个中断号(0xd);如果没有定义,那么只有24个中断号。
数组中元素定义如下:经过宏变换以后,interrupt[ i ]实际上就是IRQ0xNN_interrupt,其中NN代表十六进制数,如IRQ0x1_interrupt,IRQ0x2_interrupt等。
数组实际上是由一个ENRTY(name)宏定义的(ENTRY宏表明name是全局变量):
.previous
vector=0
ENTRY(irq_entries_start)
.rept NR_IRQS //表示循环NR_IRQS 到.endr截止
ALIGN
1: pushl $vector-256
jmp common_interrupt
.data
.long 1b
.text
vector=vector+1
.endr
每个IRQ0xNN_interrupt所做的事情就是把向量号压入栈,然后跳转到common_interrupt(通用中断处理程序,第4小节中详细解释)。
(3)中断向量表的第二次初始化
interrupt数组生成之后,内核进行第二次中断向量表idt_table的初始化。在/init/main.c中调用了init_IRQ函数(定义在arch\i386\kernel\i8259.c中),函数中有如下的循环:
int vector = FIRST_EXTERNAL_VECTOR + i;
if (i >= NR_IRQS)
break;
if (vector != SYSCALL_VECTOR)
set_intr_gate(vector, interrupt[i]); //使用interrupt数组初始化中断门
}
而set_intr_gate函数实际上就是初始化idt_table的函数,其定义如下:
{
_set_gate(idt_table+n,14,0,addr);
}
因此最终效果就是将interrupt数组对应的每一项地址放到了idt_table中。所以interrupt[]数组只是一个中间变量。最后CPU查找时仍然是查找idt_table。
(4)通用中断处理程序
当每一个中断到来时,都会执行下面的通用中断处理程序(在interrupt数组中的粗体):
SAVE_ALL // 1
movl %esp,%eax //
call do_IRQ // 2+3
jmp ret_from_intr // 4
不管引起中断的设备是什么,所有的I/O中断处理程序都执行四个相同的基本操作:
这里的4个操作就对应上面common_interrupt宏中的四条指令:SAVE_ALL宏就是将大部分的寄存器的值保存到内核中断栈中(中断都是在内核态发生的,这个宏的具体代码可以很容易的找到);然后下一行将参数入栈供do_IRQ函数调用;do_IRQ函数则执行了大部分的中断处理工作,包括给PIC发送应答以及执行本IRQ号的中断服务例程;最后一条则跳到ret_from_intr()的地址。
(5)中断处理函数:do_IRQ/__do_IRQ/handle_IRQ_event
我们知道,CPU要根据得到的IRQ号到中断向量表中查找中断描述符,以便调用相应的中断处理函数。因此,中断描述符irq_desc_t(读者可以回忆一下进程描述符task_struct)也一一对应的形成了一个数组irq_desc[]。描述符中最重要的两部分是handler对象(代表硬件PIC)和action对象(代表中断服务例程ISR)。请注意:中断服务例程ISR不同于上面说到的通用中断处理程序和中断处理函数。中断服务例程是指由发起中断的设备的驱动程序注册到系统中,当发生中断时由CPU调用的一个程序,是由设备或者用户定义的;而通用中断处理程序和中断处理函数都是Linux内核所默认执行的。
中断处理程序do_IRQ除了做一些准备工作以及切换到内核栈以外,最主要的工作调用__do_IRQ来完成,需要把IRQ号作为参数(irq,保存在eax寄存器中)传递给它。
__do_IRQ函数的主要工作有:
spin_lock(&(irq_desc[irq].lock));
// 向PIC发出应答信号
irq_desc[irq].handler->ack(irq);
// 当前状态既不是IRQ_REPLAY:The IRQ line has been disabled but the previous IRQ occurrence has not yet been acknowledged to the PIC
// 也不是IRQ_WAITING:The kernel is using the IRQ line while performing a hardware device probe; moreover, the corresponding interrupt has not been raised
irq_desc[irq].status &= ~(IRQ_REPLAY | IRQ_WAITING);
// 当前状态为IRQ_PENDING: An IRQ has occurred on the line; its occurrence has been acknowledged to the PIC, but it has not yet been serviced by the kernel
irq_desc[irq].status |= IRQ_PENDING;
if (!(irq_desc[irq].status & (IRQ_DISABLED | IRQ_INPROGRESS)) // 如果当前状态不是IRQ_DISABLED或者 IRQ_INPROGRESS
&& irq_desc[irq].action) { // 有对应的处理函数
irq_desc[irq].status |= IRQ_INPROGRESS;// 设置当前当前状态为IRQ_INPROGRESS : A handler for the IRQ is being executed
do {
irq_desc[irq].status &= ~IRQ_PENDING;// 设置当前状态不是IRQ_PENDING,因为下面要开始处理了
spin_unlock(&(irq_desc[irq].lock));
handle_IRQ_event(irq, regs, irq_desc[irq].action); // 处理事件:执行其action函数指针指向的函数
spin_lock(&(irq_desc[irq].lock));
} while (irq_desc[irq].status & IRQ_PENDING); // 如果当前状态还是IRQ_PENDING循环继续
irq_desc[irq].status &= ~IRQ_INPROGRESS; // 设置当前状态不是IRQ_INPROGRESS
}
irq_desc[irq].handler->end(irq);
spin_unlock(&(irq_desc[irq].lock));
在循环处理IRQ请求的时候,最开始要设置状态为IRQ_INPROGRESS同时不是IRQ_PENDING,这个循环处理IRQ请求的过程在当前状态是IRQ_PENDING则一直进行下去,当该循环处理完毕之后, 再将状态设置为IRQ_INPROGRESS。
其中真正执行ISR的是上面粗体标出的handle_irq_event函数,定义如下:
struct irqaction *action)
{
int ret, retval = 0, status = 0;
if (!(action->flags & SA_INTERRUPT)) //如果穿越中断门,而又没设置SA_INTERRUPT标志,这里需要允许中断
local_irq_enable();
do {
ret = action->handler(irq, action->dev_id, regs);
if (ret == IRQ_HANDLED)
status |= action->flags;
retval |= ret;
action = action->next;
} while (action);
if (status & SA_SAMPLE_RANDOM)
add_interrupt_randomness(irq);
local_irq_disable();
return retval;
}
由于系统中可能有很多个设备共享同一个IRQ号,它们在注册到系统中时,每个IRQ号的ISR(下图中的irqaction)就形成了一个链表。
所以handle_irq_event函数中使用了一个do/while循环遍历了这个链表,并执行每一个注册的ISR:action->handler()。其中此处的handler函数是一个函数指针,指向设备注册的那个函数。