ARM中断处理过程

转自:http://www.wowotech.net/irq_handler.htmlhtml

 

1、前言node

本文主要以ARM体系结构下的中断处理为例,讲述整个中断处理过程当中的硬件行为和软件动做。具体整个处理过程分红三个步骤来描述:linux

一、第二章描述了中断处理的准备过程算法

二、第三章描述了当发生中的时候,ARM硬件的行为bootstrap

三、第四章描述了ARM的中断进入过程数据结构

四、第五章描述了ARM的中断退出过程app

2、中断处理的准备过程dom

ARM处理器有多种processor mode,例如user mode(用户空间的AP所处于的模式)、supervisor mode(即SVC mode,大部分的内核态代码都处于这种mode)、IRQ mode(发生中断后,处理器会切入到该mode)等。函数

对于linux kernel,其中断处理处理过程当中,ARM 处理器大部分都是处于SVC mode。fetch

可是,实际上产生中断的时候,ARM处理器其实是先进入IRQ mode,所以在进入真正的IRQ异常处理以前会有一小段IRQ mode的操做,以后会进入SVC mode进行真正的IRQ异常处理。因为IRQ mode只是一个过分,所以IRQ mode的栈很小,只有12个字节,具体以下:

sprdroid9.0_trunk/kernel4.4/arch/arm/kernel/setup.c

132/*
133 * Cached cpu_architecture() result for use by assembler code.
134 * C code should use the cpu_architecture() function instead of accessing this
135 * variable directly.
136 */
137int __cpu_architecture __read_mostly = CPU_ARCH_UNKNOWN;
138
139struct stack {
140	u32 irq[3];
141	u32 abt[3];
142	u32 und[3];
143	u32 fiq[3];
144} ____cacheline_aligned;

除了irq mode,linux kernel在处理abt mode(当发生data abort exception或者prefetch abort exception的时候进入的模式)和und mode(处理器遇到一个未定义的指令的时候进入的异常模式)的时候也是采用了相同的策略。

也就是通过一个简短的abt或者und mode以后,stack切换到svc mode的栈上,这个栈就是发生异常那个时间点current thread的内核栈

anyway,在irq mode和svc mode之间老是须要一个stack保存数据,这就是中断模式的stack,系统初始化的时候,cpu_init函数中会进行中断模式stack的设定:

/*
518 * cpu_init - initialise one CPU.
519 *
520 * cpu_init sets up the per-CPU stacks.
521 */
522void notrace cpu_init(void)
523{
524#ifndef CONFIG_CPU_V7M
525    unsigned int cpu = smp_processor_id();------获取CPU ID
526    struct stack *stk = &stacks[cpu];---------获取该CPU对于的irq abt和und的stack指针
527
528    if (cpu >= NR_CPUS) {
529        pr_crit("CPU%u: bad primary CPU number\n", cpu);
530        BUG();
531    }
532
533    /*
534     * This only works on resume and secondary cores. For booting on the
535     * boot cpu, smp_prepare_boot_cpu is called after percpu area setup.
536     */
537    set_my_cpu_offset(per_cpu_offset(cpu));
538
539    cpu_proc_init();
540
541    /*
542     * Define the placement constraint for the inline asm directive below.
543     * In Thumb-2, msr with an immediate value is not allowed.
544     */
545#ifdef CONFIG_THUMB2_KERNEL
546#define PLC    "r"------Thumb-2下,msr指令不容许使用当即数,只能使用寄存器。
547#else
548#define PLC    "I"
549#endif
550
551    /*
552     * setup stacks for re-entrant exception handlers
553     */
554    __asm__ (
555    "msr    cpsr_c, %1\n\t"------让CPU进入IRQ mode 
556    "add    r14, %0, %2\n\t"------r14寄存器保存stk->irq 
557    "mov    sp, r14\n\t"--------设定IRQ mode的stack为stk->irq 
558    "msr    cpsr_c, %3\n\t"
559    "add    r14, %0, %4\n\t"
560    "mov    sp, r14\n\t"--------设定abt mode的stack为stk->abt 
561    "msr    cpsr_c, %5\n\t"
562    "add    r14, %0, %6\n\t"
563    "mov    sp, r14\n\t"--------设定und mode的stack为stk->und 
564    "msr    cpsr_c, %7\n\t"
565    "add    r14, %0, %8\n\t"
566    "mov    sp, r14\n\t"--------设定fiq mode的stack为stk->fiq 
567    "msr    cpsr_c, %9"--------回到SVC mode
568        :--------------------上面是code,下面的output部分是空的 
569        : "r" (stk),----------------------对应上面代码中的%0 
570          PLC (PSR_F_BIT | PSR_I_BIT | IRQ_MODE),------对应上面代码中的%1
571          "I" (offsetof(struct stack, irq[0])),------------对应上面代码中的%2 
572          PLC (PSR_F_BIT | PSR_I_BIT | ABT_MODE),------以此类推,下面不赘述 
573          "I" (offsetof(struct stack, abt[0])),
574          PLC (PSR_F_BIT | PSR_I_BIT | UND_MODE),
575          "I" (offsetof(struct stack, und[0])),
576          PLC (PSR_F_BIT | PSR_I_BIT | FIQ_MODE),
577          "I" (offsetof(struct stack, fiq[0])),
578          PLC (PSR_F_BIT | PSR_I_BIT | SVC_MODE)
579        : "r14");--------上面是input操做数列表,r14是要clobbered register列表 
580#endif
581}

