3.3.3中断请求队列的数据结构

 如前所述,在256个中断向量中,除了32个分配给异常外,还有224个作为中断向量。对于每个IRQLinux都用一个irq_desc_t数据结构来描述,我们把它叫做IRQ描述符,224IRQ形成一个数组irq_desc[],其定义在/include/linux/irq.h中:

 

 /*

  * This is the "IRQ descriptor", which contains various information

  * about the irq, including what kind of hardware handling it has,

  * whether it is disabled etc etc.

  *

  * Pad this out to 32 bytes for cache and indexing reasons.

  */

typedef struct {

         unsigned int status;            /* IRQ status */

         hw_irq_controller *handler;

         struct irqaction *action;        /* IRQ action list */

         unsigned int depth;            /* nested irq disables */

         spinlock_t lock;

} ____cacheline_aligned irq_desc_t;

 

extern irq_desc_t irq_desc [NR_IRQS];

 

编码作者对这个数据结构给出了一定的解释,“____cacheline_aligned”表示这个数据结构的存放按32字节(高速缓存行的大小)进行对齐,以便于将来存放在高速缓存并容易存取。下面对这个数据结构的各个域给予描述:

 

status

描述IRQ中断线状态的一组标志(在irq.h中定义),其具体含义及应用将在do_IRQ()函数中介绍:

handler

    指向hw_interrupt_type描述符,这个描述符是对中断控制器的描述,下面会给出具体解释。

action

指向一个单向链表的指针,这个链表就是对中断服务例程进行描述的irqaction结构,后面将给予具体描述。

depth

如果启用这条IRQ中断线,depth则为0,如果禁用这条IRQ中断线不止一次,则为一个正数。每当调用一次disable_irq(  ),该函数就对这个域的值加1;如果depth等于0,该函数就禁用这条IRQ中断线。相反,每当调用enable_irq(  )函数时,该函数就对这个域的值减1;如果depth变为0,该函数就启用这条IRQ中断线。

 

1IRQ描述符的初始化

    在系统初始化期间,init_ISA_irqs()函数对IRQ数据结构(或叫描述符)的域进行初始化(参见i8258.c)

 

    for (i = 0; i < NR_IRQS; i++) {

                 irq_desc[i].status = IRQ_DISABLED;

                 irq_desc[i].action = 0;

                irq_desc[i].depth = 1;

 

                if (i < 16) {

                        /*

                          * 16 old-style INTA-cycle interrupts:

                         */

                         irq_desc[i].handler = &i8259A_irq_type;

                } else {

                        /*

                         * 'high' PCI IRQs filled in on demand

                        */

                         irq_desc[i].handler = &no_irq_type;

               }

        }

 

从这段程序可以看出,初始化时,让所有的中断线都处于禁用状态;每条中断线上还没有任何中断服务例程(action0);因为中断线被禁用,因此depth1;对中断控制器的描述分为两种情况,一种就是通常所说的8259A,另一种是其它控制器。

然后,更新中断描述符表IDT,如3.2.3节所述,用最终的中断门来代替临时使用的中断门。

 

2.中断控制器描述符hw_interrupt_type  

 

这个描述符包含一组指针,指向与特定中断控制器电路(PIC)打交道的低级I/O例程,定义如下:

/*

* Interrupt controller descriptor. This is all we need

* to describe about the low-level hardware.

*/

struct hw_interrupt_type {

        const char * typename;

        unsigned int (*startup)(unsigned int irq);

        void (*shutdown)(unsigned int irq);

       void (*enable)(unsigned int irq);

       void (*disable)(unsigned int irq);

       void (*ack)(unsigned int irq);

       void (*end)(unsigned int irq);

       void (*set_affinity)(unsigned int irq, unsigned long mask);

};

 

typedef struct hw_interrupt_type  hw_irq_controller;

 

Linux除了支持本章前面已提到的8259A芯片外,也支持其他的PIC电路,如SMP IO-APICPIIX4的内部 8259 PIC SGIVisual Workstation Cobalt (IO-)APIC。但是,为了简单起见,我们在本章假定,我们的计算机是有两片8259A PIC的单处理机,它提供16个标准的IRQ。在这种情况下,有16irq_desc_t描述符,其中每个描述符的handler域指向8259A_irq _type变量。对其进行如下的初始化:

 

struct hw_interrupt_type i8259A_irq_type = {

            "XT-PIC",

        startup_8259A_irq,

            shutdown_8259A_irq,

            do_8259A_IRQ,

        enable_8259A_irq,

