首页 > 其他 > 详细

ARM中断处理过程

时间:2019-03-27 22:13:00      阅读:201      评论:0      收藏:0      [点我收藏+]

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

 

一、前言

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

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

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

3、第四章描述了ARM的中断进入过程

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

二、中断处理的准备过程

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

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

但是,实际上产生中断的时候,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来重新设定这些值。

2、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寄存器的值被切换到调度算法选定的新的进程的内核栈上来。

 

3、异常向量表的准备

对于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的原因了。

4、中断控制器的初始化

ARM中断处理过程

原文:https://www.cnblogs.com/haimeng2010/p/10611102.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!