中断上半部
本文摘抄于奔跑吧linux内核:基于linux内核源码问题分析
为什么中断上下文不能调用睡眠的函数?
调用休眠的函数,最终都会调用schedule函数让出CPU,让另外一个进程被执行。这个过程中涉及到了进程栈空间的切换。虽然在中断上下文也可以通过current获取struct thread_info的信息。但是在栈中保存的内存是被中断打断的进程的栈信息。并未有在中断上下文调用schedule的任何信息,就不能回到当前的中断上下文中。另外中断上半部是关闭了本CPU的中断,因此也无法响应中断。
重新再来回答一下:中断发生时,先是打断正在执行的进程或者线程,然后进入IRQ模式。IRQ中断栈(12字节)将r0,打断进程返回地址(lr_irq)以及cpsr(spsr_irq)保存到中断栈中,然后切换到svc模式。此时使用的栈则是被打断进程的内核栈,然后我们将进程被打断时的寄存器信息,返回地址cpsr等信息由保存到内核栈中。保存完了我们就去执行中断处理函数。这个中断处理函数也是在中断上下文中。
如果在中断上半部进行进程切换。感觉被中断进程的信息已经是完好的保存了的。那么我们在切换到另外一个进程感觉是没有问题的啊?另外schedule函数好像是会主动打开中断,也不存在中断屏蔽了后,切换进程不能响应中断的说法。另外中断栈在入栈的时候,也并没有修改sp_irq的值,感觉也不需要恢复中断栈,而且中断栈里面的内容后面还是被保存到了内核栈里面。
并未有在中断上下文调用schedule的任何信息,就不能回到当前的中断上下文中。
我觉得这句话才是真正的原因。就是我们在中断上下文进行进程切换的时候,我们是使用的进程的内核栈去保存进程上下文的信息。但是中断上下文这些信息,没有对应的结构去保存这些信息(进程被打断了会有各自进程的内核栈 使用pt_regs去保存现场信息。但是如果在中断上半部进行schedule,那么谁来记录当前中断执行的信息呢?目前内核里面是没有对应的结果去保存这些信息。既然没有保存,那么也不能重新回到这里)。因此如果我们调度走了,再也不能回到当前的中断上下文了,即不能继续执行该中断
硬件中断处理过程(中断上半部是关闭了本cpu中断)
# cat /proc/interrupts
CPU0 CPU1 CPU2 CPU3
16: 3092 2942 1436 1997 GIC 29 twd
17: 10 0 0 0 GIC 34 timer
17代码irq_num(内核分配的中断号), 代表硬件中断号
硬件中断号会被内核映射到一个irq_num上
参考链接
https://blog.****.net/wantingfy/article/details/78713660
奔跑吧linux内核
arm通用中断控制器GIC
GIC支持三种中断类型
1、SGI(software generated interrupt)软件触发中断。通常用于多核之间通讯。SGI通常在linux内核中用作IPI中断(inter-process interrupts),并会送达到指定的cpu上。硬件中断号0-15
2、PPI (private peripheral interrupt),这是每个处理器的私有中断。最多支持16个PPI中断(是每个CPU都支持16个??从图里面看感觉是这样)。硬件中断号从ID16-ID31。PPI通常会送到指定的CPU上。硬件中断号16-31
3、SPI外设中断(shared perhipheral Interrupt)公用的外设中断,最多支持998个外设中断,硬件中断号ID32-ID1019.
注册中断
中断处理中序最近的工作是通知硬件,设备的中断已经被接收。中断处理程序要求快速完成并且退出中断(因为中断上半部是关闭了中断的,如果时间太长会导致很多中断都处理不到)。如果中断处理程序任务大,就和快速处理中断的初衷冲突。因此中断上下半部就诞生了
上半部完成尽可能少的比较紧急的功能,它往往只是简单地读取寄存器中的中断状态等。eg上半部将数据copy到内存中,下半部就执行对数据的处理。
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)
extern int __must_check
request_threaded_irq(unsigned int irq, irq_handler_t handler,irq_handler_t thread_fn,
unsigned long flags, const char *name, void *dev);
在linux内核里面,中断具有最高的优先级。因此中断会打断进程,内核线程。只有等到所有挂起等待(pending)的中断和软中断处理完成后,才会执行进程调度。这样就会造成实时认为可能得不到及时处理。中断上下文(中断处理程序, softirq软中断。tasklet等)总会打断抢占进程上下文。
因此出现了中断线程化。将中断的一些认为交给内核线程来运行。这样实时进程可以有比中断线程更高的优先级。
arm中断处理流程:1、硬件处理,现场保护;2、软件部分,从中断向量开始
1、硬件处理:
CPU感知到中断发生之后,硬件会自动做如下一些事情。
a)保存中断发生时的状态寄存器(CPSR)到SPSR_irq寄存器中。
b)修改CPSR寄存器的M域,设置为IRQ Mode,切换处理器模式。让CPU进入处理器模式(processor mode)中的IRQ模式。
c)CPSR中的IRQ/FIQ位置1.即自动关闭中断IRQ/FIQ(那个感觉在中断上半部是屏蔽了所有的中断,因此在中断上半部期间是无法响应其他中断的)
d)保存返回地址待LR_irq寄存器中(那就只是保存了被打断程序的返回地址)
e)硬件自动跳转到中断向量表的IRQ向量中。即将PC转到中断向量处。
从中断返回时,需要软件实现如下操作:
a)从SPSR_irq寄存器中恢复数据到CPSR
b)将LR_irq寄存器的内容恢复到PC
2、软件部分,从中断向量开始
下面这个就是中断向量表
__vectors_start:
W(b) vector_rst
W(b) vector_und
W(ldr) pc, __vectors_start + 0x1000
W(b) vector_pabt
W(b) vector_dabt
W(b) vector_addrexcptn
W(b) vector_irq
W(b) vector_fiq
当CPU检测到外设中断发生后,会跳转到异常向量表中的IRQ表项。 vector_irq其定义
/*
* 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
vector_stub irq, IRQ_MODE, 4对于的代码如下:主要是负责往IRQ中断栈中填写信息,完成后切换到SVC模式
.macro vector_stub, name, mode, correction=0
.align 5
vector_\name:
/*
https://blog.****.net/weixin_44722536/article/details/106115350
*/
.if \correction
sub lr, lr, #\correction
.endif
@
@ Save r0, lr_<exception> (parent PC) and spsr_<exception>
@ (parent CPSR)
@
/*
[sp] = r0, [sp+4]=lr,不改变sp的值
这个lr好像是被打断出的返回地址
另外此时是处于IRQ模式,sp指向了IRQ模式的栈空间,
irq模式栈空间只有12字节,分别用来保存lr,r0,spsr_irq
*/
stmia sp, {r0, lr} @ save r0, lr
/* mrs read&store 将spsr赋值给lr*/
mrs lr, spsr
/* [sp+8] = lr(spsr) */
str lr, [sp, #8] @ save spsr
@
@ Prepare for SVC32 mode. IRQs remain disabled.
@
/*
从准备irq模式进入svc模式,为什么是准备呢?
因为这里是修改的spsr寄存器,而不是直接修改cpsr
只有修改cpsr寄存器才会导致模式切换
*/
mrs r0, cpsr
eor r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE)
msr spsr_cxsf, r0
@
@ the branch table must immediately follow this code
@
/*
lr里面此时保存的是spsr,获取spsr的低4位
spsr里面保存的是被打断程序的cpsr
如果中断发生在内核低四位为3,即lr=3
如果发生用户空间,lr=0
*/
and lr, lr, #0x0f
THUMB( adr r0, 1f )//r0=1f
THUMB( ldr lr, [r0, lr, lsl #2] )//lr = r0 + lr*4
/* 通过r0将IRQ模式的栈指针传给即将跳转的函数 */
mov r0, sp
/*
执行 ldr lr,[pc,lr,lsl#2]时。
PC等于 movs pc,lr下面一条指令。也就是此时的PC指向.long __irq_usr
所以如果中断发生在用户空间,那么lr = pc + 0*4
如果在内核空间 lr = pc + 3*4 指向.long __irq_svc
*/
ARM( ldr lr, [pc, lr, lsl #2] )//lr = pc + lr*4
/* movs中的s表示把SPSR copy到CPSR,从而在这里才真正实现了模式的切换*/
movs pc, lr @ branch to handler in SVC mode
ENDPROC(vector_\name)
.align 2
@ handler addresses follow this label
1:
.endm
.if \correction
sub lr, lr, #\correction
.endif
下图就是ARM的中断栈
1、对于lr = lr - 4:假设正在执行指令A时发生了中断,由于arm流水线和指令预取等原因PC指向A+8。需要等到指令A指向完成才能处理中断。而此时PC已经更新到A+12,lr = pc - 4(正在执行指令的下一条指令,arm处理器约定),即A+8。因此返回的地址应该是pc = lr-4
2、IRQ模式栈空间大小12字节。保存的数据为r0,lr和spsr(其实是被打断时的cspsr)
3、 ARM( ldr lr, [pc, lr, lsl #2] )//lr = pc + lr*4。这个指令怎么理解。其实也是和上面的取值,译码,执行有关。
执行 ldr lr,[pc,lr,lsl#2]时。PC等于 A + 8。也就是此时的PC指向.long __irq_usr。
所以如果中断发生在用户空间,那么lr = pc + 0*4。如果在内核空间 lr = pc + 3*4 指向.long __irq_svc
mrs r0, cpsr
eor r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE)
msr spsr_cxsf, r0
4、修改spsr寄存器是不会引人处理去模式转换的。因此上面的汇编注释写的是准备进入svc模式。只有修改CPSR寄存器才会导致arm处理器模式变化。因此最后的movs pc,lr才是真正进入svc模式的地方,具体原因看代码注释。
movs pc, lr @ branch to handler in SVC mode
假设中断发生在用户空间,则跳转到了__irq_usr。此时已经进入了svc模式
__irq_usr:
usr_entry//保存中断前的硬件上下文
kuser_cmpxchg_check
irq_handler
get_thread_info tsk//将进程的thread_info保存到r9中
/*
why .req r8 @ Linux syscall (!= 0)
将r8设置为0
*/
mov why, #0
b ret_to_user_from_irq//恢复被中断是的上下文,然后继续执行被打断的进程或者线程的执行
usr_entry主要用于将usr模式下的寄存器、中断返回地址保存到堆栈中。正如上面说的那样,执行到这里的时候处理器已经进入到svc模式了。这个时候的sp其实已经从前面的sp_irq变为了sp_svc了。而这里的sp_svc正是被中断进程的内核栈(具体原因可以查看arm系统调用过程_这个我好像学过的博客-****博客)。因此后续对栈的操作都在被打断的进程或者线程的内核栈中进行。
因此在中断处理程序中使用current获取到的是被中断打断的进程
.macro usr_entry
UNWIND(.fnstart )
UNWIND(.cantunwind ) @ don't unwind the user space
/*
和系统调用过程一样,进入到svc模式,会将sp_svc - sizeof(pt_regs)
从svc模式的栈中开辟一段空间,用来保存用户态的现场信息
*/
sub sp, sp, #S_FRAME_SIZE
/* 将r1-r12保存到栈中,sp值不变 */
ARM( stmib sp, {r1 - r12} )
THUMB( stmia sp, {r0 - r12} )
/*
在从irq模式进入svc模式前,r0被设置为了sp_irq
这里其实就是把r0,lr(中断前的返回地址),spsr_irq(被打断时的cpsr),即IRQ模式的栈取出来;
r0在栈中地址低, spsr_irq高
那r3 = r0, r4 = lr, r5 = spsr_irq
*/
ldmia r0, {r3 - r5}
/* r0 = sp + S_PC */
add r0, sp, #S_PC @ here for interlock avoidance
mov r6, #-1 @ "" "" "" ""
/*
[sp] = r3,此时r3里面保存的就是IRQ模式中保存的r0
将r0保存到SVC模式的栈中
至此r0-r12已经被保存到svc模式的栈中
*/
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
@
/*
r0此时指向S_PC,即ARM_pc uregs[15]
r4-r6里面内容分别是被打断进程的返回地址(lr), r5(spsr_irq = CPSR), r6=-1
至此pt_regs里面的17个成员全部保存完毕
*/
stmia r0, {r4 - r6}
/*
r0 = r0-4, [r0] = lr_user; r0 = r0-4, [r0] = sp_user
*/
ARM( stmdb r0, {sp, lr}^ )
THUMB( store_user_sp_lr r0, r1, S_SP - S_PC )
@
@ Enable the alignment trap while in kernel mode
@
alignment_trap r0, .LCcralign
@
@ Clear FP to mark the first stack frame
@
zero_fp
#ifdef CONFIG_IRQSOFF_TRACER
bl trace_hardirqs_off
#endif
ct_user_exit save = 0
.endm
执行完__irq_usr后,被打断进程的内核栈用保存的数据,如下图
.macro irq_handler
#ifdef CONFIG_MULTI_IRQ_HANDLER
ldr r1, =handle_arch_irq
mov r0, sp
/*
#define BSYM(sym) sym
我觉得9997f应该就是表示9997是一个label
因此是返回地址就是下面的9997
*/
adr lr, BSYM(9997f)
ldr pc, [r1]
#else
arch_irq_handler_default
#endif
9997:
.endm
irq_handler之后的部分和中断发生在内核是一样的。下面有对齐展开。处理完中断之后,返回到__irq_usr中执行ret_to_user_from_irq。
ENTRY(ret_to_user_from_irq)
ldr r1, [tsk, #TI_FLAGS]
tst r1, #_TIF_WORK_MASK
bne work_pending//好像是有待处理的信号或者其他什么事情,不清楚
no_work_pending:
asm_trace_hardirqs_on
/* perform architecture specific actions before user return */
arch_ret_to_user r1, lr//空的
ct_user_enter save = 0
/* 恢复用户态寄存器 */
restore_user_regs fast = 0, offset = 0
ENDPROC(ret_to_user_from_irq)
下面这个和arm系统调用返回用户态的代码是一样的 。但是到目前为止,怎么没有看到恢复中断栈的地方呢?这个不需要恢复嘛??刚刚回去看了一下,即使向中断栈中保存了内容,但是sp_irq没有被修改,因此也不用恢复中断栈
.macro restore_user_regs, fast = 0, offset = 0
/* r1 = spsr_svc, spsr_svc其实在进入svc模式时,保存了用户态的cpsr*/
ldr r1, [sp, #\offset + S_PSR] @ get calling cpsr
/*
这里面保存的是用户态的返回地址
vector_swi里面有一句
lr里面保存的是用户态的返回地址,这个指令将其保存到pt_regs里面的pc里面去了
str lr, [sp, #S_PC] @ Save calling PC
*/
ldr lr, [sp, #\offset + S_PC]! @ get pc
/* 将r1保存到spsr的控制域(c),状态域(s),x,f.感觉就是等同spsr */
msr spsr_cxsf, r1 @ save in spsr_svc
#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
/* r0 是保存在栈上的,也需要一并重新加载 */
ldmdb sp, {r0 - lr}^ @ get calling r0 - lr
.endif
mov r0, r0 @ ARMv5T and earlier require a nop
@ after ldm {}^
/*
恢复内核栈指针为初始值
ldmdb指令会修改sp的值
因此此时sp回到原来位置,只需要增加S_FRAME_SIZE - S_PC大小
*/
add sp, sp, #S_FRAME_SIZE - S_PC
/* 将用户态返回地址设置为pc,去执行用户态代码,并且movs,隐含将spsr复制到cpsr中,这个也实现了arm处理器模式切换为user模式 */
movs pc, lr
假设中断发生在内核空间,则跳转到了__irq_svc
__irq_svc:
@将中断现场保存到内核栈中
svc_entry
@中断处理过程
irq_handler
@如果开启了抢占功能,则中断返回时会检查是否可以抢占发生中断时的进程
@检查thread_info->preempt_count是否为0
#ifdef CONFIG_PREEMPT
get_thread_info tsk
ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
ldr r0, [tsk, #TI_FLAGS] @ get flags
teq r8, #0 @ if preempt count != 0
movne r0, #0 @ force flags to 0
tst r0, #_TIF_NEED_RESCHED
blne svc_preempt
#endif
svc_exit r5, irq = 1 @ return from exception
UNWIND(.fnend )
ENDPROC(__irq_svc)
保存中断现场:
.macro svc_entry, stack_hole=0
UNWIND(.fnstart )
UNWIND(.save {r0 - pc} )
/* sp执行r1的位置 */
sub sp, sp, #(S_FRAME_SIZE + \stack_hole - 4)
#ifdef CONFIG_THUMB2_KERNEL
SPFIX( str r0, [sp] ) @ temporarily saved
SPFIX( mov r0, sp )
SPFIX( tst r0, #4 ) @ test original stack alignment
SPFIX( ldr r0, [sp] ) @ restored
#else
/* sp的bit2是否为0,从0开始 */
SPFIX( tst sp, #4 )
#endif
/* bit2为0,则减4 */
SPFIX( subeq sp, sp, #4 )
/* 将r1-r12保存到sp_svc栈中 */
stmia sp, {r1 - r12}
/*
在从irq模式进入svc模式前,r0被设置为了sp_irq
这里其实就是把r0,lr(中断前的返回地址),spsr_irq(被打断时的cpsr),即IRQ模式的栈取出来;
r0在栈中地址低, spsr_irq高
那r3 = r0, r4 = lr, r5 = spsr_irq
*/
ldmia r0, {r3 - r5}
/*
r7指向pt_regs里面的r[13]/sp
为什么是#S_SP-4? 因为sp的位置是r1,因此需要-4
*/
add r7, sp, #S_SP - 4 @ here for interlock avoidance
mov r6, #-1 @ "" "" "" ""
/*
r2指向pt_regs中r[17] orig_r0
为什么r2是发生中断那一刻stack现场呢???
难道和目前代码表示中断发生在内核有关。
感觉也有道理。此时所处的栈就是被打断的内核线程的栈sp_svc
从内核栈切出去,进入IRQ模式,然后有切回来了而已.
因此 在刚进入svc_entry时的sp_svc就是被打断线程的内核栈.
接下来我们在打断线程的内核栈的栈空间中向下继续开辟空间,保存中断现场
sub sp, sp, #(S_FRAME_SIZE + \stack_hole - 4)
因此我们只需要将sp重新加回去,不就是中断现场的sp了嘛
*/
add r2, sp, #(S_FRAME_SIZE + \stack_hole - 4)
SPFIX( addeq r2, r2, #4 )
/*
r3里面保存的是从IRQ栈中获取的r0, sp_svc在开栈的时候执行的就是r1
sp - 4此时就是r0的位置
!这个是什么啊?sp的值被改变了??
*/
str r3, [sp, #-4]! @ save the "real" r0 copied
@ from the exception stack
/*
不知道这个lr_svc里面保存的是哪个地址
那么按照上面r2指向内核线程被中断时的内核栈现场这样解释
这个lr,也是被打断时的现场.从内核(svc)-->IRQ mode-->svc,这几个模式转换过程中
lr并没有变过,
因此下面的注释也能解释通了
*/
mov r3, lr
@
@ We are now ready to fill in the remaining blanks on the stack:
@
@ r2 - sp_svc
@ r3 - lr_svc
@ r4 - lr_<exception>, already fixed up for correct return/restart
@ r5 - spsr_<exception>
@ r6 - orig_r0 (see pt_regs definition in ptrace.h)
@
/*
将r2-r7保存到pt_regs中
r7是pt_regs中的r[13]
r2-r6的值上面的注释写了
*/
stmia r7, {r2 - r6}
#ifdef CONFIG_TRACE_IRQFLAGS
bl trace_hardirqs_off
#endif
.endm
svc_entry表示中断发生在内核空间,用来保存内核空间的中断现场。
add r2, sp, #(S_FRAME_SIZE + \stack_hole - 4)//r2是发现中断那一刻stack的现场
为什么S_FRAME_SIZE - 4(stack_hole 为0)就是发生中断那一刻的内核栈现场??
之前说过,svc模式下的内核栈刚好就是被中断打断进程或者线程的内核栈。
中断发生的整个流程是这样的。内核线程正在执行(假设是内核线程,此时就是在svc模式下,因此sp_svc就是指向了该内核线程的内核栈),然后中断到来,打断了该内核线程的执行,进入IRQ模式,在中断栈(sp_irq)中保存信息。最后又会切换回到svc模式,此时使用的栈仍然为被打断内核线程的内核栈,在该内核栈的栈空间中保存现场信息pt_regs。
因此可以得出从IRQ模式回到SVC模式的sp_svc起始就是被打断现场的内核栈。我们在svc_entry最开始就向下开辟了S_FRAME_SIZE + \stack_hole - 4大小的栈空间,因此将sp增加同样的大小
即可得到被打断线程的栈。
mov r3, lr
同样,当时在看这个地方时,不清楚lr_svc里面的内容是什么,svc_entry里面并没有向lr_svc里面写东西。其实lr_svc就是内核线程被打断的现场信息,我们只需要往栈里面保存信息即可。
先将中断退出流程贴出来。
其实中断退出流程和之前的也差不多。主要就是将栈中保存的pt_regs里面保存的寄存器的值恢复到寄存器中
#ifndef CONFIG_THUMB2_KERNEL
/*
rpsr = r5, r5里面的值来源于IRQ中断栈,IRQ中断栈里面的内容则是发生中断时的cpsr
irq = 1
*/
.macro svc_exit, rpsr, irq = 0
.if \irq != 0
@ IRQs already off
#ifdef CONFIG_TRACE_IRQFLAGS//未定义CONFIG_TRACE_IRQFLAGS,不走
@ The parent context IRQs must have been enabled to get here in
@ the first place, so there's no point checking the PSR I bit.
bl trace_hardirqs_on
#endif
.else
@ IRQs off again before pulling preserved data off the stack
disable_irq_notrace
#ifdef CONFIG_TRACE_IRQFLAGS//未定义CONFIG_TRACE_IRQFLAGS,不走
tst \rpsr, #PSR_I_BIT
bleq trace_hardirqs_on
tst \rpsr, #PSR_I_BIT
blne trace_hardirqs_off
#endif
.endif
msr spsr_cxsf, \rpsr//恢复spsr
#if defined(CONFIG_CPU_V6)
ldr r0, [sp]
strex r1, r2, [sp] @ clear the exclusive monitor
ldmib sp, {r1 - pc}^ @ load r1 - pc, cpsr
#elif defined(CONFIG_CPU_32v6K)
clrex @ clear the exclusive monitor
ldmia sp, {r0 - pc}^ @ load r0 - pc, cpsr
#else
/*
这里只是恢复了pc的值,但是没有修改cpsr的值呢??
是因为不需要切换模式,还是cpsr的值并没有变化啊
*/
ldmia sp, {r0 - pc}^ @ load r0 - pc, cpsr
#endif
.endm
svc_exit r5, irq = 1 @ return from exception
/*
rpsr = r5, r5里面的值来源于IRQ中断栈,IRQ中断栈里面的内容则是发生中断时的cpsr
irq = 1
*/
.macro svc_exit, rpsr, irq = 0
可以看到在发生中断时rpsr传入的是r5。我感觉这个r5就是保存的cpsr的原因是因为函数f在调用子程序,当子程序返回后,函数f里面的大部分寄存器应该都会恢复为调用之前吧
中断处理
/*
* Interrupt handling.
*/
.macro irq_handler
#ifdef CONFIG_MULTI_IRQ_HANDLER
ldr r1, =handle_arch_irq
mov r0, sp
adr lr, BSYM(9997f)
ldr pc, [r1]
#else
arch_irq_handler_default
#endif
9997:
.endm
#ifdef CONFIG_MULTI_IRQ_HANDLER
void __init set_handle_irq(void (*handle_irq)(struct pt_regs *))
{
if (handle_arch_irq)
return;
handle_arch_irq = handle_irq;
}
#endif
handle_arch_irq 0x80008588
System.map中能找到80008588 t gic_handle_irq
handle_arch_irq=gic_handle_irq
static asmlinkage void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
{
u32 irqstat, irqnr;
struct gic_chip_data *gic = &gic_data[0];
void __iomem *cpu_base = gic_data_cpu_base(gic);
do {
irqstat = readl_relaxed(cpu_base + GIC_CPU_INTACK);
irqnr = irqstat & ~0x1c00;//得到硬件中断号
/* (15,1021)说明是外设和PPI中断 */
if (likely(irqnr > 15 && irqnr < 1021)) {
irqnr = irq_find_mapping(gic->domain, irqnr);//根据硬件中断号,得到内核分配的irq_num
handle_IRQ(irqnr, regs);
continue;
}
/* 0-15是一个SGI中断 */
if (irqnr < 16) {
writel_relaxed(irqstat, cpu_base + GIC_CPU_EOI);
#ifdef CONFIG_SMP
handle_IPI(irqnr, regs);
#endif
continue;
}
break;
} while (1);
}
struct pt_regs *regs就是被中断任务的执行现场
set_irq_regs:是将一个per-CPU变量__irq_regs保存到old_regs中,然后将被中断进程的现场(这个是中断现场吗??),保存到__irq_regs中。
static inline struct pt_regs *set_irq_regs(struct pt_regs *new_regs)
{
struct pt_regs *old_regs;
old_regs = __this_cpu_read(__irq_regs);
__this_cpu_write(__irq_regs, new_regs);
return old_regs;
}
void handle_IRQ(unsigned int irq, struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);
irq_enter();//进入中断上下文
/*
* Some hardware gives randomly wrong interrupts. Rather
* than crashing, do something sensible.
*/
if (unlikely(irq >= nr_irqs)) {
if (printk_ratelimit())
printk(KERN_WARNING "Bad IRQ%u\n", irq);
ack_bad_irq(irq);
} else {
generic_handle_irq(irq);
}
irq_exit();//退出中断上下文
set_irq_regs(old_regs);
}
//将preempt_count的hard_rq域+1,中断嵌套深度+1,通知linux内核现在处于硬件中断过程
//硬件处理完成后,通过减少HARDIRQ计数,告知内核硬件中断处理过程完成
#define __irq_enter() \
do { \
account_irq_enter_time(current); \
preempt_count_add(HARDIRQ_OFFSET); \
trace_hardirq_enter(); \
} while (0)
void irq_exit(void)
{
#ifndef __ARCH_IRQ_EXIT_IRQS_DISABLED
local_irq_disable();
#else
WARN_ON_ONCE(!irqs_disabled());
#endif
account_irq_exit_time(current);
trace_hardirq_exit();
preempt_count_sub(HARDIRQ_OFFSET);
if (!in_interrupt() && local_softirq_pending())
invoke_softirq();
tick_irq_exit();
rcu_irq_exit();
}
generic_handle_irq-->generic_handle_irq_desc-->desc->handle_irq
对于SPI(shared perhipheral Interrupt)公用的外设中断
desc->handle_irq=handle_fasteoi_irq
void
handle_fasteoi_irq(unsigned int irq, struct irq_desc *desc)
{
raw_spin_lock(&desc->lock);
if (unlikely(irqd_irq_inprogress(&desc->irq_data)))
if (!irq_check_poll(desc))
goto out;
desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
kstat_incr_irqs_this_cpu(irq, desc);
/*
* If its disabled or no action available
* then mask it and get out of here:
*/
if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {
desc->istate |= IRQS_PENDING;
mask_irq(desc);//没有指定处理函数或者中断被关闭了.直接屏蔽该中断
goto out;
}
//oneshot,不支持中断嵌套,屏蔽该中断源??不是很明白
if (desc->istate & IRQS_ONESHOT)
mask_irq(desc);
preflow_handler(desc);
handle_irq_event(desc);
if (desc->istate & IRQS_ONESHOT)
cond_unmask_irq(desc);
out_eoi:
//调用中断控制器的irq_eoi函数发生EOI信号,通知中断控制器已经处理完毕
desc->irq_data.chip->irq_eoi(&desc->irq_data);
out_unlock:
raw_spin_unlock(&desc->lock);
return;
out:
if (!(desc->irq_data.chip->flags & IRQCHIP_EOI_IF_HANDLED))
goto out_eoi;
goto out_unlock;
}
irqreturn_t handle_irq_event(struct irq_desc *desc)
{
struct irqaction *action = desc->action;
irqreturn_t ret;
//清除pendingj
desc->istate &= ~IRQS_PENDING;
//设置IRQD_IRQ_INPROGRESS,表示正在处理硬件中断
irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS);
raw_spin_unlock(&desc->lock);
ret = handle_irq_event_percpu(desc, action);
raw_spin_lock(&desc->lock);
irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);
return ret;
}
handle_irq_event_percpu:循环遍历中断描述符的action链表,一次执行action中的primary handler。如果返回值为IRQ_WAKE_THREAD,说明需要换线中断内核线程;如果返回IRQ_HANDLED说明,该中断已经处理完毕。
irqreturn_t
handle_irq_event_percpu(struct irq_desc *desc, struct irqaction *action)
{
irqreturn_t retval = IRQ_NONE;
unsigned int flags = 0, irq = desc->irq_data.irq;
do {
irqreturn_t res;
trace_irq_handler_entry(irq, action);
res = action->handler(irq, action->dev_id);
trace_irq_handler_exit(irq, action, res);
if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pF enabled interrupts\n",
irq, action->handler))
local_irq_disable();
switch (res) {
case IRQ_WAKE_THREAD:
/*
* Catch drivers which return WAKE_THREAD but
* did not set up a thread function
*/
if (unlikely(!action->thread_fn)) {
warn_no_thread(irq, action);
break;
}
irq_wake_thread(desc, action);
/* Fall through to add to randomness */
case IRQ_HANDLED:
flags |= action->flags;
break;
default:
break;
}
retval |= res;
action = action->next;
} while (action);
add_interrupt_randomness(irq, flags);
if (!noirqdebug)
note_interrupt(irq, desc, retval);
return retval;
}
硬件中断处理过程(中断上半部??),软中断处理过程,以及NMI中断处理过程都属于在中断上下文
整体过程:
当GIC告知有中断发生时,会打断当前处理的进程。然后硬件自动保存一些信息。完了之后软件做的事情从中断向量表开始(在entry_armv.S)
1 在中断向量中,保存一些信息,从中断模式进入到svc模式,然后依据CSPR寄存器的m域,选择跳转到irq_usr(如果中断发生时,在用户空间)或者__irq_svc(发生在内核空间)
2 然后保存中断现场usr_entry或者svc_entry
3 执行中断处理过程irq_handler-->handle_arch_irq=gic_handle_irq(在该函数中读取GIC的寄存器获取硬件返回的中断号)-->handle_IRQ(外设和PPI中断走这个函数,修改preempt_count HARD_IRQ域,正是进入中断上下文)-->generic_handle_irq-->desc->handle_irq=handle_fasteoi_irq(在函数末尾会有返回eoi信号,表示中断处理完毕)-->handle_irq_event--->handle_irq_event_percpu(遍历action链表的handler,对于共享的中断就存在多个action)
4 在action的handler中感觉都是读取硬件的信息,复位硬件。然后触发中断下半部执行即可
例如tasklet.那么此时action对应的handler则是test_handler,然后在test_handler调用tasklet_schedle触发中断下半部,即可返回。至此中断上半部执行完毕。在handle_IRQ返回时也会修改preempt_count,退出中断上下文。同时硬件也会将SPSR_irq寄存器备份的CPSR的值恢复到CPSR,将LR_irq寄存器保存的被打断程序的返回地址恢复到PC中(如果不发生抢占)
/* 定义 taselet */
struct tasklet_struct testtasklet;
/* tasklet 处理函数 */
void testtasklet_func(unsigned long data)
{
/* tasklet 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
......
/* 调度 tasklet */
tasklet_schedule(&testtasklet);
......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{
......
/* 初始化 tasklet */
tasklet_init(&testtasklet, testtasklet_func, data);
/* 注册中断处理函数 */
request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
......
}