linux 中断管理(三)
一、中断过程
1、中断的发生
当发生中断时,CPU会跳到异常向量表,处理相应的中断
异常向量表在arch\arm\kernel\entry-armv.S
文件里面定义
.globl __vectors_start
__vectors_start:
swi SYS_ERROR0
b vector_und + stubs_offset
ldr pc, .LCvswi + stubs_offset
b vector_pabt + stubs_offset
b vector_dabt + stubs_offset
b vector_addrexcptn + stubs_offset
/* 中断入口地址。发生中断时会跳到这里,执行这句跳转语句,
*+ stubs_offset 是因为异常向量表使用高地址0xffff0000,异常向量copy0xffff0000处 */
b vector_irq + stubs_offset
b vector_fiq + stubs_offset
.globl __vectors_end
__vectors_end:
如果发生irq中断,就会执行
b vector_irq + stubs_offset
这条语,会跳转到下面的代码块
对于vector_stub
这个宏,在linux异常处理体系结构有简单的介绍
/* vector_stub 是汇编下定义的一个宏, 里面是一块代码块,主要用于计算返回地址,计算下一步要跳转的入口地址
* irq这个宏的名字,
* #define IRQ_MODE 0x00000012
* 4 这个是用于计算返回地址的
*/
vector_stub irq, IRQ_MODE, 4
/* 下面要跳转到那个入口,是在 vector_stub 宏里面计算并且跳转的*/
.long __irq_usr @ 0 (USR_26 / USR_32) /* 用户模式下发生irq异常 */
.long __irq_invalid @ 1 (FIQ_26 / FIQ_32)
.long __irq_invalid @ 2 (IRQ_26 / IRQ_32)
.long __irq_svc @ 3 (SVC_26 / SVC_32) /* 管理模式下发生irq异常 */
.long __irq_invalid @ 4
.long __irq_invalid @ 5
.long __irq_invalid @ 6
.long __irq_invalid @ 7
.long __irq_invalid @ 8
.long __irq_invalid @ 9
.long __irq_invalid @ a
.long __irq_invalid @ b
.long __irq_invalid @ c
.long __irq_invalid @ d
.long __irq_invalid @ e
.long __irq_invalid @ f
之前在linux异常处理体系结构介绍到,不管是用户模式下发生中断,还是管理模式下发生中断,中断服务函数都是相同的(只是前面需要)。
接下来就以__irq_usr
分析。
__irq_usr:
/* usr_entry 也是一个宏,里面的代码块作用是保存 r0、r1 等一些寄存器 */
usr_entry
#ifdef CONFIG_TRACE_IRQFLAGS
bl trace_hardirqs_off
#endif
get_thread_info tsk
#ifdef CONFIG_PREEMPT
ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
add r7, r8, #1 @ increment it
str r7, [tsk, #TI_PREEMPT]
#endif
irq_handler /* irq_handler 是一个宏,这个宏也是一个代码块,中断服务函数,也是在这个宏的代码块里面调用 */
#ifdef CONFIG_PREEMPT
ldr r0, [tsk, #TI_PREEMPT]
str r8, [tsk, #TI_PREEMPT]
teq r0, r7
strne r0, [r0, -r0]
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
bl trace_hardirqs_on
#endif
mov why, #0
b ret_to_user
irq_handler
这个宏也是定义在arch\arm\kernel\entry-armv.S
文件里面
.macro irq_handler
get_irqnr_preamble r5, lr /*get_irqnr_preamble 是一个宏,里面什么都没做 */
1: get_irqnr_and_base r0, r6, r5, lr /* get_irqnr_and_base 也是一个宏,在里面会计算当前的中断号(中断号通过 INTOFFSET 寄存器的某一位来确定)、中断状态等,和struct pt_regs 结构体压栈。*/
movne r1, sp /* 通过堆栈将struct pt_regs* 传给 asm_do_IRQ 函数 */
@
@ routine called with r0 = irq number, r1 = struct pt_regs *
@
adrne lr, 1b
bne asm_do_IRQ /* 到这里就是跳转过去执行中断服务函数了 */
2、中断的处理
asm_do_IRQ()
函数就是所有中断的服务函数了。arch\arm\kernel\Irq.c
文件里面定义,代码块如下:
asmlinkage void __exception asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);
/* 通过中断号获取 irq_desc 数组项*/
struct irq_desc *desc = irq_desc + irq;
/*
* Some hardware gives randomly wrong interrupts. Rather
* than crashing, do something sensible.
*/
if (irq >= NR_IRQS)
desc = &bad_irq_desc;
irq_enter();
/* 这里处理中断了 */
desc_handle_irq(irq, desc);
/* AT91 specific workaround */
irq_finish(irq);
irq_exit();
set_irq_regs(old_regs);
}
desc_handle_irq 函数里面就是调用用户注册的中断处理函数。
desc_handle_irq
函数在include\asm-arm\mach\Irq.h
文件里面定义。代码块如下
static inline void desc_handle_irq(unsigned int irq, struct irq_desc *desc)
{
desc->handle_irq(irq, desc);
}
*补充说明:
asm_do_IRQ
函数中参数irq的取值的范围是IRQ_EINT0 ~ 31
, 只有32个取值。asm_do_IRQ
函数中的irq
参数可能是一个实际的中断号,也可能是一组中断的中断号,这是由s3c2440的芯片决定的.当发生中断时后INTPND
寄存器的某一位被置1,INTOFFSET
寄存器的值确定irq
参数。每一位实际的中断在irq_desc
数组中都有一项与它对应, 他们的数目不止。当asm_do_IRQ
函数中irq表示的是"一组"中断时,irq_desc[irq].handle_irq
成员函数还需要先分辨出这一组中断的哪一个中断(假设中断号是irqno
),然后调用irq_desc[irqno].handle_irq
来进一步处理。
以外部中断EINT8~EINT23为例:
(1)中断被触发时,
INTOFFSET
寄存器的值是5,asm_do_IRQ
函数中的参数irq的值为(IRQ_EINT0+5)
,即IRQ_EINT8t23
。然后将调用irq_desc[IRQ_EINT8t23].handle_irq
函数来处理
(2)irq_desc[IRQ_EINT8t23].handle_irq
在初始化的时候,被初始化为s3c_irq_demux_extint8
(3)s3c_irq_demux_extint8
函数在arch\arm\plat-s3c24xx\Irq.c
文件里面定义。它先读取EINTPEND 和 EINTMASK
寄存器的值来确定发生了那些中断;然后重新计算中断号,然后再调用irq_desc
数组项中的handle_irq
成员函数。
s3c_irq_demux_extint8
函数的代码如下:
static void
s3c_irq_demux_extint8(unsigned int irq, struct irq_desc *desc)
{
/* 读取 EINTPEND 寄存器的值,EINT8 ~ EINT23 发生时相应的位被置1 */
unsigned long eintpnd = __raw_readl(S3C24XX_EINTPEND);
/* 读取屏蔽寄存器的值 */
unsigned long eintmsk = __raw_readl(S3C24XX_EINTMASK);
/* 清除被屏蔽位 */
eintpnd &= ~eintmsk;
/* 清除低8位,EINT8对应位8 ... */
eintpnd &= ~0xff; /* ignore lower irqs */
/* we may as well handle all the pending IRQs here */
/* 循环处理所有的子中断 */
while (eintpnd) {
/* 确定 eintpnd 中为1的最高位 */
irq = __ffs(eintpnd);
/* 将该位清零 */
eintpnd &= ~(1<<irq);
/* 重新计算中断号:前面计算出,如果 irq = __ffs(eintpnd) 算出的是8,则中断号为8 */
irq += (IRQ_EINT4 - 4);
/* 调用这个中断真正的处理函数(handle_edge_irq函数) */
desc_handle_irq(irq, irq_desc + irq);
}
}
(4)
IRQ_EINT8 ~ IRQ_EINT23
这几个真正的中断处理函数入口,在init_IRQ
函数初始化中断的时候被初始化handle_edge_irq
(边缘触发方式)函数。
handle_edge_irq
函数在kernel\irq\Chip.c
文件里面定义,代码如下:
void fastcall handle_edge_irq(unsigned int irq, struct irq_desc *desc)
{
...
/* 统计中断发生的次数 */
kstat_cpu(cpu).irqs[irq]++;
/* Start handling the irq */
/* 调用 chip 底层的操作函数 清除中断 */
desc->chip->ack(irq);
/* Mark the IRQ currently in progress.*/
desc->status |= IRQ_INPROGRESS;
do {
/* 依次调用 action 链表里面的处理函数(用户注册的函数都是挂在action链表里面的) */
action_ret = handle_IRQ_event(irq, action);
} while ((desc->status & (IRQ_PENDING | IRQ_DISABLED)) == IRQ_PENDING);
}
handle_IRQ_event
依次调用action
链表里面的处理函数;这个函数在kernel\irq\Handle.c
文件里面定义,代码块如下
irqreturn_t handle_IRQ_event(unsigned int irq, struct irqaction *action)
{
...
/* 依次调用 action 链表的成员函数*/
do {
ret = action->handler(irq, action->dev_id);
if (ret == IRQ_HANDLED)
status |= action->flags;
retval |= ret;
action = action->next;
} while (action);
...
return retval;
}
注意: 上面分析的是边缘触发的中断过程,对于边缘触发调用的是
handle_edge_irq()
函数处理,对于电平触发的调用的是handle_level_irq
函数,处理的方式都是类似的,只是电平触发的还要屏蔽这个中断,处理完之后还要开启这个中断。
总结:
(1) 发生中断时,中断向量调用
asm_do_IRQ
,传入中断号irq
。
(2)asm_do_IRQ
函数根据中断号irq
调用irq_desc[irq].handle_irq
。他是这个中断的处理函数入口。对于电平触发这个函数入口通常是handle_level_irq
,对于边缘触发入口函数通常是handle_edge_irq
。
(3) 入口函数首先清除中断,入口函数是handle_level_irq
时还要屏蔽中断。
(4) 逐个调用用户注册在irq_desc[irq].action
链表中注册的中断处理函数
(5) 入口函数是handle_level_irq
时,处理完之后还要开启这个中断。