Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1951269
  • 博文数量: 383
  • 博客积分: 10011
  • 博客等级: 上将
  • 技术积分: 4061
  • 用 户 组: 普通用户
  • 注册时间: 2008-04-24 18:53
文章分类

全部博文(383)

文章存档

2011年(1)

2010年(9)

2009年(276)

2008年(97)

我的朋友

分类: LINUX

2008-12-16 17:10:00

do_IRQ()分析

格式:

asmlinkage unsigned int do_IRQ(struce pt_regs regs)

 

1、  首先取得中断号,并且获取对应的irq_desc:

    int irq = regs.orig_eax & 0xff;    // high bits used in ret_from_ code  
int cpu = smp_processor_id();
irq_desc_t *desc = irq_desc + irq;
2、  对中断芯片(模块)应答:
desc->handler->ack(irq);
3、  修改它的状态(注:觉得这些状态只有在SMP下才有意义):
    status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);
status |= IRQ_PENDING;     /* we _want_ to handle it */
IRQ_REPLAY是指如果被禁止的中断号上又产生了中断,这个中断是不会被处理的,当这个中断号被允许产生中断时,会将这个未被处理的中断转为IRQ_REPLAY
IRQ_WAITING探测用,探测时会将所有没有挂处理函数的中断号上设置IRQ_WAITING,如果这个中断号上有中断产生,就把这个状态去掉,因此,我们就可以知道哪些中断引脚上产生过中断了。
IRQ_PENDING , IRQ_INPROGRESS是为了确保: 
1 同一个中断号的处理程序不能重入;

2 不能丢失这个中断号的下一个处理程序。
具体的说,当内核在运行某个中断号对应的处理程序()时,状态会设置成IRQ_INPROGRESS。如果……时,发现已经有一个实例在运行了,就将这下一个中断标注为IRQ_PENDING,然后返回。这个已在运行的实例结束的时候,会查看是否期间有同一中断发生了,是则再次执行一遍。
这些状态的操作不是在什么情况下都必须的。
多个CPU比较复杂,因为CPULocalAPIC,每个都有自己的中断,但是它们可能调用同一个函数,比如时钟中断,每个CPU都可能产生,它们都会调用时钟中断处理函数。
I/O APIC传过来的中断,如果是电平触发,也不会,因为在结束发出EOI前,这个引脚上是不接收中断信号。如果是边沿触发,要么是开中断,要么I/O APIC选择不同的CPU,在这两种情况下,会有重入的可能。
/*
         * If the IRQ is disabled for whatever reason, we cannot
         * use the action we have.
         */
        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 there is no IRQ handler or it was disabled, exit early.
           Since we set PENDING, if another processor is handling
           a different instance of this same irq, the other processor
           will take care of it.
         */
        if (!action)
           goto out;           /*要么该中断没有处理函数;要么被禁止运行(IRQ_DISABLE);要么有一个实例已经在运行了*/
      /*
         * Edge triggered interrupts need to remember
         * pending events.
         * This applies to any hw interrupts that allow a second
         * instance of the same irq to arrive while we are in do_IRQ
         * or in the handler. But the code here only handles the _second_
         * instance of the irq, not the third or fourth. So it is mostly
         * useful for irq hardware that does not mask cleanly in an
         * SMP environment.
         */
        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_active(cpu) & softirq_mask(cpu))
               do_softirq();    /*执行软中断*/
        return 1;
}
 

内核的策略是,当中断不是特别多的时候,及时处理中断,所以do_irq会调用do_softirq
当系统中断过多时,do_softirq才会被推迟到内核的ksoftirq内核线程中去。如何判断中断过多呢,linux的认为发生中断嵌套了,就是中断过多。do_irq在调用do_softirq时会以此为判断条件。


do_IRQ的相关对象
do_IRQ中,一个中断主要由三个对象来完成:irq_desc_thw_irq_controllerirqaction
 
其中, irq_desc_t对象构成的irq_desc[]数组元素分别对应了224个硬件中断(idt一共256项,cpu自己前保留了32项,256-32=224,当然这里面有些项是不用的,比如x80是系统调用).
   
