您的位置:首页 > 运维架构 > Linux

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

2013-12-08 17:02 721 查看
我们这里讨论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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: