3.4.4 中断处理程序的执行

    从前面的介绍,我们已经知道了 i386的中断机制及有关的初始化工作。现在,我们可以从中断请求的发生到CPU的响应,再到中断处理程序的调用和返回,沿着这一思路走一遍,以体会Linux内核对中断的响应及处理。

   假定外设的驱动程序都已完成了初始化工作,并且已把相应的中断服务例程挂入到特定的中断请求队列。 又假定当前进程正在用户空间运行(随时可以接受中断),且外设已产生了一次中断请求。当这个中断请求通过中断控制器8259A到达CPU的中断请求引线INTR时(参看图3.1,CPU就在执行完当前指令后来响应该中断。

   CPU从中断控制器的一个端口取得中断向量I,然后根据I从中断描述符表IDT中找到相应的表项,也就是找到相应的中断门。因为这是外部中断,不需要进行“门级”检查,CPU就可以从这个中断门获得中断处理程序的入口地址,假定为IRQ0x05_interrupt。因为这里假定中断发生时CPU运行在用户空间(CPL3),而中断处理程序属于内核(DPL0),因此,要进行堆栈的切换。也就是说,CPUTSS中取出内核栈指针,并切换到内核(此时还为空)。当CPU进入IRQ0x05_interrupt时,内核如图3.5除用户栈指针、EFLAGS的内容以及返回地址外再无其他内容。另外,由于CPU进入的是中断门(而不是陷阱门),因此,这条中断线已被禁用,直到重新启用。

   我们用IRQn_interrupt来表示从IRQ0x01_interrupt IRQ0x0f_interrupt任意一个中断处理程序。这个中断处理程序实际上要调用do_IRQ(),而do_IRQ()要调用handle_IRQ_event()函数;最后这个函数才真正地执行中断服务例程(ISR)。图3.7给出它们的调用关系:

 

 

 

 


             

 

 

 

 

 

 

 

 

 

                            3.7中断处理函数的调用关系

1.中断处理程序IRQn_interrupt

我们首先看一下从IRQ0x01_interrupt IRQ0x0f_interrupt的这16个函数是如何定义的,在i8259.c中定义了如下宏:

#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_IRQ0x00BUILD_IRQ0x0fBUILD_IRQ宏是一段嵌入式汇编代码(/include/i386/hw_irq.h),为了有助于理解,我们把它展开成下面的汇编语言片段:

 

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次方。最大值为25628, 因此可以用一个字节来表示这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, &regs, 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_8259A_irq()来启用这条中断线。

·      如果这个中断有后半部分,就调用do_softirq()执行后半部分。