linux中断系统那些事之----中断处理过程【转】

以外部中断irq为例来说明,当外部硬件产生中断时,linux的处理过程。首先先说明当外部中断产生时,硬件处理器所做的工作如下:

R14_irq = address of next instruction to be executed + 4/*将寄存器lr_mode设置成返回地址,即为当前pc的值,因为pc是当前执行指令的下两条指令*/

       SPSR_irq = CPSR                /*保存处理器当前状态、中断屏蔽位以及各条件标志位*/

       CPSR[4:0] = 0b10010         /*设置当前程序状态寄存器CPSR中相应的位进入IRQ模式,注意cpsr是所有模式共享的*/

       CPSR[5] = 0                        /*在ARM状态执行*/

                                          /*CPSR[6] 不变*/

       CPSR[7] = 1                       /*禁止正常中断*/

       If high vectors configured then

              PC=0xFFFF0018          /*将程序计数器(PC)值设置成该异常中断的中断向量地址,从而跳转到相应的异常中断处理程序处执行,对于ARMv7向量表普遍中断是0xFFFF0018*/

       else

              PC=0x00000018     /*对于低向量*/

假设在用户空间时,产生了外部硬件中断,则这个时候的指令跳转流程如下:
[cpp] view plain copy
 
  1. __vectors_start:---------------〉在中断向量表被拷贝后,该地址就是0xffff0000.  
  2.  ARM( swi SYS_ERROR0 )  
  3.  THUMB( svc #0 )  
  4.  THUMB( nop )  
  5. W(b) vector_und + stubs_offset  
  6. W(ldr) pc, .LCvswi + stubs_offset  
  7. W(b) vector_pabt + stubs_offset  
  8. W(b) vector_dabt + stubs_offset  
  9. W(b) vector_addrexcptn + stubs_offset  
  10. W(b)vector_irq + stubs_offset----------〉当外部中断产生时,pc直接指向这个地址。  
  11. W(b) vector_fiq + stubs_offset  
  12. .globl __vectors_end  
下面的vector_stubirq, IRQ_MODE, 4语句,展开就是vector_irq,所以上述语句跳转到如下语句执行:
[cpp] view plain copy
 
  1. __stubs_start:  
  2. /* 
  3.  * Interrupt dispatcher 
  4.  */  
  5. vector_stub irq, IRQ_MODE, 4  
  6.   
  7.   
  8. .long __irq_usr@  0  (USR_26 / USR_32)  
  9. .long __irq_invalid@  1  (FIQ_26 / FIQ_32)  
  10. .long __irq_invalid@  2  (IRQ_26 / IRQ_32)  
  11. .long __irq_svc@  3  (SVC_26 / SVC_32)  
  12. .long __irq_invalid@  4  
  13. .long __irq_invalid@  5  
  14. .long __irq_invalid@  6  
  15. .long __irq_invalid@  7  
  16. .long __irq_invalid@  8  
  17. .long __irq_invalid@  9  
  18. .long __irq_invalid@  a  
  19. .long __irq_invalid@  b  
  20. .long __irq_invalid@  c  
  21. .long __irq_invalid@  d  
  22. .long __irq_invalid@  e  
  23. .long __irq_invalid@  f  
vector_stubirq, IRQ_MODE, 4语句展开如下:
[cpp] view plain copy
 
  1. <span style="font-size:18px">/* 
  2.  * Vector stubs. 
  3.  * 
  4.  * This code is copied to 0xffff0200 so we can use branches in the 
  5.  * vectors, rather than ldr's.  Note that this code must not 
  6.  * exceed 0x300 bytes. 
  7.  * 
  8.  * Common stub entry macro: 
  9.  *   Enter in IRQ mode, spsr = SVC/USR CPSR, lr = SVC/USR PC 
  10.  * 
  11.  * SP points to a minimal amount of processor-private memory, the address 
  12.  * of which is copied into r0 for the mode specific abort handler. 
  13.  */  
  14.     .macro  vector_stub, name, mode, correction=0  
  15.     .align  5  
  16.   
  17. vector_\name:  
  18.     .if \correction  
  19.     sub lr, lr, #\correction  //因为硬件处理器是将当前指令的下两条指令的地址存储在lr寄存器中,所以这里需要减4,让他指向被中断指令的下一条,这样当中断被恢复时,可以继续被中断的指令继续执行。  
  20.     .endif<span style="white-space:pre">            </span>      //需要注意的是,这个时候的lr寄存器,已经是irq模式下的私有寄存器了,在中断产生时,硬件处理器已经自动为他赋了值。  
  21.   
  22.     @  
  23.     @ Save r0, lr_<exception> (parent PC) and spsr_<exception>  
  24.     @ (parent CPSR)  
  25.     @  
  26.     stmia   sp, {r0, lr}        @ save r0, lr//保存r0和lr寄存器,即被中断的下一条指令  
  27.     mrs lr, spsr  
  28.     str lr, [sp, #8]        @ save spsr  
  29.   
  30.     @  
  31.     @ Prepare for SVC32 mode.  IRQs remain disabled.//准备从中断模式切换到管理模式,不同的模式,对应各自不同的堆栈。  
  32.     @  
  33.     mrs r0, cpsr      
  34.     eor r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE)  
  35.     msr spsr_cxsf, r0  
  36.   
  37.     @  
  38.     @ the branch table must immediately follow this code  
  39.     @  
  40.     and lr, lr, #0x0f           //获取被中断前,处理器所处的模式  
  41.  THUMB( adr r0, 1f          )  
  42.  THUMB( ldr lr, [r0, lr, lsl #2]    )  
  43.     mov r0, sp<span style="white-space:pre">            </span>//让r0寄存器指向中断模式下堆栈的基地址  
  44.  ARM(   ldr lr, [pc, lr, lsl #2]    )  
  45.     movs    pc, lr          @ branch to handler in SVC mode,同时将中断模式下的spsr_irq(irq私有的)赋值给cpsr(该寄存器所有模式共享)  
  46. ENDPROC(vector_\name)</span>  
 
此时中断模式下的私有栈sp的存储情况如下:(注意这个时候的sp是中断模式下的堆栈sp),并且这个时候r0寄存器中,保存有sp的指针值,由于r0已经被保存到堆栈,所以可以放心被使用
根据被中断时,处理器模式的不同,分别跳转到__irq_usr和__irq_svc两个分支。
在这里我们以__irq_usr为例来说明:
[cpp] view plain copy
 
  1. <span style="font-size:18px">__irq_usr:  
  2.     usr_entry       //进行中断前的硬件上下文的保存  
  3.     kuser_cmpxchg_check  
  4.     irq_handler  
  5.     get_thread_info tsk//获取被中断的用户进程或内核线程所对应的内核栈所对应的thread info结构。  
  6.     mov why, #0  
  7.     b   ret_to_user_from_irq//恢复被中断时的上下文,然后继续被中断的进程或线程的执行  
  8.  UNWIND(.fnend      )  
  9. ENDPROC(__irq_usr)</span>  
 
usr_entry展开如下:
 
[cpp] view plain copy
 
  1. .macro  usr_entry  
  2. UNWIND(.fnstart )  
  3. UNWIND(.cantunwind  )   @ don't unwind the user space  
  4. sub sp, sp, #S_FRAME_SIZE   // #S_FRAME_SIZE的值为72  
  5. ARM(    stmib   sp, {r1 - r12}  )      //尽管当前是处于管理模式,但由于svc和usr的r0-r12是公共的,所以相当于保存用户模式的r1-r12寄存器  
  6. THUMB(  stmia   sp, {r0 - r12}  )  
  7.   
  8. ldmia   r0, {r3 - r5}          //将之前保存在中断模式堆栈中的r0_usr,lr,spsr分别存储到r3-r5中  
  9. add r0, sp, #S_PC       @ here for interlock avoidance #S_PC=60  
  10. mov r6, #-1         @  ""  ""     ""        ""  
  11.   
  12. str r3, [sp]        @ save the "real" r0 copied  
  13.                 @ from the exception stack  
  14.   
  15. @  
  16. @ We are now ready to fill in the remaining blanks on the stack:  
  17. @  
  18. @  r4 - lr_<exception>, already fixed up for correct return/restart  
  19. @  r5 - spsr_<exception>  
  20. @  r6 - orig_r0 (see pt_regs definition in ptrace.h)  
  21. @  
  22. @ Also, separately save sp_usr and lr_usr  
  23. @  
  24. stmia   r0, {r4 - r6}  
  25. ARM(    stmdb   r0, {sp, lr}^           )//保存用户模式下的sp_usr,lr_usr  
  26. THUMB(  store_user_sp_lr r0, r1, S_SP - S_PC    )  
  27.   
  28. @  
  29. @ Enable the alignment trap while in kernel mode  
  30. @  
  31. alignment_trap r0  
  32.   
  33. @  
  34. @ Clear FP to mark the first stack frame  
  35. @  
  36. zero_fp  
  37.   
  38. ifdef CONFIG_IRQSOFF_TRACER  
  39. bl  trace_hardirqs_off  
  40. endif  
  41. .endm  
至此,用户模式下所有的寄存器都被正确保存了,并且处理器模式中irq模式成功切换到管理模式,并且sp这个时候是指向保存r0_usr寄存器值得地方。此时的管理模式的内核栈分布如下:
 
需要说明的是:上图中的lr_irq即为用户模式下被中断指令的下一条指令,spsr_irq即为用户模式下被中断时的cpsr寄存器。
在这里说明下,中断时寄存器的保存是有固定的顺序的,他们顺序即如下所示:
cpsr(r16)
pc(r15)
lr(r14)
sp(r13)
r12(ip)
r11(fp)
r10
r9
r8
r7
r6
r5
r4
r3
r2
r1
r0

上图中的S_FRAME_SIZE, S_PC在arch/arm/kernel/Asm-offsets.c:中定义

  DEFINE(S_FRAME_SIZE,     sizeof(struct pt_regs));

  DEFINE(S_PC,         offsetof(struct pt_regs, ARM_pc));

include/asm-arm/Ptrace.h:

struct pt_regs {

    long uregs[18];

};

#define ARM_pc     uregs[15]

呵呵,pt_regs中对应的就是上面栈上的18个寄存器,ARM_pc是pc寄存器存放在这个数组中的偏移。

 

接着看get_thread_info, 它也是个宏,用来获取当前线程的地址。他的结构体定义如下:

include/linux/Sched.h:

union thread_union {

    struct thread_info thread_info;  /*线程属性*/

    unsigned long stack[THREAD_SIZE/sizeof(long)];  /*栈*/

};

由它定义的线程是8K字节对齐的, 并且在这8K的最低地址处存放的就是thread_info对象,即该栈拥有者线程的对象,而get_thread_info就是通过把sp低13位清0(8K边界)来获取当前thread_info对象的地址。

   arch/arm/kernel/entry-armv.S:

    .macro  get_thread_info, rd

    mov /rd, sp, lsr #13

    mov /rd, /rd, lsl #13

    .endm

调用该宏后寄存器tsk里存放的就是当前线程的地址了, tsk是哪个寄存器呢,呵呵我们在看:

arch/arm/kernel/entry-header.S:

tsk .req    r9      @ current thread_info

呵呵,tsk只是r9的别名而已, 因此这时r9里保存的就是当前线程的地址。

 
为了将汇编部分讲完,我们继续研究ret_to_user_from_irq函数,该函数展开后,如下:
[cpp] view plain copy
 
  1. <span style="font-size:18px">ENTRY(ret_to_user_from_irq)  
  2.     ldr r1, [tsk, #TI_FLAGS] //tsk如上所述,是r9寄存器的别名,并且是指向thread_info结构体的  
  3.     tst r1, #_TIF_WORK_MASK  //检测是否有待处理的任务  
  4.     bne work_pending  
  5. no_work_pending:  
  6. #if defined(CONFIG_IRQSOFF_TRACER)  
  7.     asm_trace_hardirqs_on  
  8. #endif  
  9.     /* perform architecture specific actions before user return */  
  10.     arch_ret_to_user r1, lr    //针对arm,是dummy的  
  11.   
  12.     restore_user_regs fast = 0, offset = 0//恢复之前用户模式时被中断时所保存的寄存器上下文  
  13. ENDPROC(ret_to_user_from_irq)</span>  
restore_user_regs展开如下: 
[cpp] view plain copy
 
  1. <span style="font-size:18px">   .macro  restore_user_regs, fast = 0, offset = 0  
  2.     ldr r1, [sp, #\offset + S_PSR]  @ get calling cpsr 即为被中断时,处理器的cpsr值  
  3.     ldr lr, [sp, #\offset + S_PC]!  @ get pc         即为被中断指令的,下一条指令  
  4.     msr spsr_cxsf, r1           @ save in spsr_svc  //将r1赋值给管理模式下的spsr_svc,这样在movs时,会自动将该值赋值为cpsr  
  5. #if defined(CONFIG_CPU_V6)  
  6.     strex   r1, r2, [sp]            @ clear the exclusive monitor  
  7. #elif defined(CONFIG_CPU_32v6K)  
  8.     clrex                   @ clear the exclusive monitor  
  9. #endif  
  10.     .if \fast  
  11.     ldmdb   sp, {r1 - lr}^          @ get calling r1 - lr  
  12.     .else  
  13.     ldmdb   sp, {r0 - lr}^          @ get calling r0 - lr,将保存在内核栈中的r0到r14恢复到用户模式中的寄存器  
  14.     .endif  
  15.     mov r0, r0              @ ARMv5T and earlier require a nop  
  16.                         @ after ldm {}^  
  17.     add sp, sp, #S_FRAME_SIZE - S_PC    //恢复内核栈到中断产生之前的位置。  
  18.     movs    pc, lr              @ return & move spsr_svc into cpsr  
  19.     .endm  
  20.   
  21. </span>  

至此中断汇编部分已经全部处理完成。
 
最后摘录部门网上经典的问题解答:

问题1:vector_irq已经是异常、中断处理的入口函数了,为什么还要加stubs_offset?(  b    vector_irq + stubs_offset)

答:(1)内核刚启动时(head.S文件)通过设置CP15的c1寄存器已经确定了异常向量表的起始地址(例如0xffff0000),因此需要把已经写好的内核代码中的异常向量表考到0xffff0000处,只有这样在发生异常时内核才能正确的处理异常。

(2)从上面代码看出向量表和stubs(中断处理函数)都发生了搬移,如果还用b vector_irq,那么实际执行的时候就无法跳转到搬移后的vector_irq处,因为指令码里写的是原来的偏移量,所以需要把指令码中的偏移量写成搬移后的。至于为什么搬移后的地址是vector_irq+stubs_offset,请参考我的上篇blog:linux中断系统那些事之----中断初始化过程

问题2:为什么在异常向量表中,用b指令跳转而不是用ldr绝对跳转?

答:因为使用b指令跳转比绝对跳转(ldr pc,XXXX)效率高,正因为效率高,所以把__stubs_start~__stubs_end之间的代码考到了0xffff0200起始处。

注意:

因为b跳转指令只能在+/-32MB之内跳转,所以必须拷贝到0xffff0000附近。

b指令是相对于当前PC的跳转,当汇编器看到 B 指令后会把要跳转的标签转化为相对于当前PC的偏移量写入指令码。

问题3:为什么首先进入head.S开始执行?

答:内核源代码顶层目录下的Makefile制定了vmlinux生成规则:

# vmlinux image - includingupdated kernel symbols

vmlinux: $(vmlinux-lds)$(vmlinux-init) $(vmlinux-main) vmlinux.o $(kallsyms.o)FORCE

其中$(vmlinux-lds)是编译连接脚本,对于ARM平台,就是arch/arm/kernel/vmlinux-lds文件。vmlinux-init也在顶层Makefile中定义:

vmlinux-init := $(head-y)$(init-y)

head-y 在arch/arm/Makefile中定义:

head-y:=arch/arm/kernel/head$(MMUEX T).o arch/arm/kernel/init_task.o

ifeq ($(CONFIG_MMU),)

MMUEXT := -nommu

endif

对于有MMU的处理器,MMUEXT为空白字符串,所以arch/arm/kernel/head.O 是第一个连接的文件,而这个文件是由arch/arm/kernel/head.S编译产生成的。

综合以上分析,可以得出结论,非压缩ARM Linux内核的入口点在arch/arm/kernel/head.s中。

问题4: 中断为什么必须进入svc模式?

一个最重要原因是:

如果一个中断模式(例如从usr进入irq模式,在irq模式中)中重新允许了中断,并且在这个中断例程中使用了BL指令调用子程序,BL指令会自动将子程序返回地址保存到当前模式的sp(即r14_irq)中,这个地址随后会被在当前模式下产生的中断所破坏,因为产生中断时CPU会将当前模式的PC保存到r14_irq,这样就把刚刚保存的子程序返回地址冲掉。为了避免这种情况,中断例程应该切换到SVC或者系统模式,这样的话,BL指令可以使用r14_svc来保存子程序的返回地址。

问题5:为什么跳转表中有的用了b指令跳转,而有的用了ldr  px,xxxx?

         W(b)         vector_und+ stubs_offset

         W(ldr)      pc, .LCvswi + stubs_offset

         W(b)         vector_pabt+ stubs_offset

         W(b)         vector_dabt+ stubs_offset

         W(b)         vector_addrexcptn+ stubs_offset

         W(b)         vector_irq+ stubs_offset      

         W(b)         vector_fiq+ stubs_offset

 

.LCvswi:

         .word       vector_swi

由于系统调用异常的代码编译在其他文件中,其入口地址与异常向量相隔较远,使用b指令无法跳转过去(b指令只能相对当前pc跳转32M范围)。因此将其地址存放到LCvswi中,并从内存地址中加载其入口地址,原理与其他调用是一样的。这也就是为什么系统调用的速度稍微慢一点的原因。

问题6:为什么ARM能处理中断?

因为ARM架构的CPU有一个机制,只要中断产生了,CPU就会根据中断类型自动跳转到某个特定的地址(即中断向量表中的某个地址)。如下表所示,既是中断向量表。


 ARM中断向量表及地址

问题7:什么是High vector?

A:在Linux3.1.0,arch/arm/include/asm/system.hline121 有定义如下:

#if __LINUX_ARM_ARCH__ >=4

#define vectors_high()  (cr_alignment & CR_V)

#else

#define vectors_high()  (0)

#endif

意思就是,如果使用的ARM架构大于等于4,则定义vectors_high()=cr_alignment&CR_V,该值就等于0xffff0000

在Linux3.1.0,arch/arm/include/asm/system.hline33有定义如下:

#define CR_V   (1 << 13)       /* Vectors relocated to 0xffff0000 */

 

arm下规定,在0x00000000或0xffff0000的地址处必须存放一张跳转表。

问题8:中断向量表是如何存放到0x00000000或0xffff0000地址的?

A:Uboot执行结束后会把Linux内核拷贝到内存中开始执行,linux内核执行的第一条指令是linux/arch/arm/kernel/head.S,此文件中执行一些参数设置等操作后跳入linux/init/main.c文件的start_kernel函数,此函数调用一系列初始化函数,其中trip_init()函数实现向量表的设定操作。

posted @ 2016-03-25 23:26  Sky&Zhang  阅读(1418)  评论(0编辑  收藏  举报