当发生中断时,函数do_IRQ就会在irq_desc[]相应的项中提取各种信息来完成对中断的处理。

    irq_desc
有一个字段handler指向发出这个中断的设备的处理对象hw_irq_controller,比如在单CPU,这个对象一般就是处理芯片8259的对象。为什么要指向这个对象呢?因为当发生中断的时候,内核需要对相应的中断进行一些处理,比如屏蔽这个中断等。这个时候需要对中断设备(比如8259芯片)进行操作,于是可以通过这个指针指向的对象进行操作。

    irq_desc
还有一个字段action指向对象irqaction,后者是产生中断的设备的处理对象,其中的handler就是处理函数。由于一个中断可以由多个设备发出,Linux内核采用轮询的方式,将所有产生这个中断的设备的处理对象连成一个链表,一个一个执行。

   
例如,硬盘1,硬盘2都产生中断IRQx,do_IRQ中首先找到irq_desc[x],通过字段handler对产生中断IRQx的设备进行处理(8259而言,就是屏蔽以后的中断IRQx),然后通过action先后运行硬盘1和硬盘2的处理函数。
 
 
 
hw_irq_controller
hw_irq_controller有多种: 
1.
在一般单cpu的机器上,通常采用两个8259芯片,因此hw_irq_controller指的就是  i8259A_irq_type
2.
在多CPU的机器上,采用APIC子系统来处理芯片,APIC3个部分组成,一个是I/O APIC模块,其作用可比做8259芯片,但是它发出的中断信号会通过APIC总线送到其中一个(或几个)CPU中的Local APIC模块,因此,它还起一个路由的作用;它可以接收16个中断。

中断可以采取两种方式,电平触发边沿触发,相应的,I/O APIC模块的hw_irq_controller就有两种:
    ioapic_level_irq_type
    ioapic_edge_irq_type
(
这里指的是intelAPIC,其它公司研制的APIC还不清楚)
3. Local APIC
自己也能单独处理一些直接对CPU产生的中断,例如时钟中断(这和没有使用Local APIC模块的CPU不同,它们接收的时钟中断来自外围的时钟芯片),因此,它也有自己的 hw_irq_controller:
      lapic_irq_type

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;


startup
是启动中断芯片(模块),使得它开始接收中断,一般情况下,就是将所有被屏蔽的引脚取消屏蔽;
shutdown
反之,使得芯片不再接收中断;
enable
设某个引脚可以接收中断,也就是取消屏蔽;
disable
屏蔽某个引脚,例如,如果屏蔽0那么时钟中断就不再发生;
ack
CPU收到来自中断芯片的中断信号,给相应的引脚的处理,这个各种情况下(8259, APIC电平,边沿)的处理都不相同);
end
CPU处理完某个引脚产生的中断后,对中断芯片(模块)的操作。
 
 
 
对中断芯片处理的比较
我们假定在引脚3上有3个硬件设备A,B,C,相应的在对引脚3中断处理函数链上依次挂了处理函数HA-->HB-->HC,并且在B产生中断后,处理函数HB的时候,A,C又都产生了中断。显然,不管C的请求是否被CPU接收到,HC都会被正确运行。 

APIC
电平触发
   
当内核接收到硬件B的中断信号后,它的应答(ack函数)不做任何事情,这个时候在这个引脚上忽略任何请求(I/O APICIR=1),A,CHB运行的时候都发出中断请求,那么非常巧的是,HC能被及时处理,而HA却不能,但是当这个函数链被处理完后,内核会将引脚2IR复位(end函数),这个时候就会查看是否有高电平,当发现A设备产生的高电平时就又会去执行相应的函数链了。

从这里我们可以看出:
(1) APIC
电平触发模式不会忽略中断请求。
(2)
即使在CPU开中断的情况下,也不会出现同一个处理函数链的重入问题。

APIC
边沿触发
   
