[ARM&Linux]Linux下中断处理的上下文保存与切换的一些细节

们这里讨论ARM体系下的Linux中断处理的上下文切换部分的细节。我们只讨论底层汇编处理细节,不考虑上层。

首先,Linux的中断处理程序是经过搬移的,向量表和处理程序距离很近,这是在系统初始化的时候
就完成的。这个地方我们不做深入讨论。

Linux的向量表通过MMU安排在0xffff0000的位置,向量表如下:


	.globl	__vectors_start
__vectors_start:
	swi	SYS_ERROR0
	b	vector_und + stubs_offset
	ldr	pc, .LCvswi + stubs_offset
	b	vector_pabt + stubs_offset
	b	vector_dabt + stubs_offset
	b	vector_addrexcptn + stubs_offset
	b	vector_irq + stubs_offset
	b	vector_fiq + stubs_offset



其中stubs_offset定义为__vectors_start + 0x200 - __stubs_start。
这个__vectors_start就是刚才的这个向量表,而__stubs_start为中断处理程序的开始地址。
向量表在trap_init函数中建立,该函数在start_kernel中较后的位置被建立。

  ----------------------       ---  __kuser_helper_end
 /                      \
 |                      |
 |                      |      ---   __kuser_helper_start -
 |                      |                                 |
 ------------------------      ---   __stubs_end   0xe00
 | |                                 |
 |          |      ---   __stubs_start  -
 | |                                 ^
 | |                                 |
 ------------------------      ---   __vectos_end 0x200
 | |                                 |
 | |                vector 32字节  |
 | |                                 |
 \ /                                 |
  ----------------------       ---  __vectors_start -
 
 整个__stubs的代码如下:

 
.globl	__stubs_start
__stubs_start:

/* 这些都是宏,IRQ_MODE等所有宏都在ptrace.h当中 */
	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
	
/* 这些都是宏 */
	vector_stub	dabt, ABT_MODE, 8

	.long	__dabt_usr			@  0  (USR_26 / USR_32)
	.long	__dabt_invalid			@  1  (FIQ_26 / FIQ_32)
	.long	__dabt_invalid			@  2  (IRQ_26 / IRQ_32)
	.long	__dabt_svc			@  3  (SVC_26 / SVC_32)
	.long	__dabt_invalid			@  4
	.long	__dabt_invalid			@  5
	.long	__dabt_invalid			@  6
	.long	__dabt_invalid			@  7
	.long	__dabt_invalid			@  8
	.long	__dabt_invalid			@  9
	.long	__dabt_invalid			@  a
	.long	__dabt_invalid			@  b
	.long	__dabt_invalid			@  c
	.long	__dabt_invalid			@  d
	.long	__dabt_invalid			@  e
	.long	__dabt_invalid			@  f
	
/* 这些都是宏 */
	vector_stub	pabt, ABT_MODE, 4
	
	.long	__pabt_usr			@  0 (USR_26 / USR_32)
	.long	__pabt_invalid			@  1 (FIQ_26 / FIQ_32)
	.long	__pabt_invalid			@  2 (IRQ_26 / IRQ_32)
	.long	__pabt_svc			@  3 (SVC_26 / SVC_32)
	.long	__pabt_invalid			@  4
	.long	__pabt_invalid			@  5
	.long	__pabt_invalid			@  6
	.long	__pabt_invalid			@  7
	.long	__pabt_invalid			@  8
	.long	__pabt_invalid			@  9
	.long	__pabt_invalid			@  a
	.long	__pabt_invalid			@  b
	.long	__pabt_invalid			@  c
	.long	__pabt_invalid			@  d
	.long	__pabt_invalid			@  e
	.long	__pabt_invalid			@  f
/* 这些都是宏 */
	vector_stub	und, UND_MODE

	.long	__und_usr			@  0 (USR_26 / USR_32)
	.long	__und_invalid			@  1 (FIQ_26 / FIQ_32)
	.long	__und_invalid			@  2 (IRQ_26 / IRQ_32)
	.long	__und_svc			@  3 (SVC_26 / SVC_32)
	.long	__und_invalid			@  4
	.long	__und_invalid			@  5
	.long	__und_invalid			@  6
	.long	__und_invalid			@  7
	.long	__und_invalid			@  8
	.long	__und_invalid			@  9
	.long	__und_invalid			@  a
	.long	__und_invalid			@  b
	.long	__und_invalid			@  c
	.long	__und_invalid			@  d
	.long	__und_invalid			@  e
	.long	__und_invalid			@  f

	.align	5

