ARM Linux中断源码分析(2)——中断处理流程.docx
Four short words sum up what has lifted most successful individuals above the crowd: a little bit more.-author-dateARM Linux中断源码分析(2)中断处理流程ARM Linux中断源码分析(2)中断处理流程ARM Linux中断源码分析(2)中断处理流程ARM支持7类异常中断,所以中断向量表设8个条目,每个条目4字节,共32字节。 异常名称中断向量异常中断模式优先级复位0x0特权模式1未定义的指令0x4未定义指令中止模式6软件中断0x8特权模式6指令预取中止0x0c中止模式5数据访问中止0x10中止模式2保留0x14 外部中断请求IRQ0x18IRQ模式4快速中断请求FIQ0x1cFIQ模式3 回顾第一节所讲的内容,当一个异常或中断发生时,处理器会将PC设置为特定地址,从而跳转到已经初始化好的异常向量表。因此,要理清中断处理流程,先从异常向量表开始。对于ARM Linux而言,异常向量表和异常处理程序都存在arch/arm/kernel/entry_armv.S汇编文件中。vector异常向量表点击(此处)折叠或打开1. .globl _vectors_start2. _vectors_start:3. swi SYS_ERROR04. b vector_und + stubs_offset5. ldr pc, .LCvswi + stubs_offset6. b vector_pabt + stubs_offset7. b vector_dabt + stubs_offset8. b vector_addrexcptn + stubs_offset9. b vector_irq + stubs_offset 中断入口,vector_irq10. b vector_fiq + stubs_offset11.12. .globl _vectors_end13. _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处理函数在分析vector_irq处理函数之前,先了解一下当一个异常或中断导致处理器模式改变时,ARM处理器内核的处理流程如下图所示: 中断刚发生时,处理器处于irq模式。在_stubs_start和_stubs_end之间找到vector_irq处理函数的定义vector_stub irq, IRQ_MODE, 4,其中vector_stub是一个宏(在arch/arm/kernel/entry_armv.S中定义),为了分析更直观,我们将vector_stub宏展开如下:1. /*2. * Interrupt dispatcher3. */4. vector_irq:5. .if 46. sub lr, lr, #4 在中断发生时,lr指向最后执行的指令地址加上8。只有在当前指令执行完毕后,才进入中断处理,所以返回地址应指向下一条指令,即(lr-4)处。7. .endif8.9. 10. Save r0, lr_<exception> (parent PC) and spsr_<exception>11. (parent CPSR)12. 13. stmia sp, r0, lr 保存r0, lr到irq模式下的栈中14. mrs lr, spsr15. str lr, sp, #8 保存spsr到irq模式下的栈中16.17. 18. Prepare for SVC32 mode. IRQs remain disabled.19. 20. mrs r0, cpsr 21. eor r0, r0, #( IRQ_MODE SVC_MODE) 设置成SVC模式,但未切换22. msr spsr_cxsf, r0 保存到spsr_irq中23.24. 25. the branch table must immediately follow this code26. 27. and lr, lr, #0x0f lr存储着上一个处理器模式的cpsr值,lr = lr & 0x0f取出用于判断发生中断前是用户态还是核心态的信息,该值用于下面跳转表的索引。28. mov r0, sp 将irq模式下的sp保存到r0,作为参数传递给即将调用的_irq_usr或_irq_svc29. ldr lr, pc, lr, lsl #2 pc指向当前执行指令地址加8,即跳转表的基址。lr作为索引,由于是4字节对齐,所以lr = lr << 2.30. movs pc, lr branch to handler in SVC mode31. 当mov指令后加“s”且目标寄存器为pc时,当前模式下的spsr会被复制到cpsr,从而完成模式切换(从irq模式切换到svc模式)并且跳转到pc指向的指令继续执行32. ENDPROC(vector_irq)33.34. .long _irq_usr 0 (USR_26 / USR_32)35. .long _irq_invalid 1 (FIQ_26 / FIQ_32)36. .long _irq_invalid 2 (IRQ_26 / IRQ_32)37. .long _irq_svc 3 (SVC_26 / SVC_32)38. .long _irq_invalid 439. .long _irq_invalid 540. .long _irq_invalid 641. .long _irq_invalid 742. .long _irq_invalid 843. .long _irq_invalid 944. .long _irq_invalid a45. .long _irq_invalid b46. .long _irq_invalid c47. .long _irq_invalid d48. .long _irq_invalid e49. .long _irq_invalid f_irq_usr如果发生中断前处于用户态则进入_irq_usr,其定义如下(arch/arm/kernel/entry_armv.S): 1. .align 52. _irq_usr:3. usr_entry 保存中断上下文,稍后分析4. kuser_cmpxchg_check5. #ifdef CONFIG_TRACE_IRQFLAGS6. bl trace_hardirqs_off7. #endif8. get_thread_info tsk 获取当前进程的进程描述符中的成员变量thread_info的地址,并将该地址保存到寄存器tsk(r9)(在entry-header.S中定义)9. #ifdef CONFIG_PREEMPT 如果定义了抢占,增加抢占数值10. ldr r8, tsk, #TI_PREEMPT 获取preempt计数器值11. add r7, r8, #1 preempt加1,标识禁止抢占12. str r7, tsk, #TI_PREEMPT 将加1后的结果写入进程内核栈的变量中13. #endif14. irq_handler 调用中断处理程序,稍后分析15. #ifdef CONFIG_PREEMPT16. ldr r0, tsk, #TI_PREEMPT 获取preempt计数器值17. str r8, tsk, #TI_PREEMPT 将preempt恢复到中断前的值18. teq r0, r7 比较中断前后preempt是否相等19. strne r0, r0, -r0 如果不等,则产生异常(向地址0写入数据)?20. #endif21. #ifdef CONFIG_TRACE_IRQFLAGS22. bl trace_hardirqs_on23. #endif24. mov why, #0 r8=025. b ret_to_user 中断处理完成,恢复中断上下文并返回中断产生的位置,稍后分析26. UNWIND(.fnend )27. ENDPROC(_irq_usr)宏定义usr_entry(保护上下文到栈)上面代码中的usr_entry是一个宏定义,主要用于保护上下文到栈中:1. .macro usr_entry2. UNWIND(.fnstart )3. UNWIND(.cantunwind ) dont unwind the user space4. sub sp, sp, #S_FRAME_SIZE ATPCS中,堆栈被定义为递减式满堆栈,所以首先让sp向下移动#S_FRAME_SIZE(pt_regs结构体size),准备向栈中存放数据。此处的sp是svc模式下的栈指针。5. stmib sp, r1 - r126.7. ldmia r0, r1 - r38. add r0, sp, #S_PC here for interlock avoidance9. mov r4, #-1 "" "" "" ""10.11. str r1, sp save the "real" r0 copied12. from the exception stack13.14. 15. We are now ready to fill in the remaining blanks on the stack:16. 17. r2 - lr_<exception>, already fixed up for correct return/restart18. r3 - spsr_<exception>19. r4 - orig_r0 (see pt_regs definition in ptrace.h)20. 21. Also, separately save sp_usr and lr_usr22. 23. stmia r0, r2 - r424. stmdb r0, sp, lr 将user模式下的sp和lr保存到svc模式的栈中25.26. 27. Enable the alignment trap while in kernel mode28. 29. alignment_trap r030.31. 32. Clear FP to mark the first stack frame33. 34. zero_fp35. .endm上面的这段代码主要是在填充结构体pt_regs ,在include/asm/ptrace.h中定义:1. struct pt_regs 2. long uregs18;3. ;4.5. #define ARM_cpsr uregs166. #define ARM_pc uregs157. #define ARM_lr uregs148. #define ARM_sp uregs139. #define ARM_ip uregs1210. #define ARM_fp uregs1111. #define ARM_r10 uregs1012. #define ARM_r9 uregs913. #define ARM_r8 uregs814. #define ARM_r7 uregs715. #define ARM_r6 uregs616. #define ARM_r5 uregs517. #define ARM_r4 uregs418. #define ARM_r3 uregs319. #define ARM_r2 uregs220. #define ARM_r1 uregs121. #define ARM_r0 uregs022. #define ARM_ORIG_r0 uregs17usr_entry宏填充pt_regs结构体的过程如图5-2所示,先将r1r12保存到ARM_r1ARM_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如果发生中断前处于核心态则进入_irq_svc,其定义如下(arch/arm/kernel/entry_armv.S):1. .align 52. _irq_svc:3. svc_entry 保存中断上下文4.5. #ifdef CONFIG_TRACE_IRQFLAGS6. bl trace_hardirqs_off7. #endif8. #ifdef CONFIG_PREEMPT9. get_thread_info tsk10. ldr r8, tsk, #TI_PREEMPT 获取preempt计数器值11. add r7, r8, #1 preempt加1,标识禁止抢占12. str r7, tsk, #TI_PREEMPT 将加1后的结果写入进程内核栈的变量中13. #endif14.15. irq_handler 调用中断处理程序,稍后分析16. #ifdef CONFIG_PREEMPT17. str r8, tsk, #TI_PREEMPT 恢复中断前的preempt计数器18. ldr r0, tsk, #TI_FLAGS 获取flags19. teq r8, #0 判断preempt是否等于020. movne r0, #0 如果preempt不等于0,r0=021. tst r0, #_TIF_NEED_RESCHED 将r0与#_TIF_NEED_RESCHED做“与操作”22. blne svc_preempt 如果不等于0,说明发生内核抢占,需要重新调度。23. #endif24.25. ldr r0, sp, #S_PSR irqs are already disabled26. msr spsr_cxsf, r027. #ifdef CONFIG_TRACE_IRQFLAGS28. tst r0, #PSR_I_BIT29. bleq trace_hardirqs_on30. #endif31. svc_exit r4 恢复中断上下文,稍后分析。32. UNWIND(.fnend )33. ENDPROC(_irq_svc)宏定义svc_entry(保护中断上下文到栈)其中svc_entry是一个宏定义,主要用于保护中断上下文到栈中。svc_entry主要是在当前堆栈上分配一个pt_regs结构,把r0-r15以及cpsr等保存到这个结构中,在进入irq_handler时,sp指向pt_regs底端:1. .macro svc_entry, stack_hole=02. UNWIND(.fnstart )3. UNWIND(.save r0 - pc )4. sub sp, sp, #(S_FRAME_SIZE + stack_hole)5. SPFIX( tst sp, #4 )6. SPFIX( bicne sp, sp, #4 )7. stmib sp, r1 - r128.9. ldmia r0, r1 - r310. add r5, sp, #S_SP here for interlock avoidance11. mov r4, #-1 "" "" "" ""12. add r0, sp, #(S_FRAME_SIZE + stack_hole)13. SPFIX( addne r0, r0, #4 )14. str r1, sp save the "real" r0 copied15. from the exception stack16.17. mov r1, lr18.19. 20. We are now ready to fill in the remaining blanks on the stack:21. 22. r0 - sp_svc23. r1 - lr_svc24. r2 - lr_<exception>, already fixed up for correct return/restart25. r3 - spsr_<exception>26. r4 - orig_r0 (see pt_regs definition in ptrace.h)27. 28. stmia r5, r0 - r429. .endmsvc_entry宏填充pt_regs结构体的过程如图5-2所示,先将r1r12保存到ARM_r1ARM_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.irq0。附录1,arm体系下pt_regs结构struct pt_regs long uregs18;uregs0 - uregs17分别对应,r0 - r15,cpsr,ORIG_r0附录1,irq中断时堆栈的变化-spsr-lr ,中断返回地址,修正后的-r0- <-进入irq_svc之前,sp的值,也是r0的值pt_regs- <-进入svc_entry后,sp的值irq_handler(中断处理程序)保存中断上下文后则进入中断处理程序irq_handler,定义在arch/arm/kernel/entry_armv.S文件中:1. .macro irq_handler2. get_irqnr_preamble r5, lr 3. 1: get_irqnr_and_base r0, r6, r5, lr 获取中断号,存到r0中,稍后分析4. movne r1, sp 如果中断号不等于0,将r1=sp,即pt_regs结构体首地址5. 6. routine called with r0 = irq number, r1 = struct pt_regs *7. 8. adrne lr, 1b 如果r0(中断号)不等于0, lr(返回地址)等于标号1处,即get_irqnr_and_base r0, r6, r5, lr的那行,即循环处理所有的中断。9. bne asm_do_IRQ 进入中断处理,稍后分析。10. 11. .endmget_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如果中断产生于用户空间,则调用ret_to_user来恢复中断现场并返回用户空间继续运行:1. arch/arm/kernel/entry_armv.S2. ENTRY(ret_to_user)3. ret_slow_syscall:4. disable_irq disable interrupts,此处不明白,disable_irq应该接受irq中断号作为参数,来禁止指定的irq号中断线。但是此处调用disable_irq之前并没有将irq中断号存入r0寄存器,这是为什么?5. ldr r1, tsk, #TI_FLAGS 获取thread_info->flags6. tst r1, #_TIF_WORK_MASK 判断是否有待处理的work7. bne work_pending 如果有,则进入work_pending进一步处理,主要是完成用户进程抢占相关处理。8. no_work_pending: 如果没有work待处理,则准备恢复中断现场,返回用户空间。9. /* perform architecture specific actions before user return */10. arch_ret_to_user r1, lr 调用体系结构相关的代码11.12. restore_user_regs fast = 0, offset = 0 调用restore_user_regs13. ENDPROC(ret_to_user)14.15. 以下是恢复中断现场寄存器的宏,就是将发生中断时保存在内核空间堆栈上的寄存器还原,可以对照图5-2所示的内核空间堆栈保存的内容来理解下面代码:16. .macro restore_user_regs, fast = 0, offset = 017. ldr r1, sp, #offset + S_PSR 从内核栈中获取发生中断时的cpsr值18. ldr lr, sp, #offset + S_PC! 从内核栈中获取发生中断时的下一条指令地址19. msr spsr_cxsf, r1 将r1保存到spsr_svc20. #if defined(CONFIG_CPU_32v6K)21. clrex clear the exclusive monitor22. #elif defined (CONFIG_CPU_V6)23. strex r1, r2, sp clear the exclusive monitor24. #endif25. .if fast26. ldmdb sp, r1 - lr get calling r1 - lr27. .else28. ldmdb sp, r0 - lr 存在,所以将内核栈保存的内容恢复到用户空间的r0lr寄存器29. .endif30. add sp, sp, #S_FRAME_SIZE - S_PC 31. movs pc, lr 将发生中断时的下一条指令地址存入pc,从而返回中断点继续执行,并且将发生中断时的cpsr内容恢复到cpsr寄存器中(开启中断)。32. .endmsvc_exit如果中断产生于内核空间,则调用svc_exit来恢复中断现场:1. arch/arm/kernel/ entry-header.S2. .macro svc_exit, rpsr3. msr spsr_cxsf, rpsr4. #if defined(CONFIG_CPU_32v6K)5. clrex clear the exclusive monitor6. ldmia sp, r0 - pc load r0 - pc, cpsr7. #elif defined (CONFIG_CPU_V6)8. ldr r0, sp9. strex r1, r2, sp clear the exclusive monitor10. ldmib sp, r1 - pc load r1 - pc, cpsr11. #else12. ldmia sp, r0 - pc 返回内核空间时,恢复中断现场比较简单,就是将r0-pc以及cpsr恢复即可,同时中断也被开启。13. #endif14. .endmasm_do_IRQ函数ok,分析完所有与中断相关的汇编语言代码后,下面开始分析C语言代码:在arch/arm/kernel/irq.c文件中找到asm_do_IRQ函数定义:1. asmlinkage void _exception asm_do_IRQ(unsigned int irq, struct pt_regs *regs)2. 3. /*保存新的寄存器集合指针到全局cpu变量,方便后续处理程序访问寄存器集合。*/4. struct pt_regs *old_regs = set_irq_regs(regs); 5.6. irq_enter();7.8. /*9. * Some hardware gives randomly wrong interrupts. Rather10. * than crashing, do something sensible.11. */12. if (unlikely(irq >= NR_IRQS) /判断中断号13. if (printk_ratelimit()14. printk(KERN_WARNING "Bad IRQ%un", irq);15. ack_bad_irq(irq);16. else 17. generic_handle_irq(irq); /调用中断处理函数18. 19.20. /* AT91 specific workaround */21. irq_finish(irq);22.23. irq_exit();24. set_irq_regs(old_regs);25. asm_do_IRQ是中断处理的C入口函数,主要负责调用request_irq注册的中断处理函数,其流程如图5-4所示:图5-4 asm_do_IRQ流程1、old_regs = set_irq_regs(regs)其中,set_irq_regs将指向寄存器结构体的指针保存在一个全局的CPU变量中,后续的程序可以通过该变量访问寄存器结构体。所以在进入中断处理前,先将全局CPU变量中保存的旧指针保留下来,等到中断处理结束后再将其恢复。2、irq_enterirq_enter负责更新一些统计量:1. <kernel/softirq.c>2. void irq_enter(void)3