zoukankan      html  css  js  c++  java
  • 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_ ame:  
    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_ ame)</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()函数实现向量表的设定操作。

  • 相关阅读:
    EFI下WIN8.1和Ubuntu的双系统安装
    硬盘损坏,全盘数据没了,杯具
    GEC2440的RTC时钟
    纠正一下apache2服务器的搭建
    qt和html的比较
    dump做个备份,发个随笔记录下
    忙了1天的qte-arm环境的搭建
    内核版本不同导致无法加载驱动
    wayne生产环境部署(360的容器发布平台-开源)
    openstack swift curl 常用操作
  • 原文地址:https://www.cnblogs.com/sky-heaven/p/5321556.html
Copyright © 2011-2022 走看看