Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1637641
  • 博文数量: 511
  • 博客积分: 967
  • 博客等级: 准尉
  • 技术积分: 2560
  • 用 户 组: 普通用户
  • 注册时间: 2012-07-06 14:19
文章分类

全部博文(511)

文章存档

2016年(11)

2015年(61)

2014年(257)

2013年(63)

2012年(119)

分类: LINUX

2014-05-12 01:09:02

转载请注明出处:wloveg.blog.chinaunix.net

5.中断处理流程

回顾第一节所讲的内容,当一个异常或中断发生时,处理器会将PC设置为特定地址,从而跳转到已经初始化好的异常向量表。因此,要理清中断处理流程,先从异常向量表开始。对于ARM Linux而言,异常向量表和异常处理程序都存在arch/arm/kernel/entry_armv.S汇编文件中。

异常向量表的代码实现:

点击(此处)折叠或打开

  1. .globl    __vectors_start
  2. __vectors_start:
  3.     swi    SYS_ERROR0
  4.     b    vector_und + stubs_offset
  5.     ldr    pc, .LCvswi + stubs_offset
  6.     b    vector_pabt + stubs_offset
  7.     b    vector_dabt + stubs_offset
  8.     b    vector_addrexcptn + stubs_offset
  9.     b    vector_irq + stubs_offset @中断入口,vector_irq
  10.     b    vector_fiq + stubs_offset

  11.     .globl    __vectors_end
  12. __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宏展开如下:

  1. /*
  2.  * Interrupt dispatcher
  3.  */
  4.     vector_irq:
  5.     .if 4
  6.     sub    lr, lr, #4 @在中断发生时,lr指向最后执行的指令地址加上8。只有在当前指令执行完毕后,才进入中断处理,所以返回地址应指向下一条指令,即(lr-4)处。
  7.     .endif

  8. @
  9.     @ Save r0, lr_<exception> (parent PC) and spsr_<exception>
  10.     @ (parent CPSR)
  11.     @
  12.     stmia    sp, {r0, lr}        @ 保存r0, lr到irq模式下的栈中
  13.     mrs    lr, spsr
  14.     str    lr, [sp, #8]        @保存spsr到irq模式下的栈中

  15.     @
  16.     @ Prepare for SVC32 mode. IRQs remain disabled.
  17.     @
  18.     mrs    r0, cpsr
  19.     eor    r0, r0, #( IRQ_MODE ^ SVC_MODE) @设置成SVC模式,但未切换
  20.     msr    spsr_cxsf, r0 @保存到spsr_irq中

  21.     @
  22.     @ the branch table must immediately follow this code
  23.     @
  24.     and    lr, lr, #0x0f @lr存储着上一个处理器模式的cpsr值,lr = lr & 0x0f取出用于判断发生中断前是用户态还是核心态的信息,该值用于下面跳转表的索引。
  25.     mov    r0, sp @将irq模式下的sp保存到r0,作为参数传递给即将调用的__irq_usr或__irq_svc
  26.     ldr    lr, [pc, lr, lsl #2] @pc指向当前执行指令地址加8,即跳转表的基址。lr作为索引,由于是4字节对齐,所以lr = lr << 2.
  27.     movs    pc, lr @ branch to handler in SVC mode
  28.                   @当mov指令后加“s”且目标寄存器为pc时,当前模式下的spsr会被复制到cpsr,从而完成模式切换(从irq切换到svc)并且跳转到pc指向的指令继续执行
  29. ENDPROC(vector_irq)

  30.     .long    __irq_usr            @ 0 (USR_26 / USR_32)
  31.     .long    __irq_invalid            @ 1 (FIQ_26 / FIQ_32)
  32.     .long    __irq_invalid            @ 2 (IRQ_26 / IRQ_32)
  33.     .long    __irq_svc            @ 3 (SVC_26 / SVC_32)
  34.     .long    __irq_invalid            @ 4
  35.     .long    __irq_invalid            @ 5
  36.     .long    __irq_invalid            @ 6
  37.     .long    __irq_invalid            @ 7
  38.     .long    __irq_invalid            @ 8
  39.     .long    __irq_invalid            @ 9
  40.     .long    __irq_invalid            @ a
  41.     .long    __irq_invalid            @ b
  42.     .long    __irq_invalid            @ c
  43.     .long    __irq_invalid            @ d
  44.     .long    __irq_invalid            @ e
  45.     .long    __irq_invalid            @ f
如果发生中断前处于用户态则进入__irq_usr,其定义如下(arch/arm/kernel/entry_armv.S): 

  1. .align    5
  2. __irq_usr:
  3.     usr_entry @保存中断上下文,稍后分析
  4.     kuser_cmpxchg_check
  5. #ifdef CONFIG_TRACE_IRQFLAGS
  6.     bl    trace_hardirqs_off
  7. #endif
  8.     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. #endif
  14.     irq_handler @调用中断处理程序,稍后分析
  15. #ifdef CONFIG_PREEMPT
  16.     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. #endif
  21. #ifdef CONFIG_TRACE_IRQFLAGS
  22.     bl    trace_hardirqs_on
  23. #endif
  24.     mov    why, #0 @r8=0
  25.     b    ret_to_user @中断处理完成,恢复中断上下文并返回中断产生的位置,稍后分析
  26.  UNWIND(.fnend        )
  27. ENDPROC(__irq_usr)

上面代码中的usr_entry是一个宏定义,主要用于保护上下文到栈中:

  1. .macro    usr_entry
  2.  UNWIND(.fnstart    )
  3.  UNWIND(.cantunwind    )    @ dont unwind the user space
  4.     sub    sp, sp, #S_FRAME_SIZE @ATPCS中,堆栈被定义为递减式满堆栈,所以首先让sp向下移动#S_FRAME_SIZE(pt_regs结构体size),准备向栈中存放数据。此处的sp是svc模式下的栈指针。
  5.     stmib    sp, {r1 - r12}

  6.     ldmia    r0, {r1 - r3}
  7.     add    r0, sp, #S_PC        @ here for interlock avoidance
  8.     mov    r4, #-1            @ "" "" "" ""

  9.     str    r1, [sp]        @ save the "real" r0 copied
  10.                     @ from the exception stack

  11.     @
  12.     @ We are now ready to fill in the remaining blanks on the stack:
  13.     @
  14.     @ r2 - lr_<exception>, already fixed up for correct return/restart
  15.     @ r3 - spsr_<exception>
  16.     @ r4 - orig_r0 (see pt_regs definition in ptrace.h)
  17.     @
  18.     @ Also, separately save sp_usr and lr_usr
  19.     @
  20.     stmia    r0, {r2 - r4}
  21.     stmdb    r0, {sp, lr}^ @将user模式下的sp和lr保存到svc模式的栈中

  22.     @
  23.     @ Enable the alignment trap while in kernel mode
  24.     @
  25.     alignment_trap r0

  26.     @
  27.     @ Clear FP to mark the first stack frame
  28.     @
  29.     zero_fp
  30.     .endm
上面的这段代码主要是在填充结构体pt_regs ,在include/asm/ptrace.h中定义:

  1. struct pt_regs {
  2.     long uregs[18];
  3. };

  4. #define ARM_cpsr    uregs[16]
  5. #define ARM_pc        uregs[15]
  6. #define ARM_lr        uregs[14]
  7. #define ARM_sp        uregs[13]
  8. #define ARM_ip        uregs[12]
  9. #define ARM_fp        uregs[11]
  10. #define ARM_r10        uregs[10]
  11. #define ARM_r9        uregs[9]
  12. #define ARM_r8        uregs[8]
  13. #define ARM_r7        uregs[7]
  14. #define ARM_r6        uregs[6]
  15. #define ARM_r5        uregs[5]
  16. #define ARM_r4        uregs[4]
  17. #define ARM_r3        uregs[3]
  18. #define ARM_r2        uregs[2]
  19. #define ARM_r1        uregs[1]
  20. #define ARM_r0        uregs[0]
  21. #define ARM_ORIG_r0    uregs[17]

usr_entry宏填充pt_regs结构体的过程如图5-2所示,先将r1r12保存到ARM_r1ARM_ip(绿色部分),然后将产生中断时的r0寄存器内容保存到ARM_r0(蓝色部分),接下来将产生中断时的下一条指令地址lr_irqspsr_irqr4保存到ARM_pcARM_cpsrARM_ORIG_r0(红色部分),最后将用户模式下的splr保存到ARM_sp ARM_lr 中。

5-2 usr_entry宏填充pt_regs结构体

如果发生中断前处于核心态则进入__irq_svc,其定义如下(arch/arm/kernel/entry_armv.S):


  1. .align    5
  2. __irq_svc:
  3.     svc_entry @保存中断上下文

  4. #ifdef CONFIG_TRACE_IRQFLAGS
  5.     bl    trace_hardirqs_off
  6. #endif
  7. #ifdef CONFIG_PREEMPT
  8.     get_thread_info tsk
  9.     ldr    r8, [tsk, #TI_PREEMPT]        @ 获取preempt计数器值
  10.     add    r7, r8, #1            @ preempt加1,标识禁止抢占
  11.     str    r7, [tsk, #TI_PREEMPT] @将加1后的结果写入进程内核栈的变量中
  12. #endif

  13.     irq_handler @调用中断处理程序,稍后分析
  14. #ifdef CONFIG_PREEMPT
  15.     str    r8, [tsk, #TI_PREEMPT]        @ 恢复中断前的preempt计数器
  16.     ldr    r0, [tsk, #TI_FLAGS]        @ 获取flags
  17.     teq    r8, #0                @ 判断preempt是否等于0
  18.     movne    r0, #0                @ 如果preempt不等于0,r0=0
  19.     tst    r0, #_TIF_NEED_RESCHED @将r0与#_TIF_NEED_RESCHED做“与操作”
  20.     blne    svc_preempt @如果不等于0,说明发生内核抢占,需要重新调度。
  21. #endif

  22.     ldr    r0, [sp, #S_PSR]        @ irqs are already disabled
  23.     msr    spsr_cxsf, r0
  24. #ifdef CONFIG_TRACE_IRQFLAGS
  25.     tst    r0, #PSR_I_BIT
  26.     bleq    trace_hardirqs_on
  27. #endif
  28.     svc_exit r4     @恢复中断上下文,稍后分析。
  29.  UNWIND(.fnend        )
  30. ENDPROC(__irq_svc)
其中svc_entry是一个宏定义,主要用于保护中断上下文到栈中:

  1. .macro    svc_entry, stack_hole=0
  2.  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 - r12}

  8.     ldmia    r0, {r1 - r3}
  9.     add    r5, sp, #S_SP        @ here for interlock avoidance
  10.     mov    r4, #-1            @ "" "" "" ""
  11.     add    r0, sp, #(S_FRAME_SIZE + \stack_hole)
  12.  SPFIX(    addne    r0, r0, #4    )
  13.     str    r1, [sp]        @ save the "real" r0 copied
  14.                     @ from the exception stack

  15.     mov    r1, lr

  16.     @
  17.     @ We are now ready to fill in the remaining blanks on the stack:
  18.     @
  19.     @ r0 - sp_svc
  20.     @ r1 - lr_svc
  21.     @ r2 - lr_<exception>, already fixed up for correct return/restart
  22.     @ r3 - spsr_<exception>
  23.     @ r4 - orig_r0 (see pt_regs definition in ptrace.h)
  24.     @
  25.     stmia    r5, {r0 - r4}
  26.     .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_svcirq模式下的栈栈指针sp_irqsp_usr指向在setup_arg_pages函数中创建的用户空间栈。sp_svc指向在alloc_thread_info函数中创建的内核空间栈。sp_irqcpu_init函数中被赋值,指向全局变量stacks.irq[0]

保存中断上下文后则进入中断处理程序——irq_handler,定义在arch/arm/kernel/entry_armv.S文件中:


  1. .macro    irq_handler
  2.     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.     .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来恢复中断现场并返回用户空间继续运行:

  1. arch/arm/kernel/entry_armv.S
  2. 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->flags
  6.     tst    r1, #_TIF_WORK_MASK @判断是否有待处理的work
  7.     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.     restore_user_regs fast = 0, offset = 0 @调用restore_user_regs
  12. ENDPROC(ret_to_user)

  13. 以下是恢复中断现场寄存器的宏,就是将发生中断时保存在内核空间堆栈上的寄存器还原,可以对照图5-2所示的内核空间堆栈保存的内容来理解下面代码:
  14. .macro    restore_user_regs, fast = 0, offset = 0
  15.     ldr    r1, [sp, #\offset + S_PSR]    @ 从内核栈中获取发生中断时的cpsr值
  16.     ldr    lr, [sp, #\offset + S_PC]!    @ 从内核栈中获取发生中断时的下一条指令地址
  17.     msr    spsr_cxsf, r1            @ 将r1保存到spsr_svc
  18. #if defined(CONFIG_CPU_32v6K)
  19.     clrex                    @ clear the exclusive monitor
  20. #elif defined (CONFIG_CPU_V6)
  21.     strex    r1, r2, [sp]            @ clear the exclusive monitor
  22. #endif
  23.     .if    \fast
  24.     ldmdb    sp, {r1 - lr}^    @ get calling r1 - lr
  25.     .else
  26.     ldmdb    sp, {r0 - lr}^ @ 存在^,所以将内核栈保存的内容恢复到用户空间的r0~lr寄存器
  27.     .endif
  28.     add    sp, sp, #S_FRAME_SIZE - S_PC
  29.     movs    pc, lr    @将发生中断时的下一条指令地址存入pc,从而返回中断点继续执行,并且将发生中断时的cpsr内容恢复到cpsr寄存器中(开启中断)。
  30.     .endm

如果中断产生于内核空间,则调用svc_exit来恢复中断现场:


  1. arch/arm/kernel/ entry-header.S
  2. .macro    svc_exit, rpsr
  3.     msr    spsr_cxsf, \rpsr
  4. #if defined(CONFIG_CPU_32v6K)
  5.     clrex                    @ clear the exclusive monitor
  6.     ldmia    sp, {r0 - pc}^            @ load r0 - pc, cpsr
  7. #elif defined (CONFIG_CPU_V6)
  8.     ldr    r0, [sp]
  9.     strex    r1, r2, [sp]            @ clear the exclusive monitor
  10.     ldmib    sp, {r1 - pc}^            @ load r1 - pc, cpsr
  11. #else
  12.     ldmia    sp, {r0 - pc}^            @ 返回内核空间时,恢复中断现场比较简单,就是将r0-pc以及cpsr恢复即可,同时中断也被开启。
  13. #endif
  14.     .endm

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.     irq_enter();

  6.     /*
  7.      * Some hardware gives randomly wrong interrupts. Rather
  8.      * than crashing, do something sensible.
  9.      */
  10.     if (unlikely(irq >= NR_IRQS)) { //判断中断号
  11.         if (printk_ratelimit())
  12.             printk(KERN_WARNING "Bad IRQ%u\n", irq);
  13.         ack_bad_irq(irq);
  14.     } else {
  15.         generic_handle_irq(irq); //调用中断处理函数
  16.     }

  17.     /* AT91 specific workaround */
  18.     irq_finish(irq);

  19.     irq_exit();
  20.     set_irq_regs(old_regs);
  21. }

asm_do_IRQ是中断处理的C入口函数,主要负责调用request_irq注册的中断处理函数,其流程如图5-4所示:


5-4 asm_do_IRQ流程

其中,set_irq_regs将指向寄存器结构体的指针保存在一个全局的CPU变量中,后续的程序可以通过该变量访问寄存器结构体。所以在进入中断处理前,先将全局CPU变量中保存的旧指针保留下来,等到中断处理结束后再将其恢复。irq_enter负责更新一些统计量:


  1. <kernel/softirq.c>
  2. void irq_enter(void)
  3. {
  4.     int cpu = smp_processor_id();

  5.     rcu_irq_enter();
  6.     if (idle_cpu(cpu) && !in_interrupt()) {
  7.         __irq_enter();
  8.         tick_check_idle(cpu);
  9.     } else
  10.         __irq_enter();
  11. }

如果系统开启动态时钟特性且很长时间没有产生时钟中断,则调用tick_check_idle更新全局变量jiffies(关于动态时钟特性,在后续的总结中再进行分析)。宏__irq_enter()定义如下:


  1. #define __irq_enter()                    \
  2.     do {                        \
  3.         account_system_vtime(current);        \
  4.         add_preempt_count(HARDIRQ_OFFSET);    \
  5.         trace_hardirq_enter();            \
  6.     } while (0)

add_preempt_count(HARDIRQ_OFFSET)使表示中断处理程序嵌套层次的计数器加1。计数器保存在当前进程thread_info结构的preempt_count字段中:

5-5 preempt_count结构

内核将preempt_count分成5部分:bit0~7PREEMPT相关,bit8~15用作软中断计数器,bit16~25用作硬中断计数器,bit26用作不可屏蔽中断计数器,bit28用作PREEMPT_ACTIVE标志。

generic_handle_irq是体系结构无关函数,用来调用desc->handle_irq,该函数指针在中断初始化时指向了电流处理函数(handle_level_irqhandle_edge_irq),针对不同的中断触发类型(边沿触发或电平触发)做相应的处理。然后调用handle_IRQ_event遍历action链表从而调用该中断号对应的一个或多个中断处理程序action->handler,而action->handler就是通过request_irq初始化的。

首先分析一下handle_level_irq函数:


  1. <kernel/irq/chip.c>
  2. void handle_level_irq(unsigned int irq, struct irq_desc *desc)
  3. {
  4.     struct irqaction *action;
  5.     irqreturn_t action_ret;

  6.     spin_lock(&desc->lock); /*访问desc内容之前先加自旋锁*/
  7.     mask_ack_irq(desc, irq); /*屏蔽与irq号对应的中断线 */

  8. /* 在多处理器系统上,为了避免多cpu同时处理同一中断。
  9. *当desc->status包含IRQ_INPROGRESS标志时,说明该中断
  10. *正在另一个cpu上处理,因此当前cpu可以直接放弃处理。
  11. */
  12.     if (unlikely(desc->status & IRQ_INPROGRESS))
  13.         goto out_unlock;
  14.     desc->status &= ~(IRQ_REPLAY | IRQ_WAITING);
  15.     kstat_incr_irqs_this_cpu(irq, desc);

  16.     /*
  17.      *如果没有对该中断注册处理程序,即desc->action为NULL。
  18.      * 或者desc->status设置为IRQ_DISABLED,表示该中断是被禁止的。
  19.      * 以上两种情况只要出现一种即可放弃处理。
  20. */
  21.     action = desc->action;
  22.     if (unlikely(!action || (desc->status & IRQ_DISABLED)))
  23.         goto out_unlock;

  24.     desc->status |= IRQ_INPROGRESS; /*标识中断状态为正在处理*/
  25.     spin_unlock(&desc->lock); /*释放自旋锁*/

  26.     /*调用由request_irq注册的处理函数,稍后分析。*/
  27.     action_ret = handle_IRQ_event(irq, action);
  28.     if (!noirqdebug)
  29.         note_interrupt(irq, desc, action_ret);

  30.     spin_lock(&desc->lock); /*访问desc内容前加自旋锁*/
  31.     desc->status &= ~IRQ_INPROGRESS; /*清除“正在处理”的标识*/

  32. /*如果desc->status 包含IRQ_ONESHOT,
  33. *则将desc->status设置为IRQ_MASKED,使该中断仍处于被屏蔽状态。 */
  34.     if (unlikely(desc->status & IRQ_ONESHOT))
  35.         desc->status |= IRQ_MASKED;
  36. /*如果中断处理函数中未对desc->status 设置为IRQ_ DISABLED,
  37. *且desc->chip->unmask不为空,则desc->chip->unmask所指向的芯片相关函数,
  38. *解除对该中断的屏蔽。
  39. */
  40.     else if (!(desc->status & IRQ_DISABLED) && desc->chip->unmask)
  41.         desc->chip->unmask(irq);
  42. out_unlock:
  43.     spin_unlock(&desc->lock); /*释放自旋锁*/
  44. }

再来介绍一下handle_edge_irq函数,相对于handle_level_irq要复杂一点:


  1. <kernel/irq/chip.c>
  2. void handle_edge_irq(unsigned int irq, struct irq_desc *desc)
  3. {
  4.     spin_lock(&desc->lock);
  5.     desc->status &= ~(IRQ_REPLAY | IRQ_WAITING);
  6.     /*
  7.      * 如果该中断正在被其他cpu处理,或者是该中断已被禁止,
  8.      * 则不处理该中断,但要将其标识为pending状态且屏蔽该中断以便后续处理
  9.      */
  10.     if (unlikely((desc->status & (IRQ_INPROGRESS | IRQ_DISABLED)) ||
  11.          !desc->action)) {
  12.         desc->status |= (IRQ_PENDING | IRQ_MASKED);
  13.         mask_ack_irq(desc, irq);
  14.         goto out_unlock;
  15.     }
  16.     kstat_incr_irqs_this_cpu(irq, desc);

  17.     /* Start handling the irq */
  18.     if (desc->chip->ack)
  19.         desc->chip->ack(irq);

  20.     /* 标识该中断状态为“正在处理”*/
  21.     desc->status |= IRQ_INPROGRESS;

  22.     do {
  23.         struct irqaction *action = desc->action;
  24.         irqreturn_t action_ret;

  25.         if (unlikely(!action)) {
  26.             desc->chip->mask(irq);
  27.             goto out_unlock;
  28.         }

  29.         /*
  30.          * 如果当处理该中断时有另一个中断到达,
  31.          * 那么当时可能屏蔽了该中断。
  32.          * 如果该中断没有被禁止,则解除对该中断的屏蔽。
  33.          */
  34.         if (unlikely((desc->status &
  35.              (IRQ_PENDING | IRQ_MASKED | IRQ_DISABLED)) ==
  36.              (IRQ_PENDING | IRQ_MASKED))) {
  37.             desc->chip->unmask(irq);
  38.             desc->status &= ~IRQ_MASKED;
  39.         }

  40.         desc->status &= ~IRQ_PENDING;
  41.         spin_unlock(&desc->lock);
  42.         /*调用由request_irq注册的处理函数,稍后分析。*/
  43.         action_ret = handle_IRQ_event(irq, action);
  44.         if (!noirqdebug)
  45.             note_interrupt(irq, desc, action_ret);
  46.         spin_lock(&desc->lock);
  47.         /*如果该中断没有被禁止,并且有其他中断等待处理(IRQ_PENDING),
  48. *则循环处理其他中断。
  49. */
  50.     } while ((desc->status & (IRQ_PENDING | IRQ_DISABLED)) == IRQ_PENDING);

  51.     desc->status &= ~IRQ_INPROGRESS;
  52. out_unlock:
  53.     spin_unlock(&desc->lock);
  54. }

不管是电平触发还是边沿触发,最终都会通过handle_IRQ_event来调用注册的中断处理函数。


  1. <kernel/irq/handle.c>
  2. irqreturn_t handle_IRQ_event(unsigned int irq, struct irqaction *action)
  3. {
  4.     irqreturn_t ret, retval = IRQ_NONE;
  5.     unsigned int status = 0;

  6. /*如果注册中断时没有设置IRQF_DISABLED 标志,则在此处开启硬中断!开启后允许硬中断嵌套。从2.6.36版本内核开始,IRQF_DISABLED 标志被废除,此处不再开启硬中断,以防止中断嵌套可能造成栈溢出的潜在风险。详细信息参见:http://lwn.net/Articles/380931/*/
  7.     if (!(action->flags & IRQF_DISABLED))
  8.         local_irq_enable_in_hardirq();

  9.     do {
  10.         trace_irq_handler_entry(irq, action);
  11.         /*调用由request_irq注册的中断处理函数*/
  12. ret = action->handler(irq, action->dev_id);
  13.         trace_irq_handler_exit(irq, action, ret);

  14.         switch (ret) {
  15.         case IRQ_WAKE_THREAD:/*进行中断线程化处理*/
  16.             /*
  17.              * Set result to handled so the spurious check
  18.              * does not trigger.
  19.              */
  20.             ret = IRQ_HANDLED;

  21.             if (unlikely(!action->thread_fn)) {
  22.                 warn_no_thread(irq, action);
  23.                 break;
  24.             }

  25.             if (likely(!test_bit(IRQTF_DIED,
  26.                      &action->thread_flags))) {
  27.                 set_bit(IRQTF_RUNTHREAD, &action->thread_flags);
  28.             /*唤醒由request_threaded_irq注册的中断处理线程*/
  29.                 wake_up_process(action->thread);
  30.             }

  31.             /* Fall through to add to randomness */
  32.         case IRQ_HANDLED: /*中断处理函数正常返回*/
  33.             status |= action->flags;
  34.             break;

  35.         default:
  36.             break;
  37.         }

  38.         retval |= ret;
  39.         action = action->next; /*指向下一个中断处理函数*/
  40.     } while (action); /*循环调用注册在同一中断线上的中断处理函数(共享中断线)*/

  41. /*如果注册中断时指定了IRQF_SAMPLE_RANDOM 标识,
  42. *则调用add_interrupt_randomness函数,
  43. *将发生中断的时间作为随机数产生器的熵 */
  44.     if (status & IRQF_SAMPLE_RANDOM)
  45.         add_interrupt_randomness(irq);
  46. /*由于中断的开启和禁止不是嵌套的,所以与之前中断是否禁止不相关,
  47.  *由于进入handle_IRQ_event之前是禁止中断的,所以在退出时也应该禁止中断。
  48. */
  49.     local_irq_disable();

  50.     return retval;
  51. }

完成中断处理函数调用后,返回到asm_do_IRQ继续执行,其中最重要的是执行中断退出函数irq_exit


  1. <kernel/softirq.c>
  2. void irq_exit(void)
  3. {
  4.     account_system_vtime(current);
  5.     trace_hardirq_exit();
  6.     sub_preempt_count(IRQ_EXIT_OFFSET);
  7.     if (!in_interrupt() && local_softirq_pending())
  8.         invoke_softirq();

  9. #ifdef CONFIG_NO_HZ
  10.     /* Make sure that timer wheel updates are propagated */
  11.     rcu_irq_exit();
  12.     if (idle_cpu(smp_processor_id()) && !in_interrupt() && !need_resched())
  13.         tick_nohz_stop_sched_tick(0);
  14. #endif
  15.     preempt_enable_no_resched();
  16. }

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)中:

  1. #define irq_count()    (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK | NMI_MASK))
  2. #define in_interrupt()        (irq_count())


通过分析宏in_interrupt()可知,内核认为HARDIRQSOFTIRQNMI都属于中断(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

















阅读(722) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~