5.中断处理流程
回顾第一节所讲的内容,当一个异常或中断发生时,处理器会将PC设置为特定地址,从而跳转到已经初始化好的异常向量表。因此,要理清中断处理流程,先从异常向量表开始。对于ARM Linux而言,异常向量表和异常处理程序都存在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
-
b vector_irq + stubs_offset @中断入口,vector_irq
-
b vector_fiq + stubs_offset
-
-
.globl __vectors_end
-
__vectors_end:
vector_irq+stubs_offset为中断的入口点,此处之所以要加上stubs_offset是为了实现位置无关编程。首先分析一下stubs_offset是如何计算的:
.equ stubs_offset, __vectors_start + 0x200 - __stubs_start
在第3节中已经提到,内核启动时会将异常向量表拷贝到 0xFFFF_0000,将异常向量处理程序的 stub 拷贝到 0xFFFF_0200。图5-1描述了异常向量表和异常处理程序搬移前后的内存布局。
图5-1 异常向量表和异常处理程序搬移前后对比
当汇编器看到B指令后会把要跳转的标签转化为相对于当前PC的偏移量(±32M)写入指令码。由于内核启动时中断向量表和stubs都发生了代码搬移,所以如果中断向量表中仍然写成b vector_irq,那么实际执行的时候就无法跳转到搬移后的vector_irq处,因为指令码里写的是原来的偏移量,所以需要把指令码中的偏移量写成搬移后的。设搬移后的偏移量为offset,如图5-1所示,
offset = L1+L2
= [0x200 - (irq_PC_X -
__vectors_start_X)] + (vector_irq_X - __stubs_start_X)
= [0x200 - (irq_PC - __vectors_start)]
+ (vector_irq - __stubs_start)
= 0x200 - irq_PC +
__vectors_start + vector_irq - __stubs_start
= vector_irq +
(__vectors_start + 0x200 - __stubs_start) - irq_PC
令stubs_offset
= __vectors_start + 0x200 - __stubs_start
则offset =
vector_irq + stubs_offset - irq_PC,所以中断入口点为“b vector_irq + stubs_offset”,其中减去irq_PC是由汇编器在编译时完成的。
在分析vector_irq处理函数之前,先了解一下当一个异常或中断导致处理器模式改变时,ARM处理器内核的处理流程如下图所示:
在__stubs_start和__stubs_end之间找到vector_irq处理函数的定义vector_stub irq,
IRQ_MODE, 4,其中vector_stub是一个宏(在arch/arm/kernel/entry_armv.S中定义),为了分析更直观,我们将vector_stub宏展开如下:
-
/*
-
* Interrupt dispatcher
-
*/
-
vector_irq:
-
.if 4
-
sub lr, lr, #4 @在中断发生时,lr指向最后执行的指令地址加上8。只有在当前指令执行完毕后,才进入中断处理,所以返回地址应指向下一条指令,即(lr-4)处。
-
.endif
-
-
@
-
@ Save r0, lr_<exception> (parent PC) and spsr_<exception>
-
@ (parent CPSR)
-
@
-
stmia sp, {r0, lr} @ 保存r0, lr到irq模式下的栈中
-
mrs lr, spsr
-
str lr, [sp, #8] @保存spsr到irq模式下的栈中
-
-
@
-
@ Prepare for SVC32 mode. IRQs remain disabled.
-
@
-
mrs r0, cpsr
-
eor r0, r0, #( IRQ_MODE ^ SVC_MODE) @设置成SVC模式,但未切换
-
msr spsr_cxsf, r0 @保存到spsr_irq中
-
-
@
-
@ the branch table must immediately follow this code
-
@
-
and lr, lr, #0x0f @lr存储着上一个处理器模式的cpsr值,lr = lr & 0x0f取出用于判断发生中断前是用户态还是核心态的信息,该值用于下面跳转表的索引。
-
mov r0, sp @将irq模式下的sp保存到r0,作为参数传递给即将调用的__irq_usr或__irq_svc
-
ldr lr, [pc, lr, lsl #2] @pc指向当前执行指令地址加8,即跳转表的基址。lr作为索引,由于是4字节对齐,所以lr = lr << 2.
-
movs pc, lr @ branch to handler in SVC mode
-
@当mov指令后加“s”且目标寄存器为pc时,当前模式下的spsr会被复制到cpsr,从而完成模式切换(从irq切换到svc)并且跳转到pc指向的指令继续执行
-
ENDPROC(vector_irq)
-
-
.long __irq_usr @ 0 (USR_26 / USR_32)
-
.long __irq_invalid @ 1 (FIQ_26 / FIQ_32)
-
.long __irq_invalid @ 2 (IRQ_26 / IRQ_32)
-
.long __irq_svc @ 3 (SVC_26 / SVC_32)
-
.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
如果发生中断前处于用户态则进入__irq_usr,其定义如下(arch/arm/kernel/entry_armv.S):
-
.align 5
-
__irq_usr:
-
usr_entry @保存中断上下文,稍后分析
-
kuser_cmpxchg_check
-
#ifdef CONFIG_TRACE_IRQFLAGS
-
bl trace_hardirqs_off
-
#endif
-
get_thread_info tsk @获取当前进程的进程描述符中的成员变量thread_info的地址,并将该地址保存到寄存器tsk(r9)(在entry-header.S中定义)
-
#ifdef CONFIG_PREEMPT @如果定义了抢占,增加抢占数值
-
ldr r8, [tsk, #TI_PREEMPT] @ 获取preempt计数器值
-
add r7, r8, #1 @ preempt加1,标识禁止抢占
-
str r7, [tsk, #TI_PREEMPT] @将加1后的结果写入进程内核栈的变量中
-
#endif
-
irq_handler @调用中断处理程序,稍后分析
-
#ifdef CONFIG_PREEMPT
-
ldr r0, [tsk, #TI_PREEMPT] @获取preempt计数器值
-
str r8, [tsk, #TI_PREEMPT] @将preempt恢复到中断前的值
-
teq r0, r7 @比较中断前后preempt是否相等
-
strne r0, [r0, -r0] @如果不等,则产生异常(向地址0写入数据)?
-
#endif
-
#ifdef CONFIG_TRACE_IRQFLAGS
-
bl trace_hardirqs_on
-
#endif
-
mov why, #0 @r8=0
-
b ret_to_user @中断处理完成,恢复中断上下文并返回中断产生的位置,稍后分析
-
UNWIND(.fnend )
-
ENDPROC(__irq_usr)
上面代码中的usr_entry是一个宏定义,主要用于保护上下文到栈中:
-
.macro usr_entry
-
UNWIND(.fnstart )
-
UNWIND(.cantunwind ) @ dont unwind the user space
-
sub sp, sp, #S_FRAME_SIZE @ATPCS中,堆栈被定义为递减式满堆栈,所以首先让sp向下移动#S_FRAME_SIZE(pt_regs结构体size),准备向栈中存放数据。此处的sp是svc模式下的栈指针。
-
stmib sp, {r1 - r12}
-
-
ldmia r0, {r1 - r3}
-
add r0, sp, #S_PC @ here for interlock avoidance
-
mov r4, #-1 @ "" "" "" ""
-
-
str r1, [sp] @ save the "real" r0 copied
-
@ from the exception stack
-
-
@
-
@ We are now ready to fill in the remaining blanks on the stack:
-
@
-
@ r2 - lr_<exception>, already fixed up for correct return/restart
-
@ r3 - spsr_<exception>
-
@ r4 - orig_r0 (see pt_regs definition in ptrace.h)
-
@
-
@ Also, separately save sp_usr and lr_usr
-
@
-
stmia r0, {r2 - r4}
-
stmdb r0, {sp, lr}^ @将user模式下的sp和lr保存到svc模式的栈中
-
-
@
-
@ Enable the alignment trap while in kernel mode
-
@
-
alignment_trap r0
-
-
@
-
@ Clear FP to mark the first stack frame
-
@
-
zero_fp
-
.endm
上面的这段代码主要是在填充结构体pt_regs ,在include/asm/ptrace.h中定义:
-
struct pt_regs {
-
long uregs[18];
-
};
-
-
#define ARM_cpsr uregs[16]
-
#define ARM_pc uregs[15]
-
#define ARM_lr uregs[14]
-
#define ARM_sp uregs[13]
-
#define ARM_ip uregs[12]
-
#define ARM_fp uregs[11]
-
#define ARM_r10 uregs[10]
-
#define ARM_r9 uregs[9]
-
#define ARM_r8 uregs[8]
-
#define ARM_r7 uregs[7]
-
#define ARM_r6 uregs[6]
-
#define ARM_r5 uregs[5]
-
#define ARM_r4 uregs[4]
-
#define ARM_r3 uregs[3]
-
#define ARM_r2 uregs[2]
-
#define ARM_r1 uregs[1]
-
#define ARM_r0 uregs[0]
-
#define ARM_ORIG_r0 uregs[17]
usr_entry宏填充pt_regs结构体的过程如图5-2所示,先将r1~r12保存到ARM_r1~ARM_ip(绿色部分),然后将产生中断时的r0寄存器内容保存到ARM_r0(蓝色部分),接下来将产生中断时的下一条指令地址lr_irq、spsr_irq和r4保存到ARM_pc、ARM_cpsr和ARM_ORIG_r0(红色部分),最后将用户模式下的sp和lr保存到ARM_sp 和ARM_lr 中。
图5-2 usr_entry宏填充pt_regs结构体
如果发生中断前处于核心态则进入__irq_svc,其定义如下(arch/arm/kernel/entry_armv.S):
-
.align 5
-
__irq_svc:
-
svc_entry @保存中断上下文
-
-
#ifdef CONFIG_TRACE_IRQFLAGS
-
bl trace_hardirqs_off
-
#endif
-
#ifdef CONFIG_PREEMPT
-
get_thread_info tsk
-
ldr r8, [tsk, #TI_PREEMPT] @ 获取preempt计数器值
-
add r7, r8, #1 @ preempt加1,标识禁止抢占
-
str r7, [tsk, #TI_PREEMPT] @将加1后的结果写入进程内核栈的变量中
-
#endif
-
-
irq_handler @调用中断处理程序,稍后分析
-
#ifdef CONFIG_PREEMPT
-
str r8, [tsk, #TI_PREEMPT] @ 恢复中断前的preempt计数器
-
ldr r0, [tsk, #TI_FLAGS] @ 获取flags
-
teq r8, #0 @ 判断preempt是否等于0
-
movne r0, #0 @ 如果preempt不等于0,r0=0
-
tst r0, #_TIF_NEED_RESCHED @将r0与#_TIF_NEED_RESCHED做“与操作”
-
blne svc_preempt @如果不等于0,说明发生内核抢占,需要重新调度。
-
#endif
-
-
ldr r0, [sp, #S_PSR] @ irqs are already disabled
-
msr spsr_cxsf, r0
-
#ifdef CONFIG_TRACE_IRQFLAGS
-
tst r0, #PSR_I_BIT
-
bleq trace_hardirqs_on
-
#endif
-
svc_exit r4 @恢复中断上下文,稍后分析。
-
UNWIND(.fnend )
-
ENDPROC(__irq_svc)
其中svc_entry是一个宏定义,主要用于保护中断上下文到栈中:
-
.macro svc_entry, stack_hole=0
-
UNWIND(.fnstart )
-
UNWIND(.save {r0 - pc} )
-
sub sp, sp, #(S_FRAME_SIZE + \stack_hole)
-
SPFIX( tst sp, #4 )
-
SPFIX( bicne sp, sp, #4 )
-
stmib sp, {r1 - r12}
-
-
ldmia r0, {r1 - r3}
-
add r5, sp, #S_SP @ here for interlock avoidance
-
mov r4, #-1 @ "" "" "" ""
-
add r0, sp, #(S_FRAME_SIZE + \stack_hole)
-
SPFIX( addne r0, r0, #4 )
-
str r1, [sp] @ save the "real" r0 copied
-
@ from the exception stack
-
-
mov r1, lr
-
-
@
-
@ We are now ready to fill in the remaining blanks on the stack:
-
@
-
@ r0 - sp_svc
-
@ r1 - lr_svc
-
@ r2 - lr_<exception>, already fixed up for correct return/restart
-
@ r3 - spsr_<exception>
-
@ r4 - orig_r0 (see pt_regs definition in ptrace.h)
-
@
-
stmia r5, {r0 - r4}
-
.endm
svc_entry宏填充pt_regs结构体的过程如图5-2所示,先将r1~r12保存到ARM_r1~ARM_ip(绿色部分),然后将产生中断时的r0寄存器内容保存到ARM_r0(蓝色部分),由于是在svc模式下产生的中断,所以最后将sp_svc、lr_svc、lr_irq、spsr_irq和r4保存到ARM_sp、ARM_lr、ARM_pc、ARM_cpsr和ARM_ORIG_r0(红色部分)。
图5-3 svc_entry宏填充pt_regs结构体
上述的中断上下文保存过程共涉及了3种栈指针,分别是:用户空间栈指针sp_usr,内核空间栈指针sp_svc和irq模式下的栈栈指针sp_irq。sp_usr指向在setup_arg_pages函数中创建的用户空间栈。sp_svc指向在alloc_thread_info函数中创建的内核空间栈。sp_irq在cpu_init函数中被赋值,指向全局变量stacks.irq[0]。
保存中断上下文后则进入中断处理程序——irq_handler,定义在arch/arm/kernel/entry_armv.S文件中:
-
.macro irq_handler
-
get_irqnr_preamble r5, lr
-
1: get_irqnr_and_base r0, r6, r5, lr @获取中断号,存到r0中,稍后分析
-
movne r1, sp @如果中断号不等于0,将r1=sp,即pt_regs结构体首地址
-
@
-
@ routine called with r0 = irq number, r1 = struct pt_regs *
-
@
-
adrne lr, 1b @如果r0(中断号)不等于0, lr(返回地址)等于标号1处,即get_irqnr_and_base r0, r6, r5, lr的那行,即循环处理所有的中断。
-
bne asm_do_IRQ @进入中断处理,稍后分析。
-
……
-
.endm
get_irqnr_and_base用于判断当前发生的中断号(与CPU紧密相关),此处不再分析。如果获取的中断号不等于0,则将中断号存入r0寄存器作为第一个参数,pt_regs结构体地址存入r1寄存器作为第二个参数,跳转到c语言函数asm_do_IRQ做进一步处理。为了不让大家在汇编语言和C语言之间来回切换,还是先把最后一点汇编语言代码(中断返回汇编代码)分析了再去分析asm_do_IRQ吧。回看__irq_usr和__irq_svc标号处的代码,在完成了irq_handler中断处理函数后,要完成从中断异常处理程序返回到中断点的工作。如果中断产生于用户空间,则调用ret_to_user来恢复中断现场并返回用户空间继续运行:
-
arch/arm/kernel/entry_armv.S
-
ENTRY(ret_to_user)
-
ret_slow_syscall:
-
disable_irq @ disable interrupts,此处不明白,disable_irq应该接受irq中断号作为参数,来禁止指定的irq号中断线。但是此处调用disable_irq之前并没有将irq中断号存入r0寄存器,这是为什么?
-
ldr r1, [tsk, #TI_FLAGS] @获取thread_info->flags
-
tst r1, #_TIF_WORK_MASK @判断是否有待处理的work
-
bne work_pending @如果有,则进入work_pending进一步处理,主要是完成用户进程抢占相关处理。
-
no_work_pending: @如果没有work待处理,则准备恢复中断现场,返回用户空间。
-
/* perform architecture specific actions before user return */
-
arch_ret_to_user r1, lr @调用体系结构相关的代码
-
-
restore_user_regs fast = 0, offset = 0 @调用restore_user_regs
-
ENDPROC(ret_to_user)
-
-
以下是恢复中断现场寄存器的宏,就是将发生中断时保存在内核空间堆栈上的寄存器还原,可以对照图5-2所示的内核空间堆栈保存的内容来理解下面代码:
-
.macro restore_user_regs, fast = 0, offset = 0
-
ldr r1, [sp, #\offset + S_PSR] @ 从内核栈中获取发生中断时的cpsr值
-
ldr lr, [sp, #\offset + S_PC]! @ 从内核栈中获取发生中断时的下一条指令地址
-
msr spsr_cxsf, r1 @ 将r1保存到spsr_svc
-
#if defined(CONFIG_CPU_32v6K)
-
clrex @ clear the exclusive monitor
-
#elif defined (CONFIG_CPU_V6)
-
strex r1, r2, [sp] @ clear the exclusive monitor
-
#endif
-
.if \fast
-
ldmdb sp, {r1 - lr}^ @ get calling r1 - lr
-
.else
-
ldmdb sp, {r0 - lr}^ @ 存在^,所以将内核栈保存的内容恢复到用户空间的r0~lr寄存器
-
.endif
-
add sp, sp, #S_FRAME_SIZE - S_PC
-
movs pc, lr @将发生中断时的下一条指令地址存入pc,从而返回中断点继续执行,并且将发生中断时的cpsr内容恢复到cpsr寄存器中(开启中断)。
-
.endm
如果中断产生于内核空间,则调用svc_exit来恢复中断现场:
-
arch/arm/kernel/ entry-header.S
-
.macro svc_exit, rpsr
-
msr spsr_cxsf, \rpsr
-
#if defined(CONFIG_CPU_32v6K)
-
clrex @ clear the exclusive monitor
-
ldmia sp, {r0 - pc}^ @ load r0 - pc, cpsr
-
#elif defined (CONFIG_CPU_V6)
-
ldr r0, [sp]
-
strex r1, r2, [sp] @ clear the exclusive monitor
-
ldmib sp, {r1 - pc}^ @ load r1 - pc, cpsr
-
#else
-
ldmia sp, {r0 - pc}^ @ 返回内核空间时,恢复中断现场比较简单,就是将r0-pc以及cpsr恢复即可,同时中断也被开启。
-
#endif
-
.endm
ok,分析完所有与中断相关的汇编语言代码后,下面开始分析C语言代码:
在arch/arm/kernel/irq.c文件中找到asm_do_IRQ函数定义:
-
asmlinkage void __exception asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
-
{
-
/*保存新的寄存器集合指针到全局cpu变量,方便后续处理程序访问寄存器集合。*/
-
struct pt_regs *old_regs = set_irq_regs(regs);
-
-
irq_enter();
-
-
/*
-
* Some hardware gives randomly wrong interrupts. Rather
-
* than crashing, do something sensible.
-
*/
-
if (unlikely(irq >= NR_IRQS)) { //判断中断号
-
if (printk_ratelimit())
-
printk(KERN_WARNING "Bad IRQ%u\n", irq);
-
ack_bad_irq(irq);
-
} else {
-
generic_handle_irq(irq); //调用中断处理函数
-
}
-
-
/* AT91 specific workaround */
-
irq_finish(irq);
-
-
irq_exit();
-
set_irq_regs(old_regs);
-
}
asm_do_IRQ是中断处理的C入口函数,主要负责调用request_irq注册的中断处理函数,其流程如图5-4所示:
图5-4 asm_do_IRQ流程
其中,set_irq_regs将指向寄存器结构体的指针保存在一个全局的CPU变量中,后续的程序可以通过该变量访问寄存器结构体。所以在进入中断处理前,先将全局CPU变量中保存的旧指针保留下来,等到中断处理结束后再将其恢复。irq_enter负责更新一些统计量:
-
<kernel/softirq.c>
-
void irq_enter(void)
-
{
-
int cpu = smp_processor_id();
-
-
rcu_irq_enter();
-
if (idle_cpu(cpu) && !in_interrupt()) {
-
__irq_enter();
-
tick_check_idle(cpu);
-
} else
-
__irq_enter();
-
}
如果系统开启动态时钟特性且很长时间没有产生时钟中断,则调用tick_check_idle更新全局变量jiffies(关于动态时钟特性,在后续的总结中再进行分析)。宏__irq_enter()定义如下:
-
#define __irq_enter() \
-
do { \
-
account_system_vtime(current); \
-
add_preempt_count(HARDIRQ_OFFSET); \
-
trace_hardirq_enter(); \
-
} while (0)
add_preempt_count(HARDIRQ_OFFSET)使表示中断处理程序嵌套层次的计数器加1。计数器保存在当前进程thread_info结构的preempt_count字段中:
图5-5 preempt_count结构
内核将preempt_count分成5部分:bit0~7与PREEMPT相关,bit8~15用作软中断计数器,bit16~25用作硬中断计数器,bit26用作不可屏蔽中断计数器,bit28用作PREEMPT_ACTIVE标志。
generic_handle_irq是体系结构无关函数,用来调用desc->handle_irq,该函数指针在中断初始化时指向了电流处理函数(handle_level_irq或handle_edge_irq),针对不同的中断触发类型(边沿触发或电平触发)做相应的处理。然后调用handle_IRQ_event遍历action链表从而调用该中断号对应的一个或多个中断处理程序action->handler,而action->handler就是通过request_irq初始化的。
首先分析一下handle_level_irq函数:
-
<kernel/irq/chip.c>
-
void handle_level_irq(unsigned int irq, struct irq_desc *desc)
-
{
-
struct irqaction *action;
-
irqreturn_t action_ret;
-
-
spin_lock(&desc->lock); /*访问desc内容之前先加自旋锁*/
-
mask_ack_irq(desc, irq); /*屏蔽与irq号对应的中断线 */
-
-
/* 在多处理器系统上,为了避免多cpu同时处理同一中断。
-
*当desc->status包含IRQ_INPROGRESS标志时,说明该中断
-
*正在另一个cpu上处理,因此当前cpu可以直接放弃处理。
-
*/
-
if (unlikely(desc->status & IRQ_INPROGRESS))
-
goto out_unlock;
-
desc->status &= ~(IRQ_REPLAY | IRQ_WAITING);
-
kstat_incr_irqs_this_cpu(irq, desc);
-
-
/*
-
*如果没有对该中断注册处理程序,即desc->action为NULL。
-
* 或者desc->status设置为IRQ_DISABLED,表示该中断是被禁止的。
-
* 以上两种情况只要出现一种即可放弃处理。
-
*/
-
action = desc->action;
-
if (unlikely(!action || (desc->status & IRQ_DISABLED)))
-
goto out_unlock;
-
-
desc->status |= IRQ_INPROGRESS; /*标识中断状态为正在处理*/
-
spin_unlock(&desc->lock); /*释放自旋锁*/
-
-
/*调用由request_irq注册的处理函数,稍后分析。*/
-
action_ret = handle_IRQ_event(irq, action);
-
if (!noirqdebug)
-
note_interrupt(irq, desc, action_ret);
-
-
spin_lock(&desc->lock); /*访问desc内容前加自旋锁*/
-
desc->status &= ~IRQ_INPROGRESS; /*清除“正在处理”的标识*/
-
-
/*如果desc->status 包含IRQ_ONESHOT,
-
*则将desc->status设置为IRQ_MASKED,使该中断仍处于被屏蔽状态。 */
-
if (unlikely(desc->status & IRQ_ONESHOT))
-
desc->status |= IRQ_MASKED;
-
/*如果中断处理函数中未对desc->status 设置为IRQ_ DISABLED,
-
*且desc->chip->unmask不为空,则desc->chip->unmask所指向的芯片相关函数,
-
*解除对该中断的屏蔽。
-
*/
-
else if (!(desc->status & IRQ_DISABLED) && desc->chip->unmask)
-
desc->chip->unmask(irq);
-
out_unlock:
-
spin_unlock(&desc->lock); /*释放自旋锁*/
-
}
再来介绍一下handle_edge_irq函数,相对于handle_level_irq要复杂一点:
-
<kernel/irq/chip.c>
-
void handle_edge_irq(unsigned int irq, struct irq_desc *desc)
-
{
-
spin_lock(&desc->lock);
-
desc->status &= ~(IRQ_REPLAY | IRQ_WAITING);
-
/*
-
* 如果该中断正在被其他cpu处理,或者是该中断已被禁止,
-
* 则不处理该中断,但要将其标识为pending状态且屏蔽该中断以便后续处理
-
*/
-
if (unlikely((desc->status & (IRQ_INPROGRESS | IRQ_DISABLED)) ||
-
!desc->action)) {
-
desc->status |= (IRQ_PENDING | IRQ_MASKED);
-
mask_ack_irq(desc, irq);
-
goto out_unlock;
-
}
-
kstat_incr_irqs_this_cpu(irq, desc);
-
-
/* Start handling the irq */
-
if (desc->chip->ack)
-
desc->chip->ack(irq);
-
-
/* 标识该中断状态为“正在处理”*/
-
desc->status |= IRQ_INPROGRESS;
-
-
do {
-
struct irqaction *action = desc->action;
-
irqreturn_t action_ret;
-
-
if (unlikely(!action)) {
-
desc->chip->mask(irq);
-
goto out_unlock;
-
}
-
-
/*
-
* 如果当处理该中断时有另一个中断到达,
-
* 那么当时可能屏蔽了该中断。
-
* 如果该中断没有被禁止,则解除对该中断的屏蔽。
-
*/
-
if (unlikely((desc->status &
-
(IRQ_PENDING | IRQ_MASKED | IRQ_DISABLED)) ==
-
(IRQ_PENDING | IRQ_MASKED))) {
-
desc->chip->unmask(irq);
-
desc->status &= ~IRQ_MASKED;
-
}
-
-
desc->status &= ~IRQ_PENDING;
-
spin_unlock(&desc->lock);
-
/*调用由request_irq注册的处理函数,稍后分析。*/
-
action_ret = handle_IRQ_event(irq, action);
-
if (!noirqdebug)
-
note_interrupt(irq, desc, action_ret);
-
spin_lock(&desc->lock);
-
/*如果该中断没有被禁止,并且有其他中断等待处理(IRQ_PENDING),
-
*则循环处理其他中断。
-
*/
-
} while ((desc->status & (IRQ_PENDING | IRQ_DISABLED)) == IRQ_PENDING);
-
-
desc->status &= ~IRQ_INPROGRESS;
-
out_unlock:
-
spin_unlock(&desc->lock);
-
}
不管是电平触发还是边沿触发,最终都会通过handle_IRQ_event来调用注册的中断处理函数。
-
<kernel/irq/handle.c>
-
irqreturn_t handle_IRQ_event(unsigned int irq, struct irqaction *action)
-
{
-
irqreturn_t ret, retval = IRQ_NONE;
-
unsigned int status = 0;
-
-
/*如果注册中断时没有设置IRQF_DISABLED 标志,则在此处开启硬中断!开启后允许硬中断嵌套。从2.6.36版本内核开始,IRQF_DISABLED 标志被废除,此处不再开启硬中断,以防止中断嵌套可能造成栈溢出的潜在风险。详细信息参见:http://lwn.net/Articles/380931/*/
-
if (!(action->flags & IRQF_DISABLED))
-
local_irq_enable_in_hardirq();
-
-
do {
-
trace_irq_handler_entry(irq, action);
-
/*调用由request_irq注册的中断处理函数*/
-
ret = action->handler(irq, action->dev_id);
-
trace_irq_handler_exit(irq, action, ret);
-
-
switch (ret) {
-
case IRQ_WAKE_THREAD:/*进行中断线程化处理*/
-
/*
-
* Set result to handled so the spurious check
-
* does not trigger.
-
*/
-
ret = IRQ_HANDLED;
-
-
if (unlikely(!action->thread_fn)) {
-
warn_no_thread(irq, action);
-
break;
-
}
-
-
if (likely(!test_bit(IRQTF_DIED,
-
&action->thread_flags))) {
-
set_bit(IRQTF_RUNTHREAD, &action->thread_flags);
-
/*唤醒由request_threaded_irq注册的中断处理线程*/
-
wake_up_process(action->thread);
-
}
-
-
/* Fall through to add to randomness */
-
case IRQ_HANDLED: /*中断处理函数正常返回*/
-
status |= action->flags;
-
break;
-
-
default:
-
break;
-
}
-
-
retval |= ret;
-
action = action->next; /*指向下一个中断处理函数*/
-
} while (action); /*循环调用注册在同一中断线上的中断处理函数(共享中断线)*/
-
-
/*如果注册中断时指定了IRQF_SAMPLE_RANDOM 标识,
-
*则调用add_interrupt_randomness函数,
-
*将发生中断的时间作为随机数产生器的熵 */
-
if (status & IRQF_SAMPLE_RANDOM)
-
add_interrupt_randomness(irq);
-
/*由于中断的开启和禁止不是嵌套的,所以与之前中断是否禁止不相关,
-
*由于进入handle_IRQ_event之前是禁止中断的,所以在退出时也应该禁止中断。
-
*/
-
local_irq_disable();
-
-
return retval;
-
}
完成中断处理函数调用后,返回到asm_do_IRQ继续执行,其中最重要的是执行中断退出函数irq_exit:
-
<kernel/softirq.c>
-
void irq_exit(void)
-
{
-
account_system_vtime(current);
-
trace_hardirq_exit();
-
sub_preempt_count(IRQ_EXIT_OFFSET);
-
if (!in_interrupt() && local_softirq_pending())
-
invoke_softirq();
-
-
#ifdef CONFIG_NO_HZ
-
/* Make sure that timer wheel updates are propagated */
-
rcu_irq_exit();
-
if (idle_cpu(smp_processor_id()) && !in_interrupt() && !need_resched())
-
tick_nohz_stop_sched_tick(0);
-
#endif
-
preempt_enable_no_resched();
-
}
irq_exit函数首先将preempt_count计数器减去IRQ_EXIT_OFFSET,用来标识退出硬中断,这与irq_enter函数中的add_preempt_count相对应。在没有开启内核抢占特性的系统中,IRQ_EXIT_OFFSET=HARDIRQ_OFFSET,否则IRQ_EXIT_OFFSET=(HARDIRQ_OFFSET-1),意味着如果开启内核抢占则在退出硬中断时内核要暂时禁止抢占,因为紧接着可能要处理软中断。
之后irq_exit会通过宏in_interrupt()判断当前是否处在中断(interrupt)中:
-
#define irq_count() (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK | NMI_MASK))
-
#define in_interrupt() (irq_count())
通过分析宏in_interrupt()可知,内核认为HARDIRQ、SOFTIRQ和NMI都属于中断(interrupt)。所以当irq_exit判断当前不处于中断且有软中断正等待处理,则调用invoke_softirq()来触发软中断处理函数,稍后分析。
处理完软中断后,如果内核支持动态时钟,irq_exit会做一些动态时钟相关的处理,然后会调用preempt_enable_no_resched()函数开启内核抢占。返回asm_do_IRQ,该函数最后通过set_irq_regs(old_regs)将寄存器集合指针恢复到发生中断之前的值。asm_do_IRQ结束后会返回入口点,再次回到汇编语言代码。详见前面对汇编代码的分析。
参考资料:
[1] Andrew N.Sloss, Dominic Symes, Chris Wright. ARM嵌入式系统开发——软件设计与优化. 沈建华, 译.
[2] Daniel P.Bovet, Marco Cesati. 深入理解Linux内核(第三版). 陈莉君, 张琼声, 张宏伟, 译.
[3] Pobert Love. Linux内核设计与实现. 陈莉君, 康华, 张波, 译.
[4] Wolfgang Mauerer. 深入Linux内核架构. 郭旭, 译.
[5] 陈学松. 深入Linux设备驱动程序内核机制.
[6] ARM Architecture Reference Manual
[7] The ARM-THUMB Procedure Call Standard