中断上半部

本文摘抄于奔跑吧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.csdn.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.csdn.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系统调用过程_这个我好像学过的博客-CSDN博客)。因此后续对栈的操作都在被打断的进程或者线程的内核栈中进行。

因此在中断处理程序中使用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);
......
}

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值