            disable_8259A_irq

    };

 

    在这个结构中的第一个域XT-PIC是一个名字。接下来8259A_irq_type包含的指针指向五个不同的函数这些函数就是对PIC编程的函数。前两个函数分别启动和关闭这个芯片的中断线。但是,在使用8259A芯片的情况下,这两个函数的作用与后两个函数是一样的,后两个函数是启用和禁用中断线。后面在对do_IRQ描述时具体描述do_8259A_IRQ(  )函数。

 

3.中断服务例程描述符irqaction

    IRQ描述符中我们看到指针action的结构为irqaction,它是为多个设备能共享一条中断线而设置的一个数据结构。在include/linux/interrupt.h中定义如下:

 

  struct irqaction {

        void (*handler)(int, void *, struct pt_regs *);

         unsigned long flags;

        unsigned long mask;

        const char *name;

        void *dev_id;

        struct irqaction *next;

  };

 

这个描述符包含下列域。

handler

    指向一个具体I/O设备的中断服务例程。这是允许多个设备共享同一中断线的关键域。

flags

    用一组标志描述中断线与I/O设备之间的关系。

SA_INTERRUPT

中断处理程序必须以禁用中断来执行

SA_SHIRQ

该设备允许其中断线与其他设备共享。

SA_SAMPLE_RANDOM

可以把这个设备看作是随机事件发生源;因此,内核可以用它做随机数产生器。(用户可以从/dev/random /dev/urandom设备文件中取得随机数而访问这种特征)

SA_PROBE

内核在执行硬件设备探测时正在使用这条中断线。

name

I/O设备名(通过读取/proc/interrupts文件,可以看到,在列出中断号时也显示设备名。)

dev_id

    指定I/O设备的主设备号和次设备号

next

指向irqaction描述符链表的下一个元素。共享同一中断线的每个硬件设备都有其对应的中断服务例程,链表中的每个元素就是对相应设备及中断服务例程的描述。

 

4.中断服务例程

我们这里提到的中断服务例程(Interrupt Service Routine)与以前所提到的中断处理程序(Interrupt handler)是不同的概念。具体来说,中断处理程序相当于某个中断向量的总处理程序,例如IRQ0x05_interrupt(),是中断号5(向量为37)的总处理程序,如果这个5号中断由网卡和图形卡共享,则网卡和图形卡分别有其相应的中断服务例程。每个中断服务例程都有相同的参数:

IRQ:中断号;

dev_id: 设备标识符,其类型为void*

regs: 指向内核堆栈区的指针,堆栈中存放的是中断发生后所保存的寄存器,有关pt_regs结构的具体内容将在后面介绍。

    在实际中,大多数中断服务例程并不使用这些参数。

 

3.3.2中断请求队列的初始化

IDT表初始化完成之初,每个中断服务队列还为空。此时,即使打开中断且某个外设中断真的发生了,也得不到实际的服务。因为CPU虽然通过中断门进入了某个中断向量的总处理程序,例如IRQ0x05_interrupt(),但是,具体的中断服务例程(如图形卡的)还没有挂入中断请求队列。因此,在设备驱动程序的初始化阶段,必须通过request_irq()函数将对相应的中断服务例程挂入中断请求队列。

request_irq()函数的代码在/arch/i386/kernel/irq.c中:

 

  /*

  *      request_irq - allocate an interrupt line

  *      @irq: Interrupt line to allocate

  *      @handler: Function to be called when the IRQ occurs

  *      @irqflags: Interrupt type flags

  *      @devname: An ascii name for the claiming device

  *      @dev_id: A cookie passed back to the handler function

  *

  *      This call allocates interrupt resources and enables the

  *      interrupt line and IRQ handling. From the point this

  *      call is made your handler function may be invoked. Since

  *      your handler function must clear any interrupt the board

  *      raises, you must take care both to initialise your hardware

  *      and to set up the interrupt handler in the right order.

  *

  *      Dev_id must be globally unique. Normally the address of the

  *      device data structure is used as the cookie. Since the handler

  *      receives this value it makes sense to use it.

  *

  *      If your interrupt is shared you must pass a non NULL dev_id

  *      as this is required when freeing the interrupt.

  *

  *      Flags:

  *

  *      SA_SHIRQ                Interrupt is shared

  *

  *      SA_INTERRUPT            Disable local interrupts while processing

  *

  *      SA_SAMPLE_RANDOM        The interrupt can be used for entropy

  *

  */

 

 int request_irq(unsigned int irq,

                 void (*handler)(int, void *, struct pt_regs *),

                 unsigned long irqflags,

                 const char * devname,

                 void *dev_id)

 {

         int retval;

         struct irqaction * action;

 

 #if 1

         /*

         * Sanity-check: shared interrupts should REALLY pass in

         * a real dev-ID, otherwise we'll have trouble later trying

         * to figure out which interrupt is which (messes up the

         * interrupt freeing logic etc).

         */

         if (irqflags & SA_SHIRQ) {

                if (!dev_id)

                        printk("Bad boy: %s (at 0x%x) called us without a dev_id!\n", devname, (&irq)[-1]);

         }

#endif

 

         if (irq >= NR_IRQS)

                 return -EINVAL;

         if (!handler)

                return -EINVAL;

 

        action = (struct irqaction *)

                         kmalloc(sizeof(struct irqaction), GFP_KERNEL);

         if (!action)

                return -ENOMEM;

 

         action->handler = handler;

         action->flags = irqflags;

         action->mask = 0;

         action->name = devname;       action进行初始化

         action->next = NULL;

         action->dev_id = dev_id;

 

         retval = setup_irq(irq, action); 

         if (retval)

                  kfree(action);

         return retval;

}

