我
们这里讨论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