嵌入式汇编的语法格式是:

asm(code

: output operand list

: input operand list

: clobber list);

你们对着上面的code就能够分开各段内容了。在input operand list中,有两种限制符(constraint),"r"或者"I","I"表示当即数(Immediate operands),"r"表示用通用寄存器传递参数。clobber list中有一个r14,表示在汇编代码中修改了r14的值,这些信息是编译器须要的内容。

 

对于SMP,bootstrap CPU会在系统初始化的时候执行cpu_init函数,进行本CPU的irq、abt和und三种模式的内核栈的设定,具体调用序列是:start_kernel--->setup_arch--->setup_processor--->cpu_init。

对于系统中其余的CPU,bootstrap CPU会在系统初始化的最后,对每个online的CPU进行初始化,具体的调用序列是:start_kernel--->rest_init--->kernel_init--->kernel_init_freeable--->kernel_init_freeable--->smp_init--->cpu_up--->_cpu_up--->__cpu_up。__cpu_up函数是和CPU architecture相关的。

对于ARM,其调用序列是__cpu_up--->boot_secondary--->smp_ops.smp_boot_secondary(SOC相关代码)--->secondary_startup--->__secondary_switched--->secondary_start_kernel--->cpu_init。

除了初始化,系统电源管理也须要irq、abt和und stack的设定。若是咱们设定的电源管理状态在进入sleep的时候,CPU会丢失irq、abt和und stack point寄存器的值,那么在CPU resume的过程当中,要调用cpu_init来从新设定这些值。

二、SVC模式的stack准备

咱们常常说进程的用户空间和内核空间,对于一个应用程序而言,能够运行在用户空间,也能够经过系统调用进入内核空间。在用户空间,使用的是用户栈,也就是咱们软件工程师编写用户空间程序的时候,保存局部变量的stack。陷入内核后,固然不能用用户栈了,这时候就须要使用到内核栈。所谓内核栈其实就是处于SVC mode时候使用的栈。

在linux最开始启动的时候,系统只有一个进程(更准确的说是kernel thread),就是PID等于0的那个进程,叫作swapper进程(或者叫作idle进程)。该进程的内核栈是静态定义的,以下:  

/sprdroid9.0_trunk/kernel4.4/init/init_task.c

21/*
22 * Initial thread structure. Alignment of this is handled by a special
23 * linker map entry.
24 */
25union thread_union init_thread_union __init_task_data = {
26#ifndef CONFIG_THREAD_INFO_IN_TASK
27    INIT_THREAD_INFO(init_task)
28#endif
29};

2633union thread_union {
2634#ifndef CONFIG_THREAD_INFO_IN_TASK
2635    struct thread_info thread_info;
2636#endif
2637    unsigned long stack[THREAD_SIZE/sizeof(long)];
2638};

对于ARM平台,THREAD_SIZE是8192个byte,所以占据两个page frame。