编码作者对此函数给出了比较详细的描述。其中主要语句就是对setup_irq()函数的调用,该函数才是真正对中断请求队列进行初始化的函数(有所简化):

 

int setup_irq(unsigned int irq, struct irqaction * new)

{

         int shared = 0;

         unsigned long flags;

         struct irqaction *old, **p;

         irq_desc_t *desc = irq_desc + irq;      /*获得irq的描述符*/

   

          * 对中断请求队列的操作必须在临界区中进行 */

        spin_lock_irqsave(&desc->lock,flags);   /*进入临界区*/

        p = &desc->action;   /*让p 指向irq描述符的action域,即irqaction链表的首部*/

       if ((old = *p) != NULL) {  /*如果这个链表不为空*/

                 /* Can't share interrupts unless both agree to */

                if (!(old->flags & new->flags & SA_SHIRQ)) {

                        spin_unlock_irqrestore(&desc->lock,flags);

                      return -EBUSY;

                }

 

                /* 把新的中断服务例程加入到irq中断请求队列*/

                do {

                        p = &old->next;

                        old = *p;

                } while (old);

                shared = 1;

         }

 

        *p = new;

 

        if (!shared) {       /*如果irq不被共享 */

                desc->depth = 0;    /*启用这条irq线*/

                desc->status &= ~(IRQ_DISABLED | IRQ_AUTODETECT | IRQ_WAITING);

                desc->handler->startup(irq); /*即调用startup_8259A_irq()函数*/ 

        }

        spin_unlock_irqrestore(&desc->lock,flags);   /*退出临界区*/

 

         register_irq_proc(irq);     /*在proc文件系统中显示irq的信息*/

         return 0;

}

下面我们举例说明对这两个函数的使用:

1.对register_irq()函数的使用:

    在驱动程序初始化或者在设备第一次打开时,首先要调用该函数,以申请使用该irq。其中参数handler指的是要挂入到中断请求队列中的中断服务例程。假定一个程序要对/dev/fd0/(第一个软盘对应的设备)设备进行访问,有两种方式,一是直接访问/dev/fd0/,另一种是在系统上安装一个文件系统,我们这里假定采用第一种。通常将IRQ6分配给软盘控制器,给定这个中断号6,软盘驱动程序就可以发出下列请求,以将其中断服务例程挂入中断请求队列:

            request_irq(6, floppy_interrupt,

                   SA_INTERRUPT|SA_SAMPLE_RANDOM, "floppy", NULL);

   我们可以看到,floppy_interrupt()中断服务例程运行时必须禁用中断(设置了SA_INTERRUPT标志),并且不允许共享这个IRQ(清SA_SHIRQ标志

    在关闭设备时,必须通过调用free_irq()函数释放所申请的中断请求号。例如,当软盘操作终止时(或者终止对/dev/fd0/I/O操作,或者卸载这个文件系统),驱动程序就放弃这个中断号:

free_irq(6, NULL);

2.对setup_ irq()函数的使用

   在系统初始化阶段,内核为了初始化时钟中断设备irq0描述符,在time_init(  )函数中使用了下面的语句:

struct irqaction irq0  =

    {timer_interrupt, SA_INTERRUPT, 0, "timer", NULL,};

    setup_irq(0, &irq0);

 

    首先,初始化类型为irqactionirq0变量,把handler域设置timer_interrupt(  )函数的地址,flags域设置SA_INTERRUPTname域设置"timer",最后一个域设置NULL以表示没有用dev_id值。接下来,内核调用setup_x86_irq(  ),把irq0插入到IRQ0的中断请求队列:

     类似地,内核初始化与IRQ2IRQ13相关的irqaction描述符,并把它们插入到相应的请求队列中,在 init_IRQ(  )函数中有下面的语句:

struct irqaction irq2 =

            {no_action, 0, 0, "cascade", NULL,};

struct irqaction irq13 =

    {   math_error_irq, 0, 0, "fpu", NULL,};

setup_x86_irq(2, &irq2);

setup_x86_irq(13, &irq13);