如果采用上面的方式,HC还能得到正确的处理,A的这次请求却忽略了,因为在IR复位后,A前一次的跳变已经过去了。因此,Linux内核采用另一种方式:
当内核接收到硬件B的中断信号后,它的应答(ack函数)迅速将对应的IR复位,A产生的跳变能被I/O APIC觉察到,从而能被CPU处理.

从这里我们可以看出:
(1)
任何请求不会被忽略。
(2)
带来的问题:
  
如果处理HBCPU处于开中断,那么很有可能HB,HC会出现重入。
  
即使处于关中断,I/O APIC可能会这个中断送到其它CPU去处理,于是其它CPU也会运  行这个函数链,可能出现重入的情况。
(3)
解决的办法:
  
在软件上解决:Linux用状态IRQ_INPROGRESS来保证不会重入,用IRQ_PENDING来保证后来的请求也会被处理。
  
在硬件上解决: 在应答的时候,如果发现已经处于IRQ_PENDING状态了,就屏蔽这个引脚的中断(这个不会造成中断得不到处理的情况)
(
不清楚为什么还要判断IRQ_DISABLE?)

8259PIC
   
可能是由于8259PIC芯片的一些问题,内核处理方式不象APIC的那么好。无论是电平还是边沿,处理都一样: 在应答的时候,屏蔽相应的中断,在函数链处理完后,再取消屏蔽。

可以看出:
(1)
如果是电平触发,和APIC一样,没问题。如果是边沿触发,则B的跳变被忽略。因此, B可能需要不断地跳变。
(2)
不会出现同一处理函数链重入的问题,即使在开中断的状态。
另外,8259PIC有一个问题:
无论是电平触发还是边沿触发,高电平必须保持到CPU自己产生的两个INTA脉冲之后,否则中断信号不能正确地送到CPU中。因此采用边沿触发的方式要比较小心。
 
 
硬件处理函数运行的申请与释放(irqaction)
将一个硬件处理函数挂到相应的处理队列上去(当然首先要生成一个irqaction结构):

-----------------------------------------------------

int request_irq( unsigned int irq,

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

                 unsigned long irqflags,

                 const char * devname,

                 void *dev_id)

-----------------------------------------------------                

handler是硬件处理函数,在下面的代码中可以看得很清楚:

---------------------------------------------

         do {

                 status |= action->flags;

                 action->handler(irq, action->dev_id, regs);

                 action = action->next;

         } while (action);

---------------------------------------------

第二个参数就是actiondev_id,这个参数非常灵活,可以派各种用处。而且要保证的是,这个dev_id在这个处理链中是唯一的,否则删除会遇到麻烦。
第三个参数是在entry.S中压入的各个积存器的值。


它的大致流程是
:
1.
slab中分配一个irqaction,填上必需的数据
;
  (
以下在函数setup_irq
)
2.
找到它的irq对应的结构irq_desc

3.
看它是否想对随机数做贡献;

4.
看这个结构上是否已经挂了其它处理函数了,如果有,则必须确保它本身和这个队列上所有的处理函数都是可共享的(由于传递性,只需判断一个就可以了)

5.
挂到队列最后;

6.
如果这个irq_desc只有它一个irqaction,那么还要进行一些初始化工作;

7
proc/ 下面登记 register_irq_proc(irq)


将一个处理函数去掉

     void free_irq(unsigned int irq, void *dev_id)
首先在队列里找到这个处理函数(严格的说是irqaction),主要靠dev_id来匹配,这时dev_id的唯一性就比较重要了。

将它从队列里剔除。

如果这个中断号没有处理函数了,那么禁止这个中断号上再产生中断:

        if (!desc->action) {

                desc->status |= IRQ_DISABLED;

                desc->handler->shutdown(irq);

        }

如果其它CPU在运行这个处理函数,要等到它运行完了,才释放它:

#ifdef CONFIG_SMP
/* Wait to make sure it's not being used on another CPU */

while (desc->status & IRQ_INPROGRESS)
barrier();
#endif
kfree(action);

 
阅读(3115) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~