随着初始化的进行,Linux kernel会建立若干的内核线程,而在进入用户空间后,user space的进程也会建立进程或者线程。

Linux kernel在建立进程(包括用户进程和内核线程)的时候都会分配一个(或者两个,和配置相关)page frame,具体代码以下:

static struct task_struct *dup_task_struct(struct task_struct *orig) 
{ 
    ...... 

    ti = alloc_thread_info_node(tsk, node); 
    if (!ti) 
        goto free_tsk; 

    ...... 
}

底部是struct thread_info数据结构,顶部(高地址)就是该进程的内核栈。当进程切换的时候,整个硬件和软件的上下文都会进行切换,这里就包括了svc mode的sp寄存器的值被切换到调度算法选定的新的进程的内核栈上来。

 

三、异常向量表的准备

对于ARM处理器而言,当发生异常的时候,处理器会暂停当前指令的执行,保存现场,转而去执行对应的异常向量处的指令,当处理完该异常的时候,
恢复现场,回到原来的那点去继续执行程序。系统全部的异常向量(共计8个)组成了异常向量表。向量表(vector table)的代码以下:
/sprdroid9.0_trunk/kernel4.4/arch/arm/kernel/entry-armv.S

208    .section .vectors, "ax", %progbits
1209__vectors_start:
1210    W(b)    vector_rst
1211    W(b)    vector_und
1212    W(ldr)    pc, __vectors_start + 0x1000
1213    W(b)    vector_pabt
1214    W(b)    vector_dabt
1215    W(b)    vector_addrexcptn
1216    W(b)    vector_irq---------------------------IRQ Vector
1217    W(b)    vector_fiq
1218

对于本文而言,咱们重点关注vector_irq这个exception vector。异常向量表可能被安放在两个位置上:

(1)异常向量表位于0x0的地址。这种设置叫作Normal vectors或者Low vectors。

(2)异常向量表位于0xffff0000的地址。这种设置叫作high vectors

具体是low vectors仍是high vectors是由ARM的一个叫作的SCTLR寄存器的第13个bit (vector bit)控制的。对于启用MMU的ARM Linux而言,系统使用了high vectors。为何不用low vector呢?对于linux而言,0~3G的空间是用户空间,若是使用low vector,那么异常向量表在0地址,那么则是用户空间的位置,所以linux选用high vector。固然,使用Low vector也能够,这样Low vector所在的空间则属于kernel space了(也就是说,3G~4G的空间加上Low vector所占的空间属于kernel space),不过这时候要注意一点,由于全部的进程共享kernel space,而用户空间的程序常常会发生空指针访问,这时候,内存保护机制应该能够捕获这种错误(大部分的MMU均可以作到,例如:禁止userspace访问kernel space的地址空间),防止vector table被访问到。对于内核中因为程序错误致使的空指针访问,内存保护机制也须要控制vector table被修改,所以vector table所在的空间被设置成read only的。在使用了MMU以后,具体异常向量表放在那个物理地址已经不重要了,重要的是把它映射到0xffff0000的虚拟地址就OK了,具体代码以下:

/sprdroid9.0_trunk/kernel4.4/arch/arm/mm/mmu.c
static void __init devicemaps_init(const struct machine_desc *mdesc) 
{ 
    …… 
    vectors = early_alloc(PAGE_SIZE * 2); -----分配两个page的物理页帧

    early_trap_init(vectors); -------copy向量表以及相关help function到该区域

    …… 
    map.pfn = __phys_to_pfn(virt_to_phys(vectors)); 
    map.virtual = 0xffff0000; 
    map.length = PAGE_SIZE; 
#ifdef CONFIG_KUSER_HELPERS 
    map.type = MT_HIGH_VECTORS; 
#else 
    map.type = MT_LOW_VECTORS; 
#endif 
    create_mapping(&map); ----------映射0xffff0000的那个page frame

    if (!vectors_high()) {---若是SCTLR.V的值设定为low vectors,那么还要映射0地址开始的memory 
        map.virtual = 0; 
        map.length = PAGE_SIZE * 2; 
        map.type = MT_LOW_VECTORS; 
        create_mapping(&map); 
    }


    map.pfn += 1; 
    map.virtual = 0xffff0000 + PAGE_SIZE; 
    map.length = PAGE_SIZE; 
    map.type = MT_LOW_VECTORS; 
    create_mapping(&map); ----------映射high vecotr开始的第二个page frame

…… 
}

