本文主要以ARM体系结构下的中断处理为例,讲述整个中断处理过程中的硬件行为和软件动作。文中大量参考了这篇博文:https://www.cnblogs.com/hpunix/articles/6213708.html,为了更好的理解这部分内容,也做了一些修改。
在了解软件如何处理中断前,可以先看一下这篇文章,了解ARM 硬件是怎么处理中断的:
arm 处理器进入和退出异常中断的过程_oqqYuJi12345678的博客-CSDN博客
1 中断处理的初始化
1.1中断模式的stack准备
ARM处理器有多种process 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个字节,具体如下:
struct stack {
u32 irq[3];
u32 abt[3];
u32 und[3];
} ____cacheline_aligned;static struct stack stacks[NR_CPUS];
除了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的设定:
start_kernel
---------->setup_arch
------------>setup_processor
--------------->cpu_init
void notrace cpu_init(void)
{
unsigned int cpu = smp_processor_id();------获取CPU ID
struct stack *stk = &stacks[cpu];---------获取该CPU对于的irq abt和und的stack指针
……
#ifdef CONFIG_THUMB2_KERNEL
#define PLC "r"------Thumb-2下,msr指令不允许使用立即数,只能使用寄存器。
#else
#define PLC "I"
#endif
__asm__ (
"msr cpsr_c, %1\n\t"------让CPU进入IRQ mode
"add r14, %0, %2\n\t"------r14寄存器保存stk->irq
"mov sp, r14\n\t"--------设定IRQ mode的stack为stk->irq
"msr cpsr_c, %3\n\t"
"add r14, %0, %4\n\t"
"mov sp, r14\n\t"--------设定abt mode的stack为stk->abt
"msr cpsr_c, %5\n\t"
"add r14, %0, %6\n\t"
"mov sp, r14\n\t"--------设定und mode的stack为stk->und
"msr cpsr_c, %7"--------回到SVC mode
:--------------------上面是code,下面的output部分是空的
: "r" (stk),----------------------对应上面代码中的%0
PLC (PSR_F_BIT | PSR_I_BIT | IRQ_MODE),------对应上面代码中的%1
"I" (offsetof(struct stack, irq[0])),------------对应上面代码中的%2
PLC (PSR_F_BIT | PSR_I_BIT | ABT_MODE),------以此类推,下面不赘述
"I" (offsetof(struct stack, abt[0])),
PLC (PSR_F_BIT | PSR_I_BIT | UND_MODE),
"I" (offsetof(struct stack, und[0])),
PLC (PSR_F_BIT | PSR_I_BIT | SVC_MODE)
: "r14");--------上面是input操作数列表,r14是要clobbered register列表
}
嵌入式内联汇编的语法格式是: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的值,这些信息是编译器需要的内容。
1.2 SVC模式的stack准备
我们经常说进程的用户空间和内核空间,对于一个应用程序而言,可以运行在用户空间,也可以通过系统调用进入内核空间。在用户空间,使用的是用户栈,也就是我们软件工程师编写用户空间程序的时候,保存局部变量的stack。陷入内核后,当然不能用用户栈了,这时候就需要使用到内核栈。所谓内核栈其实就是处于SVC mode时候使用的栈。
Linux kernel在创建进程(包括用户进程和内核线程)的时候都会分配一个(或者两个,和配置相关)page frame,底部是struct thread_info数据结构,顶部(高地址)就是该进程的内核栈。当进程切换的时候,整个硬件和软件的上下文都会进行切换,这里就包括了svc mode的sp寄存器的值被切换到调度算法选定的新的进程的内核栈上来。
1.3 异常向量表的准备
对于ARM处理器而言,当发生异常的时候,处理器会暂停当前指令的执行,保存现场,转而去执行对应的异常向量处的指令,当处理完该异常的时候,恢复现场,回到原来的那点去继续执行程序。系统所有的异常向量(共计8个)组成了异常向量表。向量表(vector table)的代码如下,该代码在arch/arm/kernel/entry-armv.S中 :
__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
对于本文而言,我们重点关注vector_irq这个exception vector。异常向量表可能被安放在两个位置上:
(1)异常向量表位于0x0的地址。这种设置叫做Normal vectors或者Low vectors。
(2)异常向量表位于0xffff0000的地址。这种设置叫做high vectors
具体是low vectors还是high vectors是由ARM的CP15协处理器c1寄存器中V位(bit[13])控制的。对于启用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的。具体如何设置该中断向量的起始位置呢,如下分析:
在基于ARM的Linux中,异常向量表已经被放置在了0xFFFF0000这个位置。这个过程的完成:
start_kernel
-----------> setup_arch
------------> early_trap_init
把异常中断向量表的位置设置为0xffff0000的话,需要修改协处理器CP15的寄存器C1的第13位,将其设置为1。看一下linux3.10内核的具体实现:
在arch/arm/kernel/head.s中:
mrc p15, 0, r9, c0, c0 @ get processor id //从协处理器c0中获取cpuid
bl __lookup_processor_type //根据该cpu id,查找process info list
movs r10, r5 @ invalid processor? //把找到的process info的首地址放入r10
moveq r0, #'p' @ yes, error 'p'
THUMB( it eq ) @ force fixup-able long branch encoding
beq __error_p
/*
* Use the page tables supplied from __cpu_up.
*/
adr r4, __secondary_data
ldmia r4, {r5, r7, r12} @ address to jump to after
sub lr, r4, r5 @ mmu has been enabled
ldr r4, [r7, lr] @ get secondary_data.pgdir
add r7, r7, #4
ldr r8, [r7, lr] @ get secondary_data.swapper_pg_dir
adr lr, BSYM(__enable_mmu) @ return address
mov r13, r12 @ __secondary_switched address
ARM( add pc, r10, #PROCINFO_INITFUNC ) @ initialise processor //根据r10中的process info 首地址加偏移跳转到对应cpu的设置函数
@ (return control reg)
THUMB( add r12, r10, #PROCINFO_INITFUNC )
THUMB( mov pc, r12 )
ENDPROC(secondary_startup)
上面代码可以主要有4个地方需要注意:
(1)mrc p15, 0, r9, c0, c0 @ get processor id //从协处理器c0中获取cpuid
(2)bl __lookup_processor_type //根据该cpu id,查找process info list
movs r10, r5 //并把找到的process info 起始地址放入r10
看一下__lookup_processor_type具体实现,代码在arch/arm/kernel/head-common.s中
__lookup_processor_type:
adr r3, __lookup_processor_type_data //__lookup_processor_type_data 中包含了__proc_info_begin和__proc_info_end,r3是__lookup_processor_type_data的运行地址
ldmia r3, {r4 - r6} //r4放入__lookup_processor_type_data的编译地址,r5放入__proc_info_begin 的编译地址,r6放入__proc_info_end的编译地址
sub r3, r3, r4 //得到运行地址与编译地址的偏移 @ get offset between virt&phys
add r5, r5, r3 //得到 __proc_info_begin 的运行地址@ convert virt addresses to
add r6, r6, r3 //得到r6的运行地址 @ physical address space
1: ldmia r5, {r3, r4} //得到 第一个process的cpuid和mask,分别放入r3和r4中@ value, mask
and r4, r4, r9 //r9中存放的是当前cpuid, @ mask wanted bits
teq r3, r4 //当前cpuid和mask取值以后,对比process info中的cpuid是否一样
beq 2f //一样的话直接走2分支,返回,r5中存放的是找到的process info首地址
add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list) //如果不一样,加上process info 偏移,查找下一个process info
cmp r5, r6 //如果查找到最后一个都没找到,则返回err,否则跳转到1,接着进行对比
blo 1b
mov r5, #0 //走到这边说明没找到 @ unknown processor
2: mov pc, lr
ENDPROC(__lookup_processor_type)
adr r3, __lookup_processor_type_data 获取__lookup_processor_type_data的地址:
__lookup_processor_type_data:
.long . //标记当前编译地址
.long __proc_info_begin
.long __proc_info_end
.size __lookup_processor_type_data, . - __lookup_processor_type_data
上面的代码依次查找在__proc_info_begin和__proc_info_end之间的process info list。arm9的process info 如下定义:
.section ".proc.info.init", #alloc, #execinstr
.type __arm926_proc_info,#object
__arm926_proc_info:
.long 0x41069260 @ ARM926EJ-S (v5TEJ)
.long 0xff0ffff0
.long PMD_TYPE_SECT | \
PMD_SECT_BUFFERABLE | \
PMD_SECT_CACHEABLE | \ //这边的信息用于在head.s中初始化段页表,设置页表项
PMD_BIT4 | \
PMD_SECT_AP_WRITE | \
PMD_SECT_AP_READ
.long PMD_TYPE_SECT | \
PMD_BIT4 | \
PMD_SECT_AP_WRITE | \
PMD_SECT_AP_READ
b __arm926_setup //process的初始化函数
.long cpu_arch_name
.long cpu_elf_name
.long HWCAP_SWP|HWCAP_HALF|HWCAP_THUMB|HWCAP_FAST_MULT|HWCAP_EDSP|HWCAP_JAVA
.long cpu_arm926_name
.long arm926_processor_functions
.long v4wbi_tlb_fns
.long v4wb_user_fns
.long arm926_cache_fns
.size __arm926_proc_info, . - __arm926_proc_info
(3)adr lr, BSYM(__enable_mmu) @ return address //把返回地址设置成__enable_mmu
mov r13, r12 @ __secondary_switched address
ARM( add pc, r10, #PROCINFO_INITFUNC ) @ initialise processor //根据r10中的process info 首地址加偏移跳转到对应cpu的设置函数
PROCINFO_INITFUNC 如下定义:
DEFINE(PROC_INFO_SZ, sizeof(struct proc_info_list));
该偏移是16,所以pc中的值为b __arm926_setup,跳转到__arm926_setup中进行处理:
__arm920_setup:
mov r0, #0
mcr p15, 0, r0, c7, c7 @ invalidate I,D caches on v4
mcr p15, 0, r0, c7, c10, 4 @ drain write buffer on v4
#ifdef CONFIG_MMU
mcr p15, 0, r0, c8, c7 @ invalidate I,D TLBs on v4
#endif
adr r5, arm920_crval
ldmia r5, {r5, r6}
mrc p15, 0, r0, c1, c0 @ get control register v4
bic r0, r0, r5
orr r0, r0, r6 //r0 最后会赋值给cp15协处理器的寄存器C1
mov pc, lr
.size __arm920_setup, . - __arm920_setup
上面代码 利用arm920_crval初始化了r5和r6
adr r5, arm920_crval
ldmia r5, {r5, r6}
.type arm920_crval, #object
arm920_crval:
crval clear=0x00003f3f, mmuset=0x00003135, ucset=0x00001130
crval宏的实现:
.macro crval, clear, mmuset, ucset
#ifdef CONFIG_MMU
.word \clear
.word \mmuset
#else
.word \clear
.word \ucset
#endif
.endm
所以r0 在得到c1寄存器的值以后,会与0x00003135进行或,然后第13位会被设置成1,该位决定了中断异常向量起始地址是放在0x00000000还是0xffff0000。
执行mov pc, lr跳转到__enable_mmu继续执行:
__enable_mmu:
#if defined(CONFIG_ALIGNMENT_TRAP) && __LINUX_ARM_ARCH__ < 6
orr r0, r0, #CR_A
#else
bic r0, r0, #CR_A
#endif
#ifdef CONFIG_CPU_DCACHE_DISABLE
bic r0, r0, #CR_C
#endif
#ifdef CONFIG_CPU_BPREDICT_DISABLE
bic r0, r0, #CR_Z
#endif
#ifdef CONFIG_CPU_ICACHE_DISABLE
bic r0, r0, #CR_I
#endif
#ifdef CONFIG_ARM_LPAE
mov r5, #0
mcrr p15, 0, r4, r5, c2 @ load TTBR0
#else
mov r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \
domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \
domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \
domain_val(DOMAIN_IO, DOMAIN_CLIENT))
mcr p15, 0, r5, c3, c0, 0 @ load domain access register
mcr p15, 0, r4, c2, c0, 0 @ load page table pointer
#endif
b __turn_mmu_on
ENDPROC(__enable_mmu)
该函数最后执行__turn_mmu_on:
ENTRY(__turn_mmu_on)
mov r0, r0
instr_sync
mcr p15, 0, r0, c1, c0, 0 @ write control reg //在这里把r0 设置回c1寄存器,完成中断向量表起始地址为0xffff0000的设置
mrc p15, 0, r3, c0, c0, 0 @ read id reg
instr_sync
mov r3, r3
mov r3, r13
mov pc, r3
__turn_mmu_on_end:
ENDPROC(__turn_mmu_on)
__turn_mmu_on中的mcr p15, 0, r0, c1, c0, 0 把r0设置回cp1,完成设置。
在使用了MMU之后,具体异常向量表放在那个物理地址已经不重要了,重要的是把它映射到0xffff0000的虚拟地址就OK了,具体代码如下:
start_kernel
---------->setup_arch
------------>paging_init
-------------->devicemaps_init
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可以捕获这种异常。
or (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, .LCvswi + stubs_offset,这条指令被编译器编译成ldr pc, [pc, #4080],这种情况下,该指令也是位置无关的,但是有个限制,offset必须在4K的范围内,这也是为何存在stub section的原因了。
2 如何进入中断处理
2.1 IRQ mode中的处理
发生irq中断,则执行中断向量中的这个:
.equ stubs_offset, __vectors_start + 0x200 - __stubs_start
W(b) vector_irq + stubs_offset
先看一下在把中断向量表复制到0xFFFF0000位置以后,b跳转指令如何找到正确的中断处理函数的。
下面的图很详细的解释了这个问题:
IRQ mode的处理都在vector_irq中,vector_stub是一个宏,定义如下:
上图下面的线显示了代码被“搬移”之前相对位置,上面的线显示了“搬移”后代码的相对位置。假设现在发生了一个数据预取异常,PC会先跳转到t1的位置执行b vector_dabt + stubs_offset 然后跳转到E(vector_dabt)去执行对应的处理语句。我们怎样在写代码的时候确定将“b vector_dabt + stubs_offset ”这样的代码放到中断向量表中,然后在进行代码“搬移”工作之后,发生异常事件的时候,在固定的位置执行一条指令,这条指令中包含了相对于当前PC值得偏移,进而跳转到一个新的位置开始执行后面的处理。要记住,B指令实际上是一条相对跳转指令,而且代码在编译的时候与“搬移”之后的代码在相对位置上是确定的,这里就有了位置无关的代码的用武之地。
当你看到offset=vector_dabt+stub_offset-t2的时候,你要这样理解,内核编译到t2这条指令的时候(b vector_dabt + stubs_offset )其实需要确定的是代码“搬移”之后的一个偏移,这个偏移是固定的(上图中的L1+L2,而且这个偏移在执行的时候,也就是代码“搬移”之后才有意义),偏移加上当前的PC(t2)的值是vector_dabt + stubs_offset,所以中断向量表中的这一项就写成了“b vector_dabt + stubs_offset ”。或者换句话说,如果代码没有搬移,那么在编译的时候,那么跳转指令是这样的b vector_dabt,其编译时需要确定的相对跳转offset 就是vector_dabt-t2,因为代码有了搬移,所以编译时需要确定的相对跳转offset 为vector_dabt+stub_offset-t2,-t2编译的时候由编译器自己添加,最终我们写成W(b) vector_irq + stubs_offset。
IRQ mode的处理都在vector_irq中
vector_irq 就是如下代码:
vector_stub irq, IRQ_MODE, 4
可以看到调用了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 __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。根据文章开头引用的arm 处理器进入和退出异常中断的过程一文了解到,当发生IRQ中断的时候,PC的值已经更新为中断后面那一条指令再偏移两条指令,因为中断完成以后要继续执行中断后面那条指令,而arm硬件会自动把PC-4赋值给l2,所以我们得再减4才能得到正确的返回地址,所以在vector_stub irq, IRQ_MODE, 4中传入的是4。
(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,从而实现了模式的切换。lr里面如何填写了正确的跳转指令呢,需要注意这条指令:ARM( ldr lr, [pc, lr, lsl #2] )
ldr lr, pc+lr<<2
上面如果发生中断的时候,在用户模式,则lr赋值为0,svc模式,值为3,所以用户模式,偏移为0,svc模式,偏移为4,再看PC,PC的值其实是当前的指令+8,所以lr的值在用户态发生中断则是当前指令+8,如果是svc模式,则是+20,
把上面代码合并起来,可以看到如下:
vector_stub irq, IRQ_MODE, 4
.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
.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
偏移+8,刚好是__irq_usr的地址,去执行__irq_usr的处理,而+20刚好是__irq_svc的处理,去执行__irq_svc,为什么要分用户态和svc态进入中断,做不同的处理呢,再看下面分析。
再看一下__irq_usr如何处理:
__irq_usr:
usr_entry---------保存用户现场的描述
kuser_cmpxchg_check---和本文描述的内容无关,这些不就介绍了
irq_handler----------核心处理内容,请参考本章第二节的描述
get_thread_info tsk------tsk是r9,指向当前的thread info数据结构
mov why, #0--------why是r8
b ret_to_user_from_irq----中断返回,下一章会详细描述
保存发生中断时候的现场。所谓保存现场其实就是把发生中断那一刻的硬件上下文(各个寄存器)保存在了SVC mode的stack上。
.macro usr_entry
sub sp, sp, #S_FRAME_SIZE--------------A
stmib sp, {r1 - r12} -------------------B
ldmia r0, {r3 - r5}--------------------C
add r0, sp, #S_PC-------------------D
mov r6, #-1----orig_r0的值
str r3, [sp] ----保存中断那一刻的r0
stmia r0, {r4 - r6}--------------------E
stmdb r0, {sp, lr}^-------------------F
.endm
A:代码执行到这里的时候,ARM处理已经切换到了SVC mode。一旦进入SVC mode,ARM处理器看到的寄存器已经发生变化,这里的sp已经变成了sp_svc了。因此,后续的压栈操作都是压入了发生中断那一刻的进程的(或者内核线程)内核栈(svc mode栈)。具体保存多少个寄存器值?S_FRAME_SIZE已经给出了答案,这个值是18个寄存器。r0~r15再加上CPSR也只有17个而已。先保留这个疑问,我们稍后回答。
B:压栈首先压入了r1~r12,这里为何不处理r0?因为r0在irq mode切到svc mode的时候被污染了,不过,原始的r0被保存的irq mode的stack上了。r13(sp)和r14(lr)需要保存吗,当然需要,稍后再保存。执行到这里,内核栈的布局如下图所示:
stmib中的ib表示increment before,因此,在压入R1的时候,stack pointer会先增加4,重要是预留r0的位置。stmib sp, {r1 - r12}指令中的sp没有“!”的修饰符,表示压栈完成后并不会真正更新stack pointer,因此sp保持原来的值。
C:注意,这里r0指向了irq stack,因此,r3是中断时候的r0值,r4是中断返回地址,r5是中断现场的CPSR值。
D:把r0赋值为S_PC的值。根据struct pt_regs的定义(这个数据结构反应了内核栈上的保存的寄存器的排列信息),从低地址到高地址依次为:
ARM_r0
ARM_r1
ARM_r2
ARM_r3
ARM_r4
ARM_r5
ARM_r6
ARM_r7
ARM_r8
ARM_r9
ARM_r10
ARM_fp
ARM_ip
ARM_sp
ARM_lr
ARM_pc<---------add r0, sp, #S_PC指令使得r0指向了这个位置
ARM_cpsr
ARM_ORIG_r0
为什么要给r0赋值?因此kernel不想修改sp的值,保持sp指向栈顶。这边为何不再改变svc模式下sp寄存器的值我的理解应该是这边svc 栈下已经保存了中断发生前的上下文,需要sp寄存器来跟踪这个位置,如果在svc模式下执行软中断继续发生了硬中断,kernel 需要在当前sp寄存器的基础上继续往下压栈保留现场,这也能解释在软中断中嵌套发生硬中断后,kernel 如果返回软中断现场。
E:在内核栈上保存剩余的寄存器的值,根据代码,依次是r0,PC,CPSR和orig r0。执行到这里,内核栈的布局如下图所示:
R0,PC和CPSR来自IRQ mode的stack。实际上这段操作就是从irq stack就中断现场搬移到内核栈上。
F:内核栈上还有两个寄存器没有保持,分别是发生中断时候sp和lr这两个寄存器。这时候,r0指向了保存PC寄存器那个地址(add r0, sp, #S_PC),stmdb r0, {sp, lr}^中的“db”是decrement before,因此,将sp和lr压入stack中的剩余的两个位置。需要注意的是,我们保存的是发生中断那一刻(对于本节,这是当时user mode的sp和lr),指令中的“^”符号表示访问user mode的寄存器。
irq_handler是中断的核心处理部分。irq_handler的处理有两种配置。一种是配置了CONFIG_MULTI_IRQ_HANDLER。这种情况下,linux kernel允许run time设定irq handler。如果我们需要一个linux kernel image支持多个平台,这是就需要配置这个选项。另外一种是传统的linux的做法,irq_handler实际上就是arch_irq_handler_default,具体代码如下:
.macro irq_handler
#ifdef CONFIG_MULTI_IRQ_HANDLER
ldr r1, =handle_arch_irq
mov r0, sp--------设定传递给machine定义的handle_arch_irq的参数
adr lr, BSYM(9997f)----设定返回地址
ldr pc, [r1]
#else
arch_irq_handler_default
#endif
9997:
.endm
对于情况一,machine相关代码需要设定handle_arch_irq函数指针,这里的汇编指令只需要调用这个machine代码提供的irq handler即可(当然,要准备好参数传递和返回地址设定)。情况二要稍微复杂一些(而且,看起来kernel中使用的越来越少),这里不做分析了。简单看一下情况一,就是跳转到handle_arch_irq取处理,然后返回地址为9997,即是指令get_thread_info tsk。看一下在s3c2440 处理器上面,handle_arch_irq是如何注册的。
start_kernel
--------->setup_arch
在setup_arch中,有如下代码:
handle_arch_irq = mdesc->handle_irq;
如果machine 中定义了handle_irq,就使用machine的handle_irq。这里没有定义:
DT_MACHINE_START(S3C2440, "SMDK2440")
.atag_offset = 0x100,
.dt_compat = s3c2440_dt_compat,
.init_irq = s3c2440_init_irq,
.map_io = smdk2440_map_io,
.init_machine = smdk2440_machine_init,
.init_time = samsung_timer_init,
.restart = s3c244x_restart,
MACHINE_END
没有定义的话,还有一次设置的机会:
start_kernel
---------->init_IRQ
------------->s3c2440_init_irq
--------------->s3c24xx_init_intc
----------------->set_handle_irq
----------------->handle_arch_irq=s3c24xx_handle_irq
这就是s3c2440的中断处理函数,再深入的分析放后面的文章。
当中断发生的时候,运行在内核空间,代码会在__irq_svc执行。
.align 5
__irq_svc:
svc_entry----保存发生中断那一刻的现场保存在内核栈上
irq_handler ----具体的中断处理,同user mode的处理。
#ifdef CONFIG_PREEMPT--------和preempt相关的处理,本文不进行描述
get_thread_info tsk //获取task info中的信息,放入r9 寄存器
ldr r8, [tsk, #TI_PREEMPT] @ get preempt count检查抢占标志位,当preempt count不为0 时,不能抢占
ldr r0, [tsk, #TI_FLAGS] @ get flags //获取调度标志位
teq r8, #0 @ if preempt count != 0 //检查preempt count是否为0,如果不为0,不允许抢占,则,把r0寄存器设置为0
movne r0, #0 @ force flags to 0
tst r0, #_TIF_NEED_RESCHED //如果可以抢占,检查调度标志位是否被置上,如果置上,则执行svc_preempt
blne svc_preempt
#endif
svc_exit r5, irq = 1 @ return from exception
保存现场的代码和user mode下的现场保存是类似的,因此这里不再详细描述,只是在下面的代码中内嵌一些注释。上面定义了CONFIG_PREEMPT以后,会有一段特殊的处理,这个应该是linux3.10版本的内核,已经在中断返回的时候,有了重新调度的能力,而不必每次在返回用户空间,才去执行调度。可以简单看一下svc_preempt:
svc_preempt:
mov r8, lr
1: bl preempt_schedule_irq @ irq en/disable is done inside
ldr r0, [tsk, #TI_FLAGS] @ get new tasks TI_FLAGS
tst r0, #_TIF_NEED_RESCHED
moveq pc, r8 @ go again
b 1b
可以看到,会去执行preempt_schedule_irq,这个函数内部__schedule函数,所以这边会直接切到另一个任务里面去,暂时不会返回了,直到当前进程再次被调度,再次被唤醒以后,还会接着判断标志位_TIF_NEED_RESCHED还需不需要继续被抢占,如果不需要,则退出svc_preempt继续执行中断退出流程
.macro svc_entry, stack_hole=0
sub sp, sp, #(S_FRAME_SIZE + \stack_hole - 4)----sp指向struct pt_regs中r1的位置
stmia sp, {r1 - r12} ------寄存器入栈。
ldmia r0, {r3 - r5}
add r7, sp, #S_SP - 4 ------r7指向struct pt_regs中r12的位置
mov r6, #-1 ----------orig r0设为-1
add r2, sp, #(S_FRAME_SIZE + \stack_hole - 4)----r2是发现中断那一刻stack的现场
str r3, [sp, #-4]! ----保存r0,注意有一个!,sp会加上4,这时候sp就指向栈顶的r0位置了
mov r3, lr ----保存svc mode的lr到r3
stmia r7, {r2 - r6} ---------压栈,在栈上形成形成struct pt_regs
.endm
上面 str r3, [sp, #-4]! 指令以后,sp寄存器被设置为保存整个上下文以后的位置,为防止下次发生硬中断做准备
2.2 中断退出过程
(1)中断发生在user mode下的退出过程,代码如下:
get_thread_info tsk------tsk是r9,指向当前的thread info数据结构
mov why, #0--------why是r8
b ret_to_user_from_irq----中断返回
.macro get_thread_info, rd
mov \rd, sp, lsr #13
mov \rd, \rd, lsl #13
.endm
进程的thread_info和svc状态下的stack是放在一块内存里面的,这边是2K,而thread_info从低地址开始放,所以只需要把stack 按2K 对其,即可得到thread_info,并把其放在r9寄存器中
ENTRY(ret_to_user_from_irq)
ldr r1, [tsk, #TI_FLAGS]
tst r1, #_TIF_WORK_MASK---------------A
bne work_pending
no_work_pending:
asm_trace_hardirqs_on ------和irq flag trace相关,暂且略过
/* perform architecture specific actions before user return */
arch_ret_to_user r1, lr----有些硬件平台需要在中断返回用户空间做一些特别处理
ct_user_enter save = 0 ----和trace context相关,暂且略过
restore_user_regs fast = 0, offset = 0------------B
ENDPROC(ret_to_user_from_irq)
A:thread_info中的flags成员中有一些low level的标识,如果这些标识设定了就需要进行一些特别的处理,这里检测的flag主要包括:
#define _TIF_WORK_MASK (_TIF_NEED_RESCHED | _TIF_SIGPENDING | _TIF_NOTIFY_RESUME)
这三个flag分别表示是否需要调度、是否有信号处理、返回用户空间之前是否需要调用callback函数。只要有一个flag被设定了,程序就进入work_pending这个分支。
work_pending:
mov r0, sp @ 'regs'
mov r2, why @ 'syscall'
bl do_work_pending
cmp r0, #0
beq no_work_pending
movlt scno, #(__NR_restart_syscall - __NR_SYSCALL_BASE)
ldmia sp, {r0 - r6} @ have to reload r0 - r6
b local_restart @ ... and off we go
然后去执行do_work_pending,在do_work_pending会先检查是不是被设置了调度抢占标志位,需要调度,如果需要调度的话这边暂时就不会返回,然后再去检查有没有信号需要处理,处理完后退出该函数,可以看到当返回值不为0的时候,会做一些特殊处理,这边先不分析这个,返回值为0直接跳转到no_work_pendingB:从字面的意思也可以看成,这部分的代码就是将进入中断的时候保存的现场(寄存器值)恢复到实际的ARM的各个寄存器中,从而完全返回到了中断发生的那一点。具体的代码如下:
.macro restore_user_regs, fast = 0, offset = 0
ldr r1, [sp, #\offset + S_PSR] ----r1保存了pt_regs中的spsr,也就是发生中断时的CPSR
ldr lr, [sp, #\offset + S_PC]! ----lr保存了PC值,同时sp移动到了pt_regs中PC的位置
msr spsr_cxsf, r1 ---------赋值给spsr,进行返回用户空间的准备
clrex @ clear the exclusive monitor
.if \fast
ldmdb sp, {r1 - lr}^ @ get calling r1 - lr
.else
ldmdb sp, {r0 - lr}^ ------将保存在内核栈上的数据保存到用户态的r0~r14寄存器
.endif
mov r0, r0 ---------NOP操作,ARMv5T之前的需要这个操作
add sp, sp, #S_FRAME_SIZE - S_PC----现场已经恢复,移动svc mode的sp到原来的位置
movs pc, lr --------返回用户空间
.endm
可以看到,在返回用户空间的时候,内核栈的位置会被手动复位,每次从用户空间陷入到内核时,内核栈都是干净的,从内核空间返回用户空间时,restore的只是用户sp,所以内核sp必须手动复位
(2)中断发生在svc mode下的退出过程,代码如下:
.macro svc_exit, rpsr, irq = 0
.if \irq != 0
@ IRQs already off
.else
@ IRQs off again before pulling preserved data off the stack
disable_irq_notrace
.endif
msr spsr_cxsf, \rpsr-----将中断现场的cpsr值保存到spsr中,准备返回中断发生的现场
ldmia sp, {r0 - pc}^ -----这条指令是ldm异常返回指令,这条指令除了字面上的操作,
还包括了将spsr copy到cpsr中
这边是从内核空间的中断上下文返回内核空间被打断的上下文中执行,所以只需要把记录的sp寄存器直接从stack中恢复即可,不需要像返回用户空间那样,手动复位内核sp
由于中断是发生在内核态的,所以处理上会比用户态进入的要少,最终中断处理完以后,还是在内核态接着运行。