.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:.if4sublr,lr,#4@在中断发生时,lr指向最后执行的指令地址加上8。只有在当前指令执行完毕后,才进入中断处理,所以返回地址应指向下一条指令,即(lr-4)处。.endif@@ Save r0,lr_<exception>(parent PC)andspsr_<exception>@(parent CPSR)@stmiasp,{r0,lr}@ 保存r0,lr到irq模式下的栈中mrslr,spsrstrlr,[sp,#8]@保存spsr到irq模式下的栈中@@ PrepareforSVC32 mode.IRQs remain disabled.@mrsr0,cpsreorr0,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_svcldrlr,[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_IRQFLAGSbltrace_hardirqs_off#endifget_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后的结果写入进程内核栈的变量中#endifirq_handler @调用中断处理程序,稍后分析#ifdef CONFIG_PREEMPTldrr0,[tsk,#TI_PREEMPT]@获取preempt计数器值strr8,[tsk,#TI_PREEMPT]@将preempt恢复到中断前的值teqr0,r7 @比较中断前后preempt是否相等strner0,[r0,-r0]@如果不等,则产生异常(向地址0写入数据)?#endif#ifdef CONFIG_TRACE_IRQFLAGSbltrace_hardirqs_on#endifmovwhy,#0 @r8=0bret_to_user @中断处理完成,恢复中断上下文并返回中断产生的位置,稍后分析UNWIND(.fnend)ENDPROC(__irq_usr)
上面代码中的usr_entry是一个宏定义,主要用于保护上下文到栈中:
.macrousr_entryUNWIND(.fnstart)UNWIND(.cantunwind)@ dont unwind the user spacesubsp,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 avoidancemovr4,#-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_IRQFLAGSbltrace_hardirqs_off#endif#ifdef CONFIG_PREEMPTget_thread_info tskldrr8,[tsk,#TI_PREEMPT]@ 获取preempt计数器值addr7,r8,#1@ preempt加1,标识禁止抢占strr7,[tsk,#TI_PREEMPT]@将加1后的结果写入进程内核栈的变量中#endifirq_handler @调用中断处理程序,稍后分析#ifdef CONFIG_PREEMPTstrr8,[tsk,#TI_PREEMPT]@ 恢复中断前的preempt计数器ldrr0,[tsk,#TI_FLAGS]@ 获取flagsteqr8,#0@ 判断preempt是否等于0movner0,#0@ 如果preempt不等于0,r0=0tstr0,#_TIF_NEED_RESCHED @将r0与#_TIF_NEED_RESCHED做“与操作”blnesvc_preempt @如果不等于0,说明发生内核抢占,需要重新调度。#endifldrr0,[sp,#S_PSR]@ irqs are already disabledmsrspsr_cxsf,r0#ifdef CONFIG_TRACE_IRQFLAGStstr0,#PSR_I_BITbleqtrace_hardirqs_on#endifsvc_exit r4 @恢复中断上下文,稍后分析。UNWIND(.fnend)ENDPROC(__irq_svc)
其中svc_entry是一个宏定义,主要用于保护中断上下文到栈中:
.macrosvc_entry,stack_hole=0UNWIND(.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 avoidancemovr4,#-1@""""""""addr0,sp,#(S_FRAME_SIZE+stack_hole)SPFIX(addner0,r0,#4)strr1,[sp]@ save the"real"r0 copied@ from the exception stackmovr1,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_handlerget_irqnr_preamble r5,lr1: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.SENTRY(ret_to_user)ret_slow_syscall:disable_irq@ disable interrupts,此处不明白,disable_irq应该接受irq中断号作为参数,来禁止指定的irq号中断线。但是此处调用disable_irq之前并没有将irq中断号存入r0寄存器,这是为什么?ldrr1,[tsk,#TI_FLAGS]@获取thread_info->flagststr1,#_TIF_WORK_MASK @判断是否有待处理的workbnework_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_regsENDPROC(ret_to_user)以下是恢复中断现场寄存器的宏,就是将发生中断时保存在内核空间堆栈上的寄存器还原,可以对照图5-2所示的内核空间堆栈保存的内容来理解下面代码:.macrorestore_user_regs,fast=0,offset=0ldrr1,[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.iffastldmdbsp,{r1-lr}^@ get calling r1-lr.elseldmdbsp,{r0-lr}^ @ 存在^,所以将内核栈保存的内容恢复到用户空间的r0~lr寄存器.endifaddsp,sp,#S_FRAME_SIZE-S_PCmovspc,lr@将发生中断时的下一条指令地址存入pc,从而返回中断点继续执行,并且将发生中断时的cpsr内容恢复到cpsr寄存器中(开启中断)。.endm
如果中断产生于内核空间,则调用svc_exit来恢复中断现场:
arch/arm/kernel/ entry-header.S.macrosvc_exit,rpsrmsrspsr_cxsf,rpsr#ifdefined(CONFIG_CPU_32v6K)clrex@ clear the exclusive monitorldmiasp,{r0-pc}^@ load r0-pc,cpsr#elif defined(CONFIG_CPU_V6)ldrr0,[sp]strexr1,r2,[sp]@ clear the exclusive monitorldmibsp,{r1-pc}^@ load r1-pc,cpsr#elseldmiasp,{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%un",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);#endifpreempt_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