为何要分配两个page frame呢?这里vectors table和kuser helper函数(内核空间提供的函数,可是用户空间使用)占用了一个page frame,另外异常处理的stub函数占用了另一个page frame。为何会有stub函数呢?稍后会讲到。

在early_trap_init函数中会初始化异常向量表,具体代码以下:

void __init early_trap_init(void *vectors_base) 
{ 
    unsigned long vectors = (unsigned long)vectors_base; 
    extern char __stubs_start[], __stubs_end[]; 
    extern char __vectors_start[], __vectors_end[]; 
    unsigned i;

    vectors_page = vectors_base;

    将整个vector table那个page frame填充成未定义的指令。起始vector table加上kuser helper函数并不能彻底的充满这个page,有些缝隙。若是不这么处理,当极端状况下(程序错误或者HW的issue),CPU可能从这些缝隙中取指执行,从而致使不可知的后果。若是将这些缝隙填充未定义指令,那么CPU能够捕获这种异常。 
    for (i = 0; i < PAGE_SIZE / sizeof(u32); i++) 
        ((u32 *)vectors_base)[i] = 0xe7fddef1;

  拷贝vector table,拷贝stub function 
    memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start); 
    memcpy((void *)vectors + 0x1000, __stubs_start, __stubs_end - __stubs_start);

    kuser_init(vectors_base); ----copy kuser helper function

    flush_icache_range(vectors, vectors + PAGE_SIZE * 2); 
    modify_domain(DOMAIN_USER, DOMAIN_CLIENT); 

}

