转自: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 HW对中断事件的处理
当一切准备好之后,一旦打开处理器的全局中断就可以处理来自外设的各种中断事件了。
当外设(SOC内部或者外部都可以)检测到了中断事件,就会通过interrupt requestion line上的电平或者边沿(上升沿或者下降沿或者both)通知到该外设连接到的那个中断控制器,而中断控制器就会在多个处理器中选择一个,并把该中断通过IRQ(或者FIQ,本文不讨论FIQ的情况)分发给该processor。ARM处理器感知到了中断事件后,会进行下面一系列的动作:
1、修改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。
2、保存发生中断那一点的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,剩下的交给软件去调整。
3、mask IRQ exception。也就是设定CPSR.I = 1
4、设定PC值为IRQ exception vector。基本上,ARM处理器的硬件就只能帮你帮到这里了,一旦设定PC值,ARM处理器就会跳转到IRQ的exception vector地址了,后续的动作都是软件行为了。
四、如何进入ARM中断处理
1、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,从而实现了模式的切换。
2、当发生中断的时候,代码运行在用户空间
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提供有用的信息。