ARM Linux中断机制分析

——以用户模式产生irq中断为例

以下代码基于内核linux2.6.38.3(trimslice官网下载)

本文主要分析ARM发生中断时的处理流程,以在usr态发生IRQ为例,即usr—>irq为例讨论。

 

 

 

1.内核异常向量表的初始化

1.1初始化大致流程

ARM linux内核启动时,首先运行的是linux/arch/arm/kernel/head.S,进行一些初始化工作,然后调用main.c->start_kernel()函数,进而调用trap_init()(或者调用early_trap_init()函数进行初始化)、init_IRQ()函数进行中断初始化、建立异常向量表.

1.2异常向量表的建立

异常向量表的建立过程就是拷贝过程,为了将内核代码写成位置无关的,有很多地方需要注意。

1.2.1异常向量表基地址确定

在ARM V4及V4T以后的大部分处理器中,中断向量表的位置可以有两个位置:一个是0x00000000,另一个是0xffff0000。可以通过CP15协处理器c1寄存器中V位(bit[13])控制。V和中断向量表的对应关系如下:

V=0        ~        0x00000000~0x0000001C

V=1        ~        0xffff0000~0xffff001C

注:CP15控制寄存器说明详见ARM ARMB4-1690.

在异常向量表初始化前运行的文件linux/arch/arm/kernel/head.S中设置了CP15寄存器(在 ~/arch/arm/mm/proc-v7.S文件中的__v7_setup函数中设置),这里通过设置CP15c1寄存器已经确定了异常向量表的基地址(0xffff0000)。

 

1.2.2 异常向量表拷贝过程

内核代码编译生成后,需要将异常向量表拷贝到指定位置(0x00000000 or 0xffff0000),这就需要将内核中的异常向量表设计成与位置无关的。

本文所使用内核版本使用了early_trap_init()代替trap_init()来初始化异常。

early_trap_init()在linux/arch/arm/kernel/traps.c中,代码如下:

 

1、CONFIG_VECTORS_BASE在处理器型号确定后就已经确定,其值在内核配置完成后自动生成,保存在.config文件中。本文使用内核版本在maketrimslice_deconfig后自动生成的.config中定义:CONFIG_VECTORS_BASE=0xffff0000,也就是说,异常向量表的基地址0xffff0000。

~/arch/arm/kernel/traps.c   line783

 

void __init early_trap_init(void)

{

#if defined(CONFIG_CPU_USE_DOMAINS)

         unsigned long vectors = CONFIG_VECTORS_BASE;                //vectors是中断向量基地址

#else

         unsigned long vectors = (unsigned long)vectors_page;

#endif

/*以下这些都在arch/arm/kernel/entry-armv.S下定义*/

         extern char __stubs_start[], __stubs_end[];

         extern char __vectors_start[], __vectors_end[];

         extern char __kuser_helper_start[], __kuser_helper_end[];

         int kuser_sz = __kuser_helper_end - __kuser_helper_start;

 

         /*

          * Copy the vectors, stubs and kuser helpers (in entry-armv.S)

          * into the vector page, mapped at 0xffff0000, and ensure these

          * are visible to the instruction stream.

          */

/*__vectors_end __vectors_start之间为异常向量表。__stubs_end __stubs_start之间是异常处理的位置。这些变量定义都在arch/arm/kernel/entry-armv.S*/

 

         memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);

         memcpy((void *)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start);

         memcpy((void *)vectors + 0x1000 - kuser_sz, __kuser_helper_start, kuser_sz);

         /*

          * Do processor specific fixups for the kuser helpers

          */

         kuser_get_tls_init(vectors);

         /*

          * Copy signal return handlers into the vector page, and

          * set sigreturn to be a pointer to these.

          */

         memcpy((void *)(vectors + KERN_SIGRETURN_CODE - CONFIG_VECTORS_BASE),

                sigreturn_codes, sizeof(sigreturn_codes));

         memcpy((void *)(vectors + KERN_RESTART_CODE - CONFIG_VECTORS_BASE),

                syscall_restart_code, sizeof(syscall_restart_code));

         flush_icache_range(vectors, vectors + PAGE_SIZE);

         modify_domain(DOMAIN_USER, DOMAIN_CLIENT);

} 


以下是__vectors_start, __vectors_end,__stubs_end__stubs_start的定义。

  

arch/arm/kernel/entry-armv.S

         .globl       __vectors_start