/* Linux不支持FIQ */
vector_fiq:
	disable_fiq
	subs	pc, lr, #4

/* 这个实际上是v7引入的hypervisor入口 */
vector_addrexcptn:
	b	vector_addrexcptn

	.align	5

.LCvswi:
	.word	vector_swi

	.globl	__stubs_end
__stubs_end:

 

 
 对于上面的代码我们有以下几个结论:
 1. Linux不支持FIQ,因为FIQ这东西是ARM特有的,为了保证代码的通用性,我们不应该使用这些特有的玩意儿。
 不过上面并不是理由,最大的理由是,采用现成Linux中断处理框架的话,没法保证寄存器不被corrupt。
 
 2. 对于中断/异常前的状态,Linux是分类处理的。如果位于内核态(svc模式下)那么执行__xxx_svc的代码。
 如果位于用户态(usr模式下)那么执行__xxx_usr的代码。
 
 我们下面来看vector_stub这个宏,gcc的宏相当强大:
 

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

vector_\name:
	/* 如果需要修正返回地址,那么修正吧 */
	.if \correction
	sub	lr, lr, #\correction
	.endif
	/* 保存r0,lr,spsr,但是sp不变,sp始终指向了r0 */
	stmia	sp, {r0, lr}		@ save r0, lr
	mrs	lr, spsr
	str	lr, [sp, #8]		@ save spsr

	/* 将SVC模式的CPSR保存到SPSR中 */
	mrs	r0, cpsr
	eor	r0, r0, #(\mode ^ SVC_MODE)
	msr	spsr_cxsf, r0

	/* 有趣的代码,lr保存的是进入中断之前的模式,根据这个模式分别让lr为__xxx_usr和__xxx_svc */
	and	lr, lr, #0x0f
	mov	r0, sp
	ldr	lr, [pc, lr, lsl #2]

	/* 跳转的同时将spsr写入cpsr */
	movs	pc, lr			
	.endm



上面的代码会调用__xxx_usr或__xxx_svc。为了简单起见,我们只看irq的处理。
并且假设启用了CONFIG_PREEMPT,未启用TRACE_IRQFLAGS。
我们先看__irq_usr:
注意一点,现在状态位SVC,中断未开启。

	.align 5
__irq_usr:
	usr_entry
	get_thread_info tsk
	ldr	r8, [tsk, #TI_PREEMPT]		
	add	r7, r8, #1			
	str	r7, [tsk, #TI_PREEMPT]
	irq_handler
	ldr	r0, [tsk, #TI_PREEMPT]
	str	r8, [tsk, #TI_PREEMPT]
	teq	r0, r7
	strne	r0, [r0, -r0]
	mov	why, #0
	b	ret_to_user
	.ltorg


Linux的特色就是大量使用宏,这点我很讨厌。
首先是usr_entry这个宏:

.macro	usr_entry
	/* 定义在asm-offsets.c中,也就是struct pt_regs的大小18*4字节 */
	sub		sp, sp, #S_FRAME_SIZE
	/* 先保存r1-r12,r0保存的是IRQ状态下的堆栈指针,这个堆栈中从高到低保存的是被中断前的cpsr,中断返回地址lr,中断前的r0寄存器 */
	stmib	sp, {r1 - r12}
	/* r1 = 中断前的r0,r2 = 中断返回地址lr,r3 = 中断前的cpsr */
	ldmia	r0, {r1 - r3}
	/* PC在struct pt_regs(定义在ptrace.h中)中的偏移,应该是0x3c */
	add		r0, sp, #S_PC		
	mov		r4, #-1			
	/* 保存中断前的r0 */
	str		r1, [sp]		
	
	/* 中断返回地址赋给PC这个空,中断前CPSR赋给CPSR这个空,中断前r0为-1,r0继续指向PC */
	stmia	r0, {r2 - r4}
	/* 将当前的sp和lr保存给lr和sp这两个空 */
	stmdb	r0, {sp, lr}^
	
	/* 定义在entry_header.S中,如果没定义CONFIG_ALIGNMENT_TRAP,这边都为空,一般就是读取LCcralign的值,然后写给CP15的系统控制寄存器 */
	alignment_trap r0
	/* r11/fp = 0 */
	zero_fp
	.endm


经过这个宏,我们在SVC中开辟了一个空间来保存pt_regs,然后我们保存了r0到r12,现在r0指向了pt_regs中的PC,r4为-1,r2为
中断返回地址lr,r3为中断前的cpsr,r11为0。

回到__irq_usr,接下来的宏是get_thread_info tsk。
这个宏也定义在entry_head.S中。我们看看:


tsk	.req	r9		@ current thread_info

	.macro	get_thread_info, rd
	/* 清低13位,也就是对齐到8K,我们知道SVC状态下所有任务的thread_info都保存在连续8K内存的开头 */
	mov	\rd, sp, lsr #13
	mov	\rd, \rd, lsl #13
	.endm


再次回到__irq_usr。

/* 读取thread_info中的preempt_count变量 */
	ldr	r8, [tsk, #TI_PREEMPT]		
	/* preempt_count加1 */
	add	r7, r8, #1			
	str	r7, [tsk, #TI_PREEMPT]
	
	/* 进入irq处理函数 */
	irq_handler
	
	...


现在我们的情况是r7保存了当前preempt_count的值,我们知道当这个值为0的时候将发生重调度。
我们接着进入irq_handler这个宏。

.macro	irq_handler
	get_irqnr_preamble r5, lr
1:	get_irqnr_and_base r0, r6, r5, lr
	movne	r1, sp

	adrne	lr, 1b
	bne	asm_do_IRQ
	
	test_for_ipi r0, r6, r5, lr
	movne	r0, sp
	adrne	lr, 1b
	bne	do_IPI
	
	test_for_ltirq r0, r6, r5, lr
	movne	r0, sp
	adrne	lr, 1b
	bne	do_local_timer
	.endm


依旧由很多宏构成,我们先分析从get_irq_nr_preamble r5,lr到bne asm_do_IRQ
get_irq_nr_preamble定义在和具体体系相关的文件中,一般具体体系都有个entry-macro.S
以我们内核和2440的SOC为例,这里get_irq_nr_preamble为空。
get_irqnr_and_base宏用于获得中断num,保存在r0中。
后面adrne lr,1b会将1标号的地址保存在lr中。之后调用asm_do_IRQ函数。

参数有点意思,现在r0为中断号,r1为SVC的堆栈,也就是指向struct pt_regs的r0。
asm_do_IRQ这个函数定义在体系相关的kernel/irq.c中。我们不讨论这个函数的实现,我们仅需要知道这个
函数用来处理中断。函数返回时会退到标号1处,在标号1处再次判断有没有中断产生,如果没有就退出,有的话
就继续刚才的过程。

如果是SMP平台的话,还要检查核间中断,核间中断的处理是由do_IPI实现的。
LOCAL_TIMER也是SMP平台定义的,这个我们都不用关系,我们只关心中断处理的底层过程。


下面回到__irq_usr中:

/* 再次读取preempt_count信息 */
	ldr	r0, [tsk, #TI_PREEMPT]
	/* aapcs-linux保证r8不会被改动,r8最早为进入中断前的preempt_count */
	str	r8, [tsk, #TI_PREEMPT]
	/* r7也不会发生改动,r7最早为r8+1 */
	/* 如果r0 == r7,说明中断处理函数没有变动preempt_count */
	teq	r0, r7
	/* 如果r0 != r7,说明中断处理函数中变动了preempt_count */
	strne	r0, [r0, -r0]
	/* why == r8 */
	mov	why, #0
	b	ret_to_user
	.ltorg


上面代码并不奇怪,最后直接调用ret_to_user,我们看看这个函数的实现:

ENTRY(ret_to_user)
ret_slow_syscall:
	/* 如果中断开的话,关掉中断,有些体系压根不用实现这个,因为本身Linux自己都不是中断可嵌套的 */
	disable_irq
	/* 获得thread_info的flags成员 */
	ldr	r1, [tsk, #TI_FLAGS]
	/* 是否有需要处理的,有的话跳到work_pending */
	tst	r1, #_TIF_WORK_MASK
	bne	work_pending
	...


我们一路追踪到了work_pending,我们进入这个函数看看。

work_pending:
	/* 判断是否要重调度 */
	tst	r1, #_TIF_NEED_RESCHED
	/* work_resched用于重调度 */
	bne	work_resched
	/* 这两个标志前面那个_TIF_NOTIFY_RESUME用来实现一个notification的机制,这个机制可以
让内核在从内核态切换到和用户态的时候执行一些操作,后者用于实现signal	*/
	tst	r1, #_TIF_NOTIFY_RESUME | _TIF_SIGPENDING
	/* 没有设置的话直接回到前面的函数中 */
	beq	no_work_pending
	/* why == r8 */
	mov	r0, sp				@ 'regs'
	mov	r2, why				@ 'syscall'
	/* 运行notification */
	bl	do_notify_resume
	/* 准备返回 */
	b	ret_slow_syscall		@ Check work again


这里work_resched用于上下文切换,而do_notify_resume用于实现signal。
work_resched函数直接调用schedule函数进行上下文切换。

我们这边只关心ret_slow_syscall,这个函数其实就是ret_to_user。
继续测试是否需要重调度,如果不需要的话,函数继续执行下去

no_work_pending:
	
	arch_ret_to_user r1, lr

	/* 从上面一路下来,我们的堆栈还是平衡的,现在sp指向的是struct pt_regs的r0 */
	ldr	r1, [sp, #S_PSR]		@ get calling cpsr
	/* sp 指向PC */
	ldr	lr, [sp, #S_PC]!		@ get pc
	msr	spsr_cxsf, r1			@ save in spsr_svc
	/* 全部恢复,注意只有当寄存器列表包括PC时,才将spsr赋值给cpsr,这里标识有^表示,该
指令将把r0-lr加载到用户/系统模式下的r0-lr中去。这个^不能再usr和sys模式下使用! */
	ldmdb	sp, {r0 - lr}^			@ get calling r1 - lr
	mov	r0, r0
	/* 堆栈恢复平衡 */
	add	sp, sp, #S_FRAME_SIZE - S_PC
	movs	pc, lr				@ return & move spsr_svc into cpsr


首先又来了一个arch_ret_to_user的宏。这个宏是SOC相关的,比如我们用的2440这个宏就是
个空的。

至此__irq_usr就全部分析完毕,我们接下来分析__irq_svc。__irq_svc运行在SVC模式下,当前r0
为irq模式下的堆栈,堆栈情况如下:

cpsr_svc@被中断前的cpsr
lr@中断返回地址
r0@中断前的r0


此处我们仍考虑配置了内核抢占,__irq_svc全貌如下,我们分几个部分来研究:
1. svc_entry
2. get_thread_info tsk 到 irq_handler
3. irq_handler 到 svc_prempt
4. preempt_return

.align	5
__irq_svc:
	svc_entry
	get_thread_info tsk
	ldr	r8, [tsk, #TI_PREEMPT]		@ get preempt count
	add	r7, r8, #1			@ increment it
	str	r7, [tsk, #TI_PREEMPT]
	irq_handler
	ldr	r0, [tsk, #TI_FLAGS]		@ get flags
	tst	r0, #_TIF_NEED_RESCHED
	blne	svc_preempt
preempt_return:
	ldr	r0, [tsk, #TI_PREEMPT]		@ read preempt value
	str	r8, [tsk, #TI_PREEMPT]		@ restore preempt count
	teq	r0, r7
	strne	r0, [r0, -r0]			@ bug()
	ldr	r0, [sp, #S_PSR]		@ irqs are already disabled
	msr	spsr_cxsf, r0
	ldmia	sp, {r0 - pc}^			@ load r0 - pc, cpsr

	.ltorg



我们先来研究第一部分,svc_entry这个宏作用类似于usr_entry

.macro	svc_entry
	/* 定义在asm-offsets.c中,也就是struct pt_regs的大小18*4字节 */
	sub		sp,	sp,	#S_FRAME_SIZE
	
	/* 先保存r1-r12 */
	stmib	sp,	{r1 - r12}
	/* r0为irq_sp,现在r1为中断前的r0,r2为中断返回地址,r3为中断前的cpsr */
	ldmia	r0,	{r1 - r3}
	
	/* r5指向SP这个SLOT,具体见struct pt_regs */
	add		r5,	sp,	#S_SP
	mov		r4,	#-1
	/* r0指向SVC中断前的栈 */
	add		r0,	sp,	#S_FRAME_SIZE
	
	/* 将中断前的r0保存到r0这个SLOT */
	str		r1,	[sp]
	/* 保存中断前的lr */
	mov		r1,	lr
	/* 分别保存-1,CPSR到CPSR,中断返回地址到PC,中断前lr到LR,中断前堆栈到sp */
	stmia	r5,	{r0 - r4}
	.endm


svc_entry操作完成后,pt_regs已经保存了中断前的所有数据用于返回中断时使用。
下面我们来研究第二部分。

/* 这个没啥好说的,前面说过了,tsk为r9 */
	get_thread_info tsk
	/* 读取当前任务抢占计数 */
	ldr	r8, [tsk, #TI_PREEMPT]		@ get preempt count
	/* 抢占计数+1 */
	add	r7, r8, #1			@ increment it
	/* 保存进去,进行irq_handler */
	str	r7, [tsk, #TI_PREEMPT]
	irq_handler


这一部分代码和usr_entry完全一样。下面我们来研究第三部分

/* 读取是否需要重调度标志 */
	ldr	r0, [tsk, #TI_FLAGS]		@ get flags
	/* 测试是否需要重调度 */
	tst	r0, #_TIF_NEED_RESCHED
	/* 如果需要则跳转到svc_preempt函数 */
	blne	svc_preempt


我们来看看svc_preempt,这个场景是内核态任务抢占。

svc_preempt:
	/* 中断前抢占计数是否为0 */
	teq	r8, #0				@ was preempt count = 0
	/* irq_stat是一个irq_cpustat_t结构体,LCirq_stat保存了这个结构体的地址 */
	ldreq	r6, .LCirq_stat
	/* 非0不进行内核抢占 */
	movne	pc, lr				@ no

	ldr	r0, [r6, #4]			@ local_irq_count
	ldr	r1, [r6, #8]			@ local_bh_count
	/* local_irq_count和local_bh_count都为0 */
	adds	r0, r0, r1
	/* 不为0的话不进行上下文切换 */
	movne	pc, lr
	/* 将抢占计数清位0 */
	mov	r7, #0				@ preempt_schedule_irq
	str	r7, [tsk, #TI_PREEMPT]		@ expects preempt_count == 0
	/* 调用preempt_schedule_irq */
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
	/* 不需要就直接preempt_return */
	beq	preempt_return			@ go again
	/* 否则继续任务切换 */
	b	1b


这一块代码都非常清晰,我们直接回到了preempt_return。

preempt_return:
	/* 再次读抢占计数 */
	ldr	r0, [tsk, #TI_PREEMPT]		@ read preempt value
	/* 还原最早的抢占计数 */
	str	r8, [tsk, #TI_PREEMPT]		@ restore preempt count
	/* 抢占计数变化了么? */
	teq	r0, r7
	/* 变化了就奇怪了 */
	strne	r0, [r0, -r0]			@ bug()
	/* 读取中断前的CPSR */
	ldr	r0, [sp, #S_PSR]		@ irqs are already disabled
	/* 保存到spsr */
	msr	spsr_cxsf, r0
	/* 恢复所有寄存器,返回中断前的状态 */
	ldmia	sp, {r0 - pc}^			@ load r0 - pc, cpsr



你可能感兴趣的:(Linux内核,ARM体系,arm,内核,kernel,linux)