.equstubs_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,所以中断入口点为“bvector_irq + stubs_offset”,其中减去irq_PC是由汇编器在编译时完成的。
在分析vector_irq处理函数之前,先了解一下当一个异常或中断导致处理器模式改变时,ARM处理器内核的处理流程如下图所示:
在__stubs_start和__stubs_end之间找到vector_irq处理函数的定义vector_stubirq, IRQ_MODE, 4,其中vector_stub是一个宏(在arch/arm/kernel/entry_armv.S中定义),为了分析更直观,我们将vector_stub宏展开如下:
/*
*Interrupt dispatcher
*/
vector_irq:
.if4
sublr,lr,#4@在中断发生时,lr指向最后执行的指令地址加上8。只有在当前指令执行完毕后,才进入中断处理,所以返回地址应指向下一条指令,即(lr-4)处。
.endif
@
@ Save r0,lr_<exception>(parent PC)andspsr_<exception>
@(parent CPSR)
@
stmiasp,{r0,lr}@ 保存r0,lr到irq模式下的栈中
mrslr,spsr
strlr,[sp,#8]@保存spsr到irq模式下的栈中
@
@ PrepareforSVC32 mode.IRQs remain disabled.
@
mrsr0,cpsr
eorr0,r0,#(IRQ_MODE ^ SVC_MODE)@设置成SVC模式,但未切换
msrspsr_cxsf,r0 @保存到spsr_irq中
@
@ the branch table must immediately followthiscode
@
andlr,lr,#0x0f @lr存储着上一个处理器模式的cpsr值,lr=lr & 0x0f取出用于判断发生中断前是用户态还是核心态的信息,该值用于下面跳转表的索引。
movr0,sp @将irq模式下的sp保存到r0,作为参数传递给即将调用的__irq_usr或__irq_svc
ldrlr,[pc,lr,lsl#2]@pc指向当前执行指令地址加8,即跳转表的基址。lr作为索引,由于是4字节对齐,所以lr=lr<<2.
movspc,lr @ branch to handlerinSVC 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):
.align5
__irq_usr:
usr_entry @保存中断上下文,稍后分析
kuser_cmpxchg_check
#ifdef CONFIG_TRACE_IRQFLAGS
bltrace_hardirqs_off
#endif
get_thread_info tsk @获取当前进程的进程描述符中的成员变量thread_info的地址,并将该地址保存到寄存器tsk(r9)(在entry-header.S中定义)
#ifdef CONFIG_PREEMPT @如果定义了抢占,增加抢占数值
ldrr8,[tsk,#TI_PREEMPT]@ 获取preempt计数器值
addr7,r8,#1@ preempt加1,标识禁止抢占
strr7,[tsk,#TI_PREEMPT]@将加1后的结果写入进程内核栈的变量中
#endif
irq_handler @调用中断处理程序,稍后分析
#ifdef CONFIG_PREEMPT
ldrr0,[tsk,#TI_PREEMPT]@获取preempt计数器值
strr8,[tsk,#TI_PREEMPT]@将preempt恢复到中断前的值
teqr0,r7 @比较中断前后preempt是否相等
strner0,[r0,-r0]@如果不等,则产生异常(向地址0写入数据)?
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
bltrace_hardirqs_on
#endif
movwhy,#0 @r8=0
bret_to_user @中断处理完成,恢复中断上下文并返回中断产生的位置,稍后分析
UNWIND(.fnend)
ENDPROC(__irq_usr)
上面代码中的usr_entry是一个宏定义,主要用于保护上下文到栈中:
.macrousr_entry
UNWIND(.fnstart)
UNWIND(.cantunwind)@ dont unwind the user space
subsp,sp,#S_FRAME_SIZE @ATPCS中,堆栈被定义为递减式满堆栈,所以首先让sp向下移动#S_FRAME_SIZE(pt_regs结构体size),准备向栈中存放数据。此处的sp是svc模式下的栈指针。
stmibsp,{r1-r12}
ldmiar0,{r1-r3}
addr0,sp,#S_PC@ hereforinterlock avoidance
movr4,#-1@""""""""
strr1,[sp]@ save the"real"r0 copied
@ from the exception stack
@
@ We are now ready to fillinthe remaining blanks on the stack:
@
@ r2-lr_<exception>,already fixed upforcorrect return/restart
@ r3-spsr_<exception>
@ r4-orig_r0(see pt_regs definitioninptrace.h)
@
@ Also,separately save sp_usrandlr_usr
@
stmiar0,{r2-r4}
stmdbr0,{sp,lr}^ @将user模式下的sp和lr保存到svc模式的栈中
@
@ Enable the alignment trapwhileinkernel mode
@
alignment_trap r0
@
@ Clear FP to mark the first stack frame
@
zero_fp
.endm
上面的这段代码主要是在填充结构体pt_regs ,在include/asm/ptrace.h中定义:
structpt_regs {
long uregs[18];
};
#define ARM_cpsruregs[16]
#define ARM_pcuregs[15]
#define ARM_lruregs[14]
#define ARM_spuregs[13]
#define ARM_ipuregs[12]
#define ARM_fpuregs[11]
#define ARM_r10uregs[10]
#define ARM_r9uregs[9]
#define ARM_r8uregs[8]
#define ARM_r7uregs[7]
#define ARM_r6uregs[6]
#define ARM_r5uregs[5]
#define ARM_r4uregs[4]
#define ARM_r3uregs[3]
#define ARM_r2uregs[2]
#define ARM_r1uregs[1]
#define ARM_r0uregs[0]
#define ARM_ORIG_r0uregs[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):
.align5
__irq_svc:
svc_entry @保存中断上下文
#ifdef CONFIG_TRACE_IRQFLAGS
bltrace_hardirqs_off
#endif
#ifdef CONFIG_PREEMPT
get_thread_info tsk
ldrr8,[tsk,#TI_PREEMPT]@ 获取preempt计数器值
addr7,r8,#1@ preempt加1,标识禁止抢占
strr7,[tsk,#TI_PREEMPT]@将加1后的结果写入进程内核栈的变量中
#endif
irq_handler @调用中断处理程序,稍后分析
#ifdef CONFIG_PREEMPT
strr8,[tsk,#TI_PREEMPT]@ 恢复中断前的preempt计数器
ldrr0,[tsk,#TI_FLAGS]@ 获取flags
teqr8,#0@ 判断preempt是否等于0
movner0,#0@ 如果preempt不等于0,r0=0
tstr0,#_TIF_NEED_RESCHED @将r0与#_TIF_NEED_RESCHED做“与操作”
blnesvc_preempt @如果不等于0,说明发生内核抢占,需要重新调度。
#endif
ldrr0,[sp,#S_PSR]@ irqs are already disabled
msrspsr_cxsf,r0
#ifdef CONFIG_TRACE_IRQFLAGS
tstr0,#PSR_I_BIT
bleqtrace_hardirqs_on
#endif
svc_exit r4 @恢复中断上下文,稍后分析。
UNWIND(.fnend)
ENDPROC(__irq_svc)
其中svc_entry是一个宏定义,主要用于保护中断上下文到栈中:
.macrosvc_entry,stack_hole=0
UNWIND(.fnstart)
UNWIND(.save {r0-pc})
subsp,sp,#(S_FRAME_SIZE+\stack_hole)
SPFIX(tstsp,#4)
SPFIX(bicnesp,sp,#4)
stmibsp,{r1-r12}
ldmiar0,{r1-r3}
addr5,sp,#S_SP@ hereforinterlock avoidance
movr4,#-1@""""""""
addr0,sp,#(S_FRAME_SIZE+\stack_hole)
SPFIX(addner0,r0,#4)
strr1,[sp]@ save the"real"r0 copied
@ from the exception stack
movr1,lr
@
@ We are now ready to fillinthe remaining blanks on the stack:
@
@ r0-sp_svc
@ r1-lr_svc
@ r2-lr_<exception>,already fixed upforcorrect return/restart
@ r3-spsr_<exception>
@ r4-orig_r0(see pt_regs definitioninptrace.h)
@
stmiar5,{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文件中:
.macroirq_handler
get_irqnr_preamble r5,lr
1:get_irqnr_and_base r0,r6,r5,lr @获取中断号,存到r0中,稍后分析
movner1,sp @如果中断号不等于0,将r1=sp,即pt_regs结构体首地址
@
@ routine called with r0=irq number,r1=structpt_regs*
@
adrnelr,1b @如果r0(中断号)不等于0, lr(返回地址)等于标号1处,即get_irqnr_and_base r0,r6,r5,lr的那行,即循环处理所有的中断。
bneasm_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寄存器,这是为什么?
ldrr1,[tsk,#TI_FLAGS]@获取thread_info->flags
tstr1,#_TIF_WORK_MASK @判断是否有待处理的work
bnework_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所示的内核空间堆栈保存的内容来理解下面代码:
.macrorestore_user_regs,fast=0,offset=0
ldrr1,[sp,#\offset+S_PSR]@ 从内核栈中获取发生中断时的cpsr值
ldrlr,[sp,#\offset+S_PC]!@ 从内核栈中获取发生中断时的下一条指令地址
msrspsr_cxsf,r1@ 将r1保存到spsr_svc
#ifdefined(CONFIG_CPU_32v6K)
clrex@ clear the exclusive monitor
#elif defined(CONFIG_CPU_V6)
strexr1,r2,[sp]@ clear the exclusive monitor
#endif
.if\fast
ldmdbsp,{r1-lr}^@ get calling r1-lr
.else
ldmdbsp,{r0-lr}^ @ 存在^,所以将内核栈保存的内容恢复到用户空间的r0~lr寄存器
.endif
addsp,sp,#S_FRAME_SIZE-S_PC
movspc,lr@将发生中断时的下一条指令地址存入pc,从而返回中断点继续执行,并且将发生中断时的cpsr内容恢复到cpsr寄存器中(开启中断)。
.endm
如果中断产生于内核空间,则调用svc_exit来恢复中断现场:
arch/arm/kernel/ entry-header.S
.macrosvc_exit,rpsr
msrspsr_cxsf,\rpsr
#ifdefined(CONFIG_CPU_32v6K)
clrex@ clear the exclusive monitor
ldmiasp,{r0-pc}^@ load r0-pc,cpsr
#elif defined(CONFIG_CPU_V6)
ldrr0,[sp]
strexr1,r2,[sp]@ clear the exclusive monitor
ldmibsp,{r1-pc}^@ load r1-pc,cpsr
#else
ldmiasp,{r0-pc}^@ 返回内核空间时,恢复中断现场比较简单,就是将r0-pc以及cpsr恢复即可,同时中断也被开启。
#endif
.endm
ok,分析完所有与中断相关的汇编语言代码后,下面开始分析C语言代码:
在arch/arm/kernel/irq.c文件中找到asm_do_IRQ函数定义:
asmlinkage void __exception asm_do_IRQ(unsignedintirq,structpt_regs*regs)
{
/*保存新的寄存器集合指针到全局cpu变量,方便后续处理程序访问寄存器集合。*/
structpt_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)
{
intcpu=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(unsignedintirq,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所指向的芯片相关函数,
*解除对该中断的屏蔽。
*/
elseif(!(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(unsignedintirq,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(unsignedintirq,struct irqaction*action)
{
irqreturn_t ret,retval=IRQ_NONE;
unsignedintstatus=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){
caseIRQ_WAKE_THREAD:/*进行中断线程化处理*/
/*
*Setresulttohandled so the spurious check
*doesnottrigger.
*/
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 throughtoaddtorandomness*/
caseIRQ_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