从前面的介绍,我们已经知道了 i386的中断机制及有关的初始化工作。现在,我们可以从中断请求的发生到CPU的响应,再到中断处理程序的调用和返回,沿着这一思路走一遍,以体会Linux内核对中断的响应及处理。
假定外设的驱动程序都已完成了初始化工作,并且已把相应的中断服务例程挂入到特定的中断请求队列。 又假定当前进程正在用户空间运行(随时可以接受中断),且外设已产生了一次中断请求。当这个中断请求通过中断控制器
CPU从中断控制器的一个端口取得中断向量I,然后根据I从中断描述符表IDT中找到相应的表项,也就是找到相应的中断门。因为这是外部中断,不需要进行“门级”检查,CPU就可以从这个中断门获得中断处理程序的入口地址,假定为IRQ0x05_interrupt。因为这里假定中断发生时CPU运行在用户空间(CPL=3),而中断处理程序属于内核(DPL=0),因此,要进行堆栈的切换。也就是说,CPU从TSS中取出内核栈指针,并切换到内核栈(此时栈还为空)。当CPU进入IRQ0x05_interrupt时,内核栈如图3.5的,栈中除用户栈指针、EFLAGS的内容以及返回地址外再无其他内容。另外,由于CPU进入的是中断门(而不是陷阱门),因此,这条中断线已被禁用,直到重新启用。
我们用IRQn_interrupt来表示从IRQ0x01_interrupt
到IRQ0x
图3.7中断处理函数的调用关系
1.中断处理程序IRQn_interrupt
我们首先看一下从IRQ0x01_interrupt 到IRQ0x
#define BI(x,y) \
BUILD_IRQ(x##y)
#define BUILD_16_IRQS(x) \
BI(x,0) BI(x,1) BI(x,2) BI(x,3) \
BI(x,4) BI(x,5) BI(x,6) BI(x,7) \
BI(x,8) BI(x,9) BI(x,a) BI(x,b) \
BI(x,c) BI(x,d) BI(x,e) BI(x,f)
BUILD_16_IRQS(0x0)
经过gcc的预处理,宏定义BUILD_16_IRQS(0x0) 会被展开成BUILD_IRQ(0x00)至BUILD_IRQ(0x
IRQn_interrupt:
pushl $n-256
jmp common_interrupt
把中断号减256的结果保存在栈中,这就是进入中断处理程序后第一个压入堆栈的值,也就是堆栈中ORIG_EAX的值,如图3.6。这是一个负数,正数留给系统调用使用。对于每个中断处理程序,唯一不同的就是压入栈中的这个数。然后,所有的中断处理程序都跳到一段相同的代码common_interrupt。这段代码可以在BUILD_COMMON_IRQ 宏中找到,同样,我们略去其嵌入式汇编源代码,而把这个宏展开成下列的汇编语言片段:
common_interrupt:
SAVE_ALL
call do_IRQ
jmp ret_from_intr
SAVE_ALL宏已经在前面介绍过,它把中断处理程序会使用的所有CPU寄存器都保存在栈中。然后,BUILD_COMMON_IRQ 宏调用do_IRQ( )函数,因为通过CALL调用这个函数,因此,该函数的返回地址被压入栈。当执行完do_IRQ(
),就跳转到ret_from_intr(
)地址(参见后面的“从中断和异常返回)。
2. do_IRQ( )函数
do_IRQ()这个函数处理所有外设的中断请求。当这个函数执行时,内核栈从栈顶到栈底包括:
· do_IRQ( )的返回地址
· 由SAVE_ALL 推进栈中的一组寄存器的值
· ORIG_EAX(即n-256)
· CPU自动保存的寄存器
该函数的实现用到中断线的状态,下面给予具体说明:
#define IRQ_INPROGRESS 1 /* 正在执行这个IRQ的一个处理程序*/
#define IRQ_DISABLED 2 /* 由设备驱动程序已经禁用了这条IRQ中断线 */
#define IRQ_PENDING 4 /* 一个IRQ已经出现在中断线上,且被应答,但还没有为它提供服务 */
#define IRQ_REPLAY 8 /* 当Linux重新发送一个已被删除的IRQ时 */
#define IRQ_AUTODETECT 16 /* 当进行硬件设备探测时,内核使用这条IRQ中断线 */
#define IRQ_WAITING 32 /*当对硬件设备进行探测时,设置这个状态以标记正在被测试的irq */
#define IRQ_LEVEL 64 /* IRQ level triggered */
#define IRQ_MASKED 128 /* IRQ masked - shouldn't be seen again */
#define IRQ_PER_CPU 256 /* IRQ is per CPU */
这8个状态的前5个状态比较常用,因此我们给出了具体解释。另外,我们还看到每个状态的常量是2的幂次方。最大值为256(28), 因此可以用一个字节来表示这8个状态,其中每一位对应一个状态。
该函数在arch//i386/kernel/irq.c中定义如下:
asmlinkage unsigned int do_IRQ(struct
pt_regs regs)
{
/* 函数返回0则意味着这个irq正在由另一个CPU进行处理,
或这条中断线被禁用*/
int irq = regs.orig_eax & 0xff; /* 还原中断号 */
int
cpu = smp_processor_id();
/*获得CPU号*/
irq_desc_t *desc = irq_desc + irq; /*在irq_desc[]数组中获得irq 的描述符*/
struct irqaction
* action;
unsigned int status;
kstat.irqs[cpu][irq]++;
spin_lock(&desc->lock);
/*针对多处理机加锁*/
desc->handler->ack(irq);
/*CPU对中断请求给予确认*/
status =
desc->status & ~(IRQ_REPLAY | IRQ_WAITING);
status |= IRQ_PENDING; /* we _want_ to handle it */
action = NULL;
if (!(status & (IRQ_DISABLED | IRQ_INPROGRESS))) {
action = desc->action;
status &= ~IRQ_PENDING; /* we commit to
handling */
status |= IRQ_INPROGRESS; /* we are handling it
*/
}
desc->status = status;
if (!action)
goto out;
for (;;) {
spin_unlock(&desc->lock);
/*进入临界区*
handle_IRQ_event(irq, ®s, action);
spin_lock(&desc->lock); /*出临界区*/
if (!(desc->status & IRQ_PENDING))
break;
desc->status &= ~IRQ_PENDING;
}
desc->status &= ~IRQ_INPROGRESS;
out:
/*
* The ->end() handler has to deal with
interrupts which got
* disabled while the handler was running.
*/
desc->handler->end(irq);
spin_unlock(&desc->lock);
if (softirq_pending(cpu))
do_softirq(); /*处理软中断*/
return 1;
}
下面对这个函数进行进一步的讨论:
· 当执行到for (;;)这个无限循环时,就准备对中断请求队列进行处理,这是由handle_IRQ_event ()函数完成的。因为中断请求队列为一临界资源,因此在进入这个函数前要加锁。
· handle_IRQ_event ()函数的主要代码片段为:
if
(!(action->flags & SA_INTERRUPT))
__sti(); /*关中断*/
do {
status |= action->flags;
action->handler(irq, action->dev_id,
regs);
action = action->next;
} while (action);
__cli(); /*开中断*/
这个循环依次调用请求队列中的每个中断服务例程。中断服务例程及其参数已经在前面进行过简单描 述,至于更具体的解释将在驱动程序一章进行描述。
· 这里要说明的是,中断服务例程都在关中断的条件下进行(不包括非屏蔽中断),这也是为什么CPU在穿过中断门时自动关闭中断的原因。但是,关中断时间绝不能太长,否则就可能丢失其它重要的中断。也就是说,中断服务例程应该处理最紧急的事情,而把剩下的事情交给另外一部分来处理。即后半部分(bottom half)来处理,这一部分内容将在下一节进行讨论。
· 经验表明,应该避免在同一条中断线上的中断嵌套,内核通过IRQ_PENDING标志位的应用保证了这一点。当do_IRQ()执行到for (;;)循环时,desc->status 中的IRQ_PENDING的标志位肯定为0(想想为什么?)。当CPU执行完handle_IRQ_event ()函数返回时,如果这个标志位仍然为0,那么循环就此结束。如果这个标志位变为1,那就说明这条中断线上又有中断产生(对单CPU而言),所以循环又执行一次。通过这种循环方式,就把可能发生在同一中断线上的嵌套循环化解为“串行”。
· 不同的CPU不允许并发地进入同一中断服务例程,否则,那就要求所有的中断服务例程必须是“可重入”的纯代码。可重入代码的设计和实现就复杂多了,因此,Linux在设计内核时巧妙地“避难就易”,以解决问题为主要目标。
· 在循环结束后调用desc->handler->end()函数,具体来说,如果没有设置IRQ_DISABLED标志位,就调用低级函数enable_
· 如果这个中断有后半部分,就调用do_softirq()执行后半部分。