一旦涉及代码的拷贝,咱们就须要关心其编译链接时地址(link-time address)和运行时地址(run-time address)。在kernel完成连接后,__vectors_start有了其link-time address,若是link-time address和run-time address一致,那么这段代码运行时毫无压力。可是,目前对于vector table而言,其被copy到其余的地址上(对于High vector,这是地址就是0xffff00000),也就是说,link-time address和run-time address不同了,若是仍然想要这些代码能够正确运行,那么须要这些代码是位置无关的代码。对于vector table而言,必需要位置无关。B这个branch instruction自己就是位置无关的,它能够跳转到一个当前位置的offset。不过并不是全部的vector都是使用了branch instruction,对于软中断,其vector地址上指令是“W(ldr)    pc, __vectors_start + 0x1000 ”,这条指令被编译器编译成ldr     pc, [pc, #4080],这种状况下,该指令也是位置无关的,可是有个限制,offset必须在4K的范围内,这也是为什么存在stub section的缘由了。

四、中断控制器的初始化

3、ARM HW对中断事件的处理

当一切准备好以后,一旦打开处理器的全局中断就能够处理来自外设的各类中断事件了。

当外设(SOC内部或者外部均可以)检测到了中断事件,就会经过interrupt requestion line上的电平或者边沿(上升沿或者降低沿或者both)通知到该外设链接到的那个中断控制器,而中断控制器就会在多个处理器中选择一个,并把该中断经过IRQ(或者FIQ,本文不讨论FIQ的状况)分发给该processor。ARM处理器感知到了中断事件后,会进行下面一系列的动做:

一、修改CPSR(Current Program Status Register)寄存器中的M[4:0]。M[4:0]表示了ARM处理器当前处于的模式( processor modes)。ARM定义的mode包括:

处理器模式 缩写 对应的M[4:0]编码 Privilege level
User usr 10000 PL0
FIQ fiq 10001 PL1
IRQ irq 10010 PL1
Supervisor svc 10011 PL1
Monitor mon 10110 PL1
Abort abt 10111 PL1
Hyp hyp 11010 PL2
Undefined und 11011 PL1
System sys 11111 PL1

一旦设定了CPSR.M,ARM处理器就会将processor mode切换到IRQ mode。

二、保存发生中断那一点的CPSR值(step 1以前的状态)和PC值

ARM处理器支持9种processor mode,每种mode看到的ARM core register(R0~R15,共计16个)都是不一样的。每种mode都是从一个包括全部的Banked ARM core register中选取。所有Banked ARM core register包括:

Usr System Hyp Supervisor abort undefined Monitor IRQ FIQ
R0_usr                
R1_usr                
R2_usr                
R3_usr                
R4_usr                
R5_usr                
R6_usr                
R7_usr                
R8_usr               R8_fiq
R9_usr               R9_fiq
R10_usr               R10_fiq
R11_usr               R11_fiq
R12_usr               R12_fiq
SP_usr   SP_hyp SP_svc SP_abt SP_und SP_mon SP_irq SP_fiq
LR_usr     LR_svc LR_abt LR_und LR_mon LR_irq LR_fiq
PC                
CPSR                
    SPSR_hyp SPSR_svc SPSR_abt SPSR_und SPSR_mon SPSR_irq SPSR_fiq
    ELR_hyp            

 

在IRQ mode下,CPU看到的R0~R12寄存器、PC以及CPSR是和usr mode(userspace)或者svc mode(kernel space)是同样的。不一样的是IRQ mode下,有本身的R13(SP,stack pointer)、R14(LR,link register)和SPSR(Saved Program Status Register)。

CPSR是共用的,虽然中断可能发生在usr mode(用户空间),也多是svc mode(内核空间),不过这些信息都是体如今CPSR寄存器中。硬件会将发生中断那一刻的CPSR保存在SPSR寄存器中(因为不一样的mode下有不一样的SPSR寄存器,所以更准确的说应该是SPSR-irq,也就是IRQ mode中的SPSR寄存器)。

PC也是共用的,因为后续PC会被修改成irq exception vector,所以有必要保存PC值。固然,与其说保存PC值,不如说是保存返回执行的地址。对于IRQ而言,咱们指望返回地址是发生中断那一点执行指令的下一条指令。具体的返回地址保存在lr寄存器中(注意:这个lr寄存器是IRQ mode的lr寄存器,能够表示为lr_irq):

(1)对于thumb state,lr_irq = PC

(2)对于ARM state,lr_irq = PC - 4

为什么要减去4?个人理解是这样的(不必定对)。因为ARM采用流水线结构,当CPU正在执行某一条指令的时候,其实取指的动做早就执行了,这时候PC值=正在执行的指令地址 + 8,以下所示:

----> 发生中断的指令

               发生中断的指令+4

-PC-->发生中断的指令+8

               发生中断的指令+12

一旦发生了中断,当前正在执行的指令固然要执行完毕,可是已经完成取指、译码的指令则终止执行。当发生中断的指令执行完毕以后,原来指向(发生中断的指令+8)的PC会继续增长4,所以发生中断后,ARM core的硬件着手处理该中断的时候,硬件现场以下图所示:

 

----> 发生中断的指令

               发生中断的指令+4 <-------中断返回的指令是这条指令

              发生中断的指令+8

-PC-->发生中断的指令+12

 

这时候的PC值实际上是比发生中断时候的指令超前12。减去4以后,lr_irq中保存了(发生中断的指令+8)的地址。为何HW不帮忙直接减去8呢?这样,后续软件不就不用再减去4了。这里咱们不能孤立的看待问题,实际上ARM的异常处理的硬件逻辑不只仅处理IRQ的exception,还要处理各类exception,很遗憾,不一样的exception指望的返回地址不统一,所以,硬件只是帮忙减去4,剩下的交给软件去调整。

三、mask IRQ exception。也就是设定CPSR.I = 1

四、设定PC值为IRQ exception vector。基本上,ARM处理器的硬件就只能帮你帮到这里了,一旦设定PC值,ARM处理器就会跳转到IRQ的exception vector地址了,后续的动做都是软件行为了。

 

4、如何进入ARM中断处理

一、IRQ mode中的处理

IRQ mode的处理都在vector_irq中,vector_stub是一个宏,定义以下:

.macro    vector_stub, name, mode, correction=0 
    .align    5

vector_\name: 
    .if \correction 
    sub    lr, lr, #\correction-------------(1) 
    .endif

    @ 
    @ Save r0, lr_ (parent PC) and spsr_ 
    @ (parent CPSR) 
    @ 
    stmia    sp, {r0, lr}        @ save r0, lr--------(2) 
    mrs    lr, spsr 
    str    lr, [sp, #8]        @ save spsr

    @ 
    @ Prepare for SVC32 mode.  IRQs remain disabled. 
    @ 
    mrs    r0, cpsr-----------------------(3) 
    eor    r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE) 
    msr    spsr_cxsf, r0

    @ 
    @ the branch table must immediately follow this code 
    @ 
    and    lr, lr, #0x0f---lr保存了发生IRQ时候的CPSR,经过and操做,能够获取CPSR.M[3:0]的值

                            这时候,若是中断发生在用户空间,lr=0,若是是内核空间,lr=3 
THUMB( adr    r0, 1f            )----根据当前PC值,获取lable 1的地址 
THUMB( ldr    lr, [r0, lr, lsl #2]  )-lr根据当前mode,要么是__irq_usr的地址 ,要么是__irq_svc的地址 
    mov    r0, sp------将irq mode的stack point经过r0传递给即将跳转的函数 
ARM(    ldr    lr, [pc, lr, lsl #2]    )---根据mode,给lr赋值,__irq_usr或者__irq_svc 
    movs    pc, lr            @ branch to handler in SVC mode-----(4) 
ENDPROC(vector_\name)

    .align    2 
    @ handler addresses follow this label 
1: 
    .endm

(1)咱们指望在栈上保存发生中断时候的硬件现场(HW context),这里就包括ARM的core register。上一章咱们已经了解到,当发生IRQ中断的时候,lr中保存了发生中断的PC+4,若是减去4的话,获得的就是发生中断那一点的PC值。

(2)当前是IRQ mode,SP_irq在初始化的时候已经设定(12个字节)。在irq mode的stack上,依次保存了发生中断那一点的r0值、PC值以及CPSR值(具体操做是经过spsr进行的,其实硬件已经帮咱们保存了CPSR到SPSR中了)。为什么要保存r0值?由于随后的代码要使用r0寄存器,所以咱们要把r0放到栈上,只有这样才能完彻底全恢复硬件现场。

(3)可怜的IRQ mode稍纵即逝,这段代码就是准备将ARM推送到SVC mode。如何准备?其实就是修改SPSR的值,SPSR不是CPSR,不会引发processor mode的切换(毕竟这一步只是准备而已)。

(4)不少异常处理的代码返回的时候都是使用了stack相关的操做,这里没有。“movs    pc, lr ”指令除了字面上意思(把lr的值付给pc),还有一个隐含的操做(movs中‘s’的含义):把SPSR copy到CPSR,从而实现了模式的切换。

二、当发生中断的时候,代码运行在用户空间

Interrupt dispatcher的代码以下:

vector_stub    irq, IRQ_MODE, 4 -----减去4,确保返回发生中断以后的那条指令

.long    __irq_usr            @  0  (USR_26 / USR_32)   <---------------------> base address + 0 
.long    __irq_invalid            @  1  (FIQ_26 / FIQ_32) 
.long    __irq_invalid            @  2  (IRQ_26 / IRQ_32) 
.long    __irq_svc            @  3  (SVC_26 / SVC_32)<---------------------> base address + 12 
.long    __irq_invalid            @  4 
.long    __irq_invalid            @  5 
.long    __irq_invalid            @  6 
.long    __irq_invalid            @  7 
.long    __irq_invalid            @  8 
.long    __irq_invalid            @  9 
.long    __irq_invalid            @  a 
.long    __irq_invalid            @  b 
.long    __irq_invalid            @  c 
.long    __irq_invalid            @  d 
.long    __irq_invalid            @  e 
.long    __irq_invalid            @  f

这其实就是一个lookup table,根据CPSR.M[3:0]的值进行跳转(参考上一节的代码:and    lr, lr, #0x0f)。所以,该lookup table共设定了16个入口,固然只有两项有效,分别对应user mode和svc mode的跳转地址。其余入口的__irq_invalid也是很是关键的,这保证了在其模式下发生了中断,系统能够捕获到这样的错误,为debug提供有用的信息。

相关文章
相关标签/搜索