分类: 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比较复杂,因为CPU由LocalAPIC,每个都有自己的中断,但是它们可能调用同一个函数,比如时钟中断,每个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_t、hw_irq_controller、irqaction。
其中, 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子系统来处理芯片,APIC有3个部分组成,一个是I/O APIC模块,其作用可比做8259芯片,但是它发出的中断信号会通过APIC总线送到其中一个(或几个)CPU中的Local APIC模块,因此,它还起一个路由的作用;它可以接收16个中断。
中断可以采取两种方式,电平触发和边沿触发,相应的,I/O APIC模块的hw_irq_controller就有两种:
ioapic_level_irq_type
ioapic_edge_irq_type
(这里指的是intel的APIC,其它公司研制的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 APIC的IR位=1),A,C在HB运行的时候都发出中断请求,那么非常巧的是,HC能被及时处理,而HA却不能,但是当这个函数链被处理完后,内核会将引脚2的IR复位(end函数),这个时候就会查看是否有高电平,当发现A设备产生的高电平时就又会去执行相应的函数链了。
从这里我们可以看出:
(1) APIC电平触发模式不会忽略中断请求。
(2) 即使在CPU开中断的情况下,也不会出现同一个处理函数链的重入问题。
APIC边沿触发
如果采用上面的方式,HC还能得到正确的处理,A的这次请求却忽略了,因为在IR复位后,A前一次的跳变已经过去了。因此,Linux内核采用另一种方式:
当内核接收到硬件B的中断信号后,它的应答(ack函数)迅速将对应的IR复位,A产生的跳变能被I/O APIC觉察到,从而能被CPU处理.
从这里我们可以看出:
(1)任何请求不会被忽略。
(2)带来的问题:
如果处理HB的CPU处于开中断,那么很有可能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);
---------------------------------------------
第二个参数就是action的dev_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);