linux中断系统那些事之----中断处理过程【转】
R14_irq = address of next instruction to be executed + 4/*将寄存器lr_mode设置成返回地址,即为当前pc的值,因为pc是当前执行指令的下两条指令*/
SPSR_irq = CPSR /*保存处理器当前状态、中断屏蔽位以及各条件标志位*/
CPSR[4:0] = 0b10010 /*设置当前程序状态寄存器CPSR中相应的位进入IRQ模式,注意cpsr是所有模式共享的*/
CPSR[5] = 0 /*在ARM状态执行*/
/*CPSR[6] 不变*/
CPSR[7] = 1 /*禁止正常中断*/
If high vectors configured then
PC=0xFFFF0018 /*将程序计数器(PC)值设置成该异常中断的中断向量地址,从而跳转到相应的异常中断处理程序处执行,对于ARMv7向量表普遍中断是0xFFFF0018*/
else
PC=0x00000018 /*对于低向量*/
假设在用户空间时,产生了外部硬件中断,则这个时候的指令跳转流程如下:- __vectors_start:---------------〉在中断向量表被拷贝后,该地址就是0xffff0000.
- 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----------〉当外部中断产生时,pc直接指向这个地址。
- W(b) vector_fiq + stubs_offset
- .globl __vectors_end
- __stubs_start:
- /*
- * Interrupt dispatcher
- */
- 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
- <span style="font-size:18px">/*
- * Vector stubs.
- *
- * This code is copied to 0xffff0200 so we can use branches in the
- * vectors, rather than ldr's. Note that this code must not
- * exceed 0x300 bytes.
- *
- * Common stub entry macro:
- * Enter in IRQ mode, spsr = SVC/USR CPSR, lr = SVC/USR PC
- *
- * SP points to a minimal amount of processor-private memory, the address
- * of which is copied into r0 for the mode specific abort handler.
- */
- .macro vector_stub, name, mode, correction=0
- .align 5
- vector_\name:
- .if \correction
- sub lr, lr, #\correction //因为硬件处理器是将当前指令的下两条指令的地址存储在lr寄存器中,所以这里需要减4,让他指向被中断指令的下一条,这样当中断被恢复时,可以继续被中断的指令继续执行。
- .endif<span style="white-space:pre"> </span> //需要注意的是,这个时候的lr寄存器,已经是irq模式下的私有寄存器了,在中断产生时,硬件处理器已经自动为他赋了值。
- @
- @ Save r0, lr_<exception> (parent PC) and spsr_<exception>
- @ (parent CPSR)
- @
- stmia sp, {r0, lr} @ save r0, lr//保存r0和lr寄存器,即被中断的下一条指令
- mrs lr, spsr
- str lr, [sp, #8] @ save spsr
- @
- @ Prepare for SVC32 mode. IRQs remain disabled.//准备从中断模式切换到管理模式,不同的模式,对应各自不同的堆栈。
- @
- mrs r0, cpsr
- eor r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE)
- msr spsr_cxsf, r0
- @
- @ the branch table must immediately follow this code
- @
- and lr, lr, #0x0f //获取被中断前,处理器所处的模式
- THUMB( adr r0, 1f )
- THUMB( ldr lr, [r0, lr, lsl #2] )
- mov r0, sp<span style="white-space:pre"> </span>//让r0寄存器指向中断模式下堆栈的基地址
- ARM( ldr lr, [pc, lr, lsl #2] )
- movs pc, lr @ branch to handler in SVC mode,同时将中断模式下的spsr_irq(irq私有的)赋值给cpsr(该寄存器所有模式共享)
- ENDPROC(vector_\name)</span>
- <span style="font-size:18px">__irq_usr:
- usr_entry //进行中断前的硬件上下文的保存
- kuser_cmpxchg_check
- irq_handler
- get_thread_info tsk//获取被中断的用户进程或内核线程所对应的内核栈所对应的thread info结构。
- mov why, #0
- b ret_to_user_from_irq//恢复被中断时的上下文,然后继续被中断的进程或线程的执行
- UNWIND(.fnend )
- ENDPROC(__irq_usr)</span>
- .macro usr_entry
- UNWIND(.fnstart )
- UNWIND(.cantunwind ) @ don't unwind the user space
- sub sp, sp, #S_FRAME_SIZE // #S_FRAME_SIZE的值为72
- ARM( stmib sp, {r1 - r12} ) //尽管当前是处于管理模式,但由于svc和usr的r0-r12是公共的,所以相当于保存用户模式的r1-r12寄存器
- THUMB( stmia sp, {r0 - r12} )
- ldmia r0, {r3 - r5} //将之前保存在中断模式堆栈中的r0_usr,lr,spsr分别存储到r3-r5中
- add r0, sp, #S_PC @ here for interlock avoidance #S_PC=60
- mov r6, #-1 @ "" "" "" ""
- str r3, [sp] @ save the "real" r0 copied
- @ from the exception stack
- @
- @ 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}
- ARM( stmdb r0, {sp, lr}^ )//保存用户模式下的sp_usr,lr_usr
- THUMB( store_user_sp_lr r0, r1, S_SP - S_PC )
- @
- @ Enable the alignment trap while in kernel mode
- @
- alignment_trap r0
- @
- @ Clear FP to mark the first stack frame
- @
- zero_fp
- ifdef CONFIG_IRQSOFF_TRACER
- bl trace_hardirqs_off
- endif
- .endm
cpsr(r16) |
pc(r15) |
lr(r14) |
sp(r13) |
r12(ip) |
r11(fp) |
r10 |
r9 |
r8 |
r7 |
r6 |
r5 |
r4 |
r3 |
r2 |
r1 |
r0 |
上图中的S_FRAME_SIZE, S_PC在arch/arm/kernel/Asm-offsets.c:中定义
DEFINE(S_FRAME_SIZE, sizeof(struct pt_regs));
DEFINE(S_PC, offsetof(struct pt_regs, ARM_pc));
include/asm-arm/Ptrace.h:
struct pt_regs {
long uregs[18];
};
#define ARM_pc uregs[15]
呵呵,pt_regs中对应的就是上面栈上的18个寄存器,ARM_pc是pc寄存器存放在这个数组中的偏移。
接着看get_thread_info, 它也是个宏,用来获取当前线程的地址。他的结构体定义如下:
include/linux/Sched.h:
union thread_union {
struct thread_info thread_info; /*线程属性*/
unsigned long stack[THREAD_SIZE/sizeof(long)]; /*栈*/
};
由它定义的线程是8K字节对齐的, 并且在这8K的最低地址处存放的就是thread_info对象,即该栈拥有者线程的对象,而get_thread_info就是通过把sp低13位清0(8K边界)来获取当前thread_info对象的地址。
arch/arm/kernel/entry-armv.S:
.macro get_thread_info, rd
mov /rd, sp, lsr #13
mov /rd, /rd, lsl #13
.endm
调用该宏后寄存器tsk里存放的就是当前线程的地址了, tsk是哪个寄存器呢,呵呵我们在看:
arch/arm/kernel/entry-header.S:
tsk .req r9 @ current thread_info
呵呵,tsk只是r9的别名而已, 因此这时r9里保存的就是当前线程的地址。
- <span style="font-size:18px">ENTRY(ret_to_user_from_irq)
- ldr r1, [tsk, #TI_FLAGS] //tsk如上所述,是r9寄存器的别名,并且是指向thread_info结构体的
- 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 //针对arm,是dummy的
- restore_user_regs fast = 0, offset = 0//恢复之前用户模式时被中断时所保存的寄存器上下文
- ENDPROC(ret_to_user_from_irq)</span>
- <span style="font-size:18px"> .macro restore_user_regs, fast = 0, offset = 0
- ldr r1, [sp, #\offset + S_PSR] @ get calling cpsr 即为被中断时,处理器的cpsr值
- ldr lr, [sp, #\offset + S_PC]! @ get pc 即为被中断指令的,下一条指令
- msr spsr_cxsf, r1 @ save in spsr_svc //将r1赋值给管理模式下的spsr_svc,这样在movs时,会自动将该值赋值为cpsr
- #if defined(CONFIG_CPU_V6)
- strex r1, r2, [sp] @ clear the exclusive monitor
- #elif defined(CONFIG_CPU_32v6K)
- clrex @ clear the exclusive monitor
- #endif
- .if \fast
- ldmdb sp, {r1 - lr}^ @ get calling r1 - lr
- .else
- ldmdb sp, {r0 - lr}^ @ get calling r0 - lr,将保存在内核栈中的r0到r14恢复到用户模式中的寄存器
- .endif
- mov r0, r0 @ ARMv5T and earlier require a nop
- @ after ldm {}^
- add sp, sp, #S_FRAME_SIZE - S_PC //恢复内核栈到中断产生之前的位置。
- movs pc, lr @ return & move spsr_svc into cpsr
- .endm
- </span>
至此中断汇编部分已经全部处理完成。
问题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,请参考我的上篇blog:linux中断系统那些事之----中断初始化过程
问题2:为什么在异常向量表中,用b指令跳转而不是用ldr绝对跳转?
答:因为使用b指令跳转比绝对跳转(ldr pc,XXXX)效率高,正因为效率高,所以把__stubs_start~__stubs_end之间的代码考到了0xffff0200起始处。
注意:
因为b跳转指令只能在+/-32MB之内跳转,所以必须拷贝到0xffff0000附近。
b指令是相对于当前PC的跳转,当汇编器看到 B 指令后会把要跳转的标签转化为相对于当前PC的偏移量写入指令码。
问题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指令会自动将子程序返回地址保存到当前模式的sp(即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()函数实现向量表的设定操作。