__vectors_start:

 ARM(     swi   SYS_ERROR0   )

 THUMB(         svc   #0             )

 THUMB(         nop                   )

         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

         .globl       __vectors_end

 

         .globl       __stubs_start

__stubs_start:

/*

 * Interrupt dispatcher

 */

        vector stub irq,IRQ_MODE,4    //vector_stub是一个宏,展开后是一块代码,后面紧跟着跳转表

          .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

……

……

……

……

         .globl       __stubs_end

__stubs_end:

 

         .equ stubs_offset, __vectors_start + 0x200 - __stubs_start

 

vector_stub     irq, IRQ_MODE, 4  展开后如下:

// -------------------------------- begin 展开

.align       5                //将异常入口强制进行2^5字节对齐,即一个cache line大小对齐,出于性能考虑

vector_irq:

       sub  lr, lr, 4         //需要调整pc返回值,对于irq异常,将lr减去4,对于其他异常需要作出不同调整

      

       @ Save r0, lr_<exception> (parent PC) and spsr_<exception>

       @ (parent CPSR)

       @

       stmia       sp, {r0, lr}            @ save r0, lr

       mrs  lr, spsr

       str   lr, [sp, #8]             @ save spsr 

      

       @ Prepare for SVC32 mode.  IRQs remain disabled.

       @

       mrs  r0, cpsr

       eor   r0, r0, IRQ_MODE ^ SVC_MODE)

       msr  spsr_cxsf, r0

      

       @ the branch table must immediately follow this code

       @

       and  lr, lr, #0x0f

       mov r0, sp

       ldr   lr, [pc, lr, lsl #2]

       movs      pc, lr                     @ branch to handler in SVC mode

       // -------------------------------- end 展开

 


异常向量表的拷贝过程用图表示比较清晰,如下图所示:

图一  向量表搬移及offset偏移量计算示图

图一说明:上面两条有方向的横线,横线方向代表地址生长方向,下面那个是Code/Load视图,是搬移前的代码在生成的二进制内核中的组织情况,上面的Exec view是代码在内存中开始执行后的分配情况。

 

2.linux对ARM异常、中断的处理流程

2.1当IRQ发生时,硬件完成的操作

R14_irq= address of next instruction to be executed + 4/*将寄存器lr_mode设置成返回地址*/

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

       CPSR[4:0] = 0b10010         /*设置当前程序状态寄存器CPSR中相应的位进入IRQ模式*/

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

                                                 /*CPSR[6] 不变*/

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

       If high vectors configured then

              PC=0xFFFF0018          /*将程序计数器(PC)值设置成该异常中断的中断向量地址,从

*而跳转到相应的异常中断处理程序处执行,对于ARMv7向量表普遍是0xFFFF0018

*/

       else

              PC=0x00000018

 

2.2 指令流跳转过程

以上CPU操作完成后,PC跳转到0xFFFF0018,该地址就是指令W(b) vector_irq + stubs_offset所在地址。然后跳转到vector_stub         irq,IRQ_MODE, 4去执行相应的异常、中断处理函数。

接下来具体看代码:

         .globl        __vectors_start                       //异常向量表开始0xFFFF0000

__vectors_start:

 ARM(     swi   SYS_ERROR0   )

 THUMB(         svc   #0             )

 THUMB(         nop                    )

         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       //中断发生后的跳转地址0xFFFF0018

         W(b)         vector_fiq + stubs_offset

         .globl        __vectors_end

__vectors_end:

 

stubs_offset只是个偏移量,用来修正跳转地址的,主要的操作是vector_irq执行。vector_irq是由宏vector_stub  irq,IRQ_MODE,4(IRQ_MODE在include\asm\ptrace.h中定义:0x12)生成。以下是vector_irq生成后的代码(汇编代码中,@开始的语句、///**/都代表注释)

.align       5

vector_irq:

       sub  lr, lr, 4                                        //因为异常发生时,cpupc地址+4赋值给lr,这里做修正。     

       @ Save r0, lr_<exception> (parent PC) and spsr_<exception>

       @ (parent CPSR)

       @

       stmia       sp, {r0, lr}       //保存r0, lr,到irq堆栈中(每个异常都有属于自己的堆栈)

       mrs  lr, spsr                                 //lr保存spsr_irq的值,即usr状态的cpsr的值(见2.1

       str   lr, [sp, #8]             // 保存 spsr[sp+8]

     

       @ Prepare for SVC32 mode.  IRQs remain disabled.

       @

       mrs  r0, cpsr

       eor   r0, r0,#( IRQ_MODE ^ SVC_MODE| PSR_ISETSTATE)    //  PSR_ISETSTATE:选择ARM/Thumb指令集                                             

       msr  spsr_cxsf, r0                              //这里的cxsf表示从低到高分别占用的48bit的数据域

 

 

异或运算是可以交换位置的,也即A^B^C等价于A^C^B。所以这里的r0^( IRQ_MODE ^ SVC_MODE| PSR_ISETSTATE)等价于r0^ IRQ_MODE ^SVC_MODE,由于r0的低5位模式位与IRQ_MODE相同,所以r0^ IRQ_MODE的运算结果的低5位全被清零,然后再^SVC_MODE,也即低5位被设置为SVC_MODE其它位保持不变。

       @ the branch table must immediately follow this code

       and  lr, lr, #0x0f               //提取发生异常前的处理器模式,这里也就是usr模式

       mov  r0, sp              

       ldr   lr, [pc, lr, lsl #2]                                             

       movs      pc, lr                    

 

 


sp是SVC32模式下的堆栈指针,这里将它移到r0中,就可以作为C函数的第一个参数,即C函数中的pt_regs参数。

pc是当前地址+8,也就是本段代码后面紧跟的跳转表的基地址,lr用于在跳转表中索引,lr左移两位等同于*4,因为每个条目是4字节。从usr模式进入irq模式,则lr=pc+4*0,若从svc模式进入irq,则lr=pc+4*3,即__irq_svc的地址,其他地址进入__irq_invalid出错处理,因为不能从其他模式进入irq异常。

假设这里是从usr进入irq,则执行跳转表中的第一条指令。跳转的基准地址为当前pc,因为ARMv4是三级流水线结构的,它总是指向当前指令的下两条指令的地址,尽管以后版本的指令流水线扩展为5级和8级,但是这一特性一直被兼容处理,也即pc(excute)=pc(fetch) + 8,其中:pc(fetch)是当前正在执行的指令,就是之前取该指令时候的PC的值;pc(execute):当前指令执行的,计算中如果用到pc,是指此时pc的值。

mov指令的目标寄存器是PC,且指令以S结束,则它会把spsr的值恢复给cpsr,上面说到当前的spsr中保存的是r0的值,即svc模式。所以本条指令是跳转到__irq_usr的同时将处理器模式转为svc模式。异常处理一定要进入svc模式的原因是:异常处理一定要进入PL1特权级;另一个原因是使能嵌套中断。具体原因在问题4中解释。关于__irq_svc和__irq_invalid暂时不讨论。

/*

 * Interrupt dispatcher以下跳转表必须紧跟在vector_irq之后

 */

vector_stub                                                                         irq, IRQ_MODE, 4   //生成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)

   /* SVC进入中断的处理函数 */

.long                                                                                     __irq_svc                    @  3  (SVC_26 / SVC_32)

.long                                                                                     __irq_invalid                       @  4

.long                                                                                     __irq_invalid                       @  5

.long                                                                                     __irq_invalid                       @  6


 

 

图2IRQ中断处理跳转示意图

 

注意,以下操作都是在svc模式中,因为要借用SVC模式进行ISP处理,所以需要保存所有SVC模式下的寄存器到SVC堆栈中,最后才去调用中断服务例程(ISPirq_handler

 

2.2.1 __irq_usr

         .align        5

__irq_usr:

         usr_entry         //用于用户模式下发生中断时初始化中断处理堆栈,同时保存所有SVC态寄存器到堆栈。

         kuser_cmpxchg_check   //对低版本的ARM核来说,用户态无法实现原子比较交换。如果用户态在处理原

                        //子比较交换的过程中发生中断,需要特殊处理,略过

         get_thread_info tsk   //根据当前sp指针,将该指针最右边13位清0,获得当前任务的thread_info

#ifdef CONFIG_PREEMPT   //如果可以抢占,递增任务的抢占计数

         ldr    r8, [tsk, #TI_PREEMPT]           //T被定义为offsetof(struct thread_info, preempt_count),显然通过tsk

                                                                     可以很容易得到进程preempt_count成员的地址了

         add  r7, r8, #1                   @ increment it

         str    r7, [tsk, #TI_PREEMPT]

#endif

         irq_handler              //中断服务例程,后面分析

#ifdef CONFIG_PREEMPT 

         ldr    r0, [tsk, #TI_PREEMPT]   // 获得当前的抢占计数

         str    r8, [tsk, #TI_PREEMPT]  //并将r8中的值保存回去。相当于将前一步递增的抢占计数减回去了

         teq   r0, r7                                  //r0,r7是调用irq_handler前后的抢占计数,这里进行比较,是防止驱动的ISR

                            //程序没有配对操作抢占计数导致系统错误

 ARM(     strne        r0, [r0, -r0]      ) //如果抢占计数被破坏,则强制写入0.

 THUMB(         movne     r0, #0                )

 THUMB(         strne        r0, [r0]     )

#endif

         mov why, #0

         b       ret_to_user     //返回到用户态

 UNWIND(.fnend            )

ENDPROC(__irq_usr)

 

 


接下来分别看各个函数的功能

arch/arm/include/asm/ptrace.h

struct pt_regs {

         unsigned long uregs[18];

};

#endif /* __KERNEL__ */

#define ARM_cpsr           uregs[16]

#define ARM_pc              uregs[15]

#define ARM_lr                uregs[14]

#define ARM_sp              uregs[13]

#define ARM_ip               uregs[12]

#define ARM_fp              uregs[11]

#define ARM_r10            uregs[10]

#define ARM_r9              uregs[9]

……

#define ARM_ORIG_r0   uregs[17]

 

 pt_regs结构体定义,后面要用到。

.macro      usr_entry  //usr_entry宏定义

 UNWIND(.fnstart )

 UNWIND(.cantunwind )        @ don't unwind the user space

sub sp, sp, #S_FRAME_SIZE          @ S_FRAME_SIZE定义在 trimslice-kernel\arch\arm\kernel\arm-offsets.cS_FRAME_SIZE被定义为 sizeof(struct pt_regs)的大小=18*4=72字节,将svc32堆栈指针向低地址方向移动一个pt_regs结构大小,用于保存svc模式下的寄存器现场。

ARM(        stmib        sp, {r1 - r12}    )  @svc32堆栈中保存寄存器现场

 THUMB(         stmia        sp, {r0 - r12}    )

 

         ldmia        r0, {r3 - r5}      @前面r0存放的是irq模式下的栈指针sp的值,从栈中取出r0-r2存放到r3-r5

         add  r0, sp, #S_PC            @ here for interlock avoidanceS_PC定义为offsetof(struct pt_regs, ARM_pc),所

                                                    以这里通过add指令将r0指向ARM_pc

         mov r6, #-1                       @  r6中保存-1

 

         str    r3, [sp]             @ save the "real" r0 copied从中断栈中取出真实的r0存放到pt_regs->r0中。

                                            @ from the exception stack

 

2.2.2 usr_entry

 

         @ We are now ready to fill in the remaining blanks on the stack:

         @

         @  r4 - lr_<exception>, already fixed up for correct return/restart

         @  r5 - spsr_<exception>

         @  r6 - orig_r0 (see pt_regs definition in ptrace.h)

         @

         @ Also, separately save sp_usr and lr_usr

         @

         stmia        r0, {r4 - r6}   //stmiasvc模式下的寄存器r4-r6保存到ARM_pcARM_cpsr

                                                             ARM_ORIG_r0,显然ARM_ORIG_r0保存了-1r6)这个常量

 ARM(     stmdb      r0, {sp, lr}^                        ) //stmdb指令的^标志表示存储发生中断的模式下的sp,lr寄存器

                                                                    ARM_spARM_lr中。

 THUMB(         store_user_sp_lr r0, r1, S_SP - S_PC      )

         @ Enable the alignment trap while in kernel mode

         alignment_trap r0           //alignment_trap在配置CONFIG_ALIGNMENT_TRAP时有效,如果开启了该选

                                                     //项,中断处理中将支持对齐跟踪

         @ Clear FP to mark the first stack frame

         zero_fp                              //zero_fp用来设置fp栈帧寄存器为0

#ifdef CONFIG_IRQSOFF_TRACER

         bl      trace_hardirqs_off

#endif

         .endm                        @usr_entry宏定义结束

 

 

以上的指令的作用可以总结如下,其中将普通寄存器r1到r12保存到ARM_r1- ARM_r12,这相当于把发生中断时的寄存器r1-r12进行了保存。接下来保存中断发生时的r0,lr_irq和spsr_irq保存到r1-r3,r4赋值为-1,它们接下来将被使用。接下来保存r0到ARM_r0,lr_irq,spsr_irq和-1到ARM_pc ARM_cpsr ARM_ORIG_R0,注意到stmdb指令中的"^",它保存sp_usr和lr_usr分别到ARM_sp和ARM_lr,显然这里将所有中断发生时的寄存器都进行了保存。

 

图3  保存中断堆栈

2.2.3 get_thread_info

get_thread_info宏用来根据当前的sp值,通过lsr和lsl分别右移左移13位,相当于对sp向下圆整到8K对齐。这里也就是thread_info所在的地址。

arch/arm/kernel/entry-header.S

  .macro  get_thread_info, rd

  mov     \rd, sp, lsr #13

  mov     \rd, \rd, lsl #13

  .endm

 

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

/*

 * Interrupt handling.  Preserves r7, r8, r9

 */

         .macro     irq_handler

#ifdef CONFIG_MULTI_IRQ_HANDLER

         ldr    r5, =handle_arch_irq

         mov r0, sp

         ldr    r5, [r5]

         adr   lr, BSYM(9997f)

         teq   r5, #0

         movne     pc, r5

#endif

         arch_irq_handler_default

9997:

         .endm

2.2.4 irq_handler

 

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

/*

 * Interrupt handling.  Preserves r7, r8, r9

 */

         .macro     irq_handler

#ifdefCONFIG_MULTI_IRQ_HANDLER

         ldr    r5,=handle_arch_irq

         mov r0,sp

         ldr    r5,[r5]

         adr   lr,BSYM(9997f)

         teq   r5,#0

         movne     pc,r5

#endif

         arch_irq_handler_default

9997:

         .endm

2.2.5 arch_irq_handler_default

irq_handler是真正的IRQ中断处理入口,在中断处理中需要预留r7,r8和r9寄存器。它们被用来处理内核抢占。在没有配置MULTI_IRQ_HANDLER 的情况下,irq_handler的逻辑很简单,就是简单的调用arch_irq_handler_default。

如果配置了该选项,平台代码可以修改全局变量:handle_arch_irq,这里只讨论默认实现.

arch/arm/include/asm/entry_macro_multi.S

/*

 * Interrupt handling.  Preserves r7, r8, r9

 */

         .macro     arch_irq_handler_default    //get_irqnr_preamble用来获取中断状态寄存器基地址

         get_irqnr_preamble  r5, lr   //将中断控制器的状态寄存器的地址存储到r5

1:     get_irqnr_and_base r0, r6, r5, lr //判断中断号,通过r0返回

         movne     r1, sp  //如果还存在中断,就将sp作为第二个参数,调用asm_do_IRQsp目前指向pt_regs

         @

         @ routine called with r0 = irq number, r1 = struct pt_regs *

         @

         adrne       lr, BSYM(1b) //这里将lr设置为get_irqnr_and_base的第二条指令,因为第二次循环时,不必执行其第一条指令(加载寄存器基址)

         bne  asm_do_IRQ   //将中断号、pt_regs(中断前的寄存器现场)传递给asm_do_IRQasm_do_IRQ返回时,

                                            //会返回到get_irqnr_and_base处,直到所有中断都已经处理完毕才退出循环。

#ifdef CONFIG_SMP  //针对SMP系统的处理

         /*

          * this macro assumes that irqstat (r6) and base (r5) are

          * preserved from get_irqnr_and_base above

          */

         ALT_SMP(test_for_ipi r0, r6, r5, lr) // 这里是从寄存器中读取ipi标志

         ALT_UP_B(9997f)

         movne     r1, sp

         adrne       lr, BSYM(1b)  //同理,这里也是将返回地址设置为ALT_SMP的第二条指令,构造成一个循环

         bne  do_IPI    //只要存在IPI就调用do_IPI,并循环直到处理完所有IPI

 

#ifdef CONFIG_LOCAL_TIMERS   // 同理,这里循环处理多核系统中的本地时钟中断。

         test_for_ltirq r0, r6, r5, lr

         movne     r0, sp

         adrne       lr, BSYM(1b)

         bne  do_local_timer

#endif

#endif

9997:

         .endm

 

 

2.2.6 get_irqnr_preamble

get_irqnr_preamble用于获得中断状态寄存器基地址,特定于CPU,这里CPU用的是tegra,其定义如下

 

 

/* arch/arm/mach-tegra/include/mach/entry-macro.S     

         /* Uses the GIC interrupt controller built into the cpu */

#define ICTRL_BASE (IO_CPU_VIRT + 0x40100)  // #define IO_CPU_VIRT     0xFE000000

.macro     get_irqnr_preamble, base, tmp

         movw \base, #(ICTRL_BASE & 0x0000ffff)

         movt \base, #((ICTRL_BASE & 0xffff0000) >> 16)

         .endm

 

 


2.2.7 get_irqnr_and_base

get_irqnr_and_base用来获取中断号。

/* arch/arm/mach-tegra/include/mach/entry-macro.S     

.macro     get_irqnr_and_base, irqnr, irqstat, base, tmp

         ldr \irqnr, [\base, #0x20]        @ EVT_IRQ_STS

         cmp \irqnr, #0x80

         .endm

 

 

get_irqnr_preamble和get_irqnr_and_base两个宏由machine级的代码定义,目的就是从中断控制器中获得IRQ编号,紧接着就调用asm_do_IRQ,从这个函数开始,中断程序进入C代码中,传入的参数是IRQ编号和寄存器结构指针,

2.2.8 asm_do_IRQ

 

图4 asm_do_IRQ流程图

asm_do_IRQ是ARM处理硬件中断的核心函数,第一个参数指定了硬中断的中断号,第二个参数是寄存器备份组成的一个结构体,保存了中断发生时的模式对应的寄存器的值,在中断返回时使用。

 

linux/arch/arm/kernel/irq.c

asmlinkage void __exception_irq_entry

asm_do_IRQ(unsigned int irq, struct pt_regs *regs)

{

         struct pt_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%u\n", irq);

                  ack_bad_irq(irq);

         } else {

                  generic_handle_irq(irq);

         }

         /* AT91 specific workaround */

         irq_finish(irq);

         irq_exit();

         set_irq_regs(old_regs);

}

 

 

 

2.2.9 irq_enter

irq_enter是更新一些系统的统计信息,同时在__irq_enter宏中禁止了进程的抢占。虽然在产生IRQ时,ARM会自动把CPSR中的I位置位,禁止新的IRQ请求,直到中断控制转到相应的流控层后才通过local_irq_enable()打开。那为何还要禁止抢占?这是因为要考虑中断嵌套的问题,一旦流控层或驱动程序主动通过local_irq_enable打开了IRQ,而此时该中断还没处理完成,新的irq请求到达,这时代码会再次进入irq_enter,在本次嵌套中断返回时,内核不希望进行抢占调度,而是要等到最外层的中断处理完成后才做出调度动作,所以才有了禁止抢占这一处理。

linux/kernel/softirq.c

void irq_enter(void)

{

         int cpu = smp_processor_id();

         rcu_irq_enter();

         if (idle_cpu(cpu) && !in_interrupt()) {

                  /* Prevent raise_softirq from needlessly waking up ksoftirqd

                   * here, as softirq will be serviced on return from interrupt.*/

                  local_bh_disable();

                  tick_check_idle(cpu);

                  _local_bh_enable();

         }

         __irq_enter();

}

#define __irq_enter()                                        \

         do {                                             \

                  account_system_vtime(current);           \

                  add_preempt_count(HARDIRQ_OFFSET);     \

                  trace_hardirq_enter();                     \

         } while (0)

 

2.2.10 generic_handle_irq

~/include /linux/Irqdesc.h

/*

 * Architectures call this to let the generic IRQ layer

 * handle an interrupt. If the descriptor is attached to an

 * irqchip-style controller then we call the ->handle_irq() handler,

 * and it calls __do_IRQ() if it's attached to an irqtype-style controller.

 */

static inline void generic_handle_irq_desc(unsigned int irq, struct irq_desc *desc)

{

         desc->handle_irq(irq, desc);  //调用该irq注册的函数处理,该函数在注册中断时填写irq_desc结构体时指定。

}                                                            // handle_irq是个函数指针,它用来实现中断处理器的电流处理。电流处理分为边

                                                             //沿跳变处理和电平处理。

 

static inline void generic_handle_irq(unsigned int irq) //该函数是与体系结构无关的通用逻辑层API

{

         generic_handle_irq_desc(irq, irq_to_desc(irq));

}

 

 

2.2.11 ret_to_user

以上内容处理结束后,退回用户层。

 


arch/arm/kernel/entry-common.S

/*

 * "slow" syscall return path.  "why" tells us if this was a real syscall.

 */

ENTRY(ret_to_user)

ret_slow_syscall:

         disable_irq                                 @ disable interrupts

         ldr    r1, [tsk, #TI_FLAGS]   //从任务的TI_FLAGS标志判断是否需要处理抢占或者信号。

         tst    r1, #_TIF_WORK_MASK

         bne  work_pending    //处理抢占或者信号

no_work_pending:         //没有抢占或者信号需要处理,或者已经处理完毕,开始退回用户态

#if defined(CONFIG_IRQSOFF_TRACER)  //退回用户态必然会打开中断,这里记录下打开中断的事实,供调试用。

         asm_trace_hardirqs_on

#endif

         /* perform architecture specific actions before user return */

         arch_ret_to_user r1, lr   //在返回用户态前,处理各个体系结构的钩子

 

         restore_user_regs fast = 0, offset = 0  //恢复寄存器现场,并切回用户态。这里不再具体分析恢复方式。

ENDPROC(ret_to_user)

 

 

 


3.问题及解答

问题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,如图一所示。下图是搬移示意图更加清晰说明了搬移过程。。

 

 

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

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

注意:

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

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

             

 

 

经过Uboot的启动,内核跳入linux/arch/arm/kernel/head.S开始执行。

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

 

 

 

 

问题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指令会自动将子程序返回地址保存到当前模式的lr(即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()函数实现向量表的设定操作。

 

参考文献

1. ARM Linux中断向量表搬移设计过程http://blog.chinaunix.net/uid-361890-id-175347.html

2. 《LINUX3.0内核源代码分析》第二章:中断和异常  http://blog.chinaunix.net/uid-25845340-id-2982887.html

3. Kernel Memory Layout on ARM Linux http://www.arm.linux.org.uk/developer/memory.txt

4.http://emblinux.sinaapp.com/ar01s16.html#id3603818

5. Linux中断(interrupt)子系统之二:arch相关的硬件封装层http://blog.csdn.net/droidphone/article/details/7467436

附录1

Kernel Memory Layout on ARM Linux

Start                  End           Use

--------------------------------------------------------------------------

ffff8000   ffffffff      copy_user_page / clear_user_page use.

                                   ForSA11xx and Xscale, this is used to

                                   setupa minicache mapping.

 

ffff1000   ffff7fff     Reserved.

                                   Platformsmust not use this address range.

 

ffff0000   ffff0fff     CPUvector page.

                                   The CPU vectors are mapped here ifthe

                                   CPU supports vector relocation(control

                                   register V bit.)

 

ffc00000 fffeffff     DMA memory mapping region.  Memory returned

                                   bythe dma_alloc_xxx functions will be

                                   dynamicallymapped here.

 

ff000000 ffbfffff     Reserved for future expansion of DMA

                                   mappingregion.

 

VMALLOC_END       feffffff     Free for platform use, recommended.

                                   VMALLOC_ENDmust be aligned to a 2MB

                                   boundary.

 

VMALLOC_START   VMALLOC_END-1   vmalloc() /ioremap() space.

                                   Memoryreturned by vmalloc/ioremap will

                                   bedynamically placed in this region.

                                   VMALLOC_STARTmay be based upon the value

                                   ofthe high_memory variable.

 

PAGE_OFFSET high_memory-1      Kernel direct-mapped RAM region.

                                   Thismaps the platforms RAM, and typically

                                   mapsall platform RAM in a 1:1 relationship.

 

TASK_SIZE        PAGE_OFFSET-1      Kernel module space

                                   Kernelmodules inserted via insmod are

                                   placedhere using dynamic mappings.

 

00001000        TASK_SIZE-1    User space mappings

                                   Per-threadmappings are placed here via

                                   themmap() system call.

 

00000000        00000fff  CPU vector page / null pointer trap

                                   CPUswhich do not support vector remapping

                                   placetheir vector page here.  NULL pointer

                                   dereferencesby both the kernel and user

                                   spaceare also caught via this mapping.

 

Please note that mappings which collidewith the above areas may result

in a non-bootable kernel, or may cause thekernel to (eventually) panic

at run time.

 

Since future CPUs may impact the kernelmapping layout, user programs

must not access any memory which is notmapped inside their 0x0001000

to TASK_SIZE address range.  If they wish to access these areas, they

must set up their own mappings using open()and mmap().



转载请注明出处:http://blog.csdn.net/qianlong4526888/article/details/8598524

GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