Linux 中断处理入口

ARM中断

本文会介绍linux 中断系统 中与ARM架构相关的部分,对于通用的中断处理(平台无关的),有另一篇博客介绍。

首先,容我在开头就指出 一个重要的结论:linux只会让代码运行在两个空间,user space 和 kernel space,这是与平台无关的。对于ARM架构来说,user space对应user mode,kernel space 则对应SVC mode。对于ARM 中的剩余mode,linux 会将其业务都放到SVC mode 中处理。例如,当IRQ产生时,cpu会短暂的进入IRQ mode,保存寄存器后,立刻切换到SVC mode,在SVC mode 中执行中断处理函数。

总的来说,linux 希望在user mode处理应用逻辑,在svc mode 处理其他的代码逻辑。有了这个认识,就让我们开始学习Linux 是如何从头处理arm 中断的。

一、ARM中断

本文只追踪ARM 的IRQ中断,如下所示,ARM core 有R0-R15 16个通用寄存器和一个CPSR 寄存器,ARM寄存器的定义可以参考另一篇博客。这里只需要知道重点是:

红色框的部分是banked 寄存器,当cpu 处于user mode时,使用的是R13、R14,当切换为IRQ mode 时,R13寄存器实际上对应的是SP_svc,R14寄存器实际上对应的是LR_svc。在不同mode下,虽然寄存器在代码的命名上相同,但实际的物理电路却是不一样的。

在这里插入图片描述

当ARM core 接受到一个IRQ中断时,硬件会自动完成以下步骤:

  1. 将CPSR寄存器的值复制到SPSR_irq寄存器中,保存被中断时的cpu状态
  2. 将返回地址保存到LR_irq寄存器中,以便将来能继续执行
  3. 设置CPSR,切换到IRQ mode,并且关闭中断
  4. 将PC 设置为中断向量表中IRQ 处理函数的地址

上面4步由硬件完成后,cpu 就会去执行中断向量表IRQ处理函数,这个函数在linux 初始化时就设置好了。

二、vector_stub

2.1、中断向量表

我们开始进入linux 的代码世界,幸运的是,我们所需的代码都在 arch/arm/kernel/entry-armv.S 中,其中中断向量表的定义为:

	.section .vectors, "ax", %progbits
.L__vectors_start:
	W(b)	vector_rst
	W(b)	vector_und
	W(ldr)	pc, .L__vectors_start + 0x1000
	W(b)	vector_pabt
	W(b)	vector_dabt
	W(b)	vector_addrexcptn
	W(b)	vector_irq
	W(b)	vector_fiq

可以看到每个向量对应一种异常模式,可是我搜索了vector_irq,却找不到其定义,这是为何?

我们在文章的开头中提到,linux 希望将所有异常的实际处理逻辑都放到svc mode 中去处理,故在此,linux 定义了一个宏vector_stub

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

vector_\name:
	......
ENDPROC(vector_\name)

并且在代码中有 vector_stub irq, IRQ_MODE, 4 ,将其宏展开之后就是:

	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)

@宏展开后:

vector_irq:
	......
ENDPROC(vector_irq)
	.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)

原来vector_irq 就是在这里定义的。

vector_stub 宏的作用就是:

  1. 把被打断执行的程序的寄存器保存到异常模式的栈中
  2. 将cpu mode切换到svc mode
  3. 根据被打断时的cpu mode,跳转到指定的函数处理

结合文章开头说的,linux 希望将所有异常的实际处理逻辑都放到svc mode 中去处理,vector_stub概括了所有异常所需的操作,保存寄存器,切到svc mode,执行对应的代码。具体的实现如下:

#name 是名称,mode 是异常模式,correction是纠正lr为下一条指令
	.macro	vector_stub, name, mode, correction=0

vector_\name:
	# 对lr 进行纠正,将lr设置成 "被打断指令"的下一条指令,具体见附录说明1
	.if \correction
	sub	lr, lr, #\correction
	.endif

	# 将r0,lr 保存到栈中
	stmia	sp, {r0, lr}
	# 把spsr复制到lr中(spsr只能使用mrs指令访问)
	mrs	lr, spsr
	# 把lr保存到sp加上偏移8的地址,保存的是spsr的值
	str	lr, [sp, #8]

	# 把cpsr复制到r0
	mrs	r0, cpsr
	# 将r0 的mode bit 修改为SVC_MODE
	eor	r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE)
	# 将r0 复制到spsr_cxsf
	msr	spsr_cxsf, r0

	# lr = lr & 0x0f:lr 保存的是spsr,即取低四bit,也就是lr 保存的是cpu mode
	and	lr, lr, #0x0f
	# 将sp 复制到r0
	mov	r0, sp
    # lr = *(pc + (lr << 2)),根据cpu mode更新lr 地址
    ldr	lr, [pc, lr, lsl #2]
    # 跳转到lr地址执行,并将spsr_cxsf 的值复制到cpsr,即切换到SVC Mode
	movs	pc, lr
ENDPROC(vector_\name)

2.1、vector_irq

这么看还是比较难理解,我们以vector_irq 展开后为例子进行分析,假设当前cpu 正在svc mode执行(kernel space),此时中断产生:

# 第一节中所述,当进入vector_irq时,lr保存的是返回地址,spsr保存的是被中断时的cpu cpsr,sp切换到SP_irq
vector_irq:
	# lr - 4 就是IRQ返回时要继续执行的指令
	sub	lr, lr, #4

	# 将r0,lr按地址递增保存到sp 为基地址的栈中
	stmia	sp, {r0, lr}
	# 把spsr复制到lr中(spsr只能使用mrs指令访问)
	mrs	lr, spsr
	# 把lr保存到sp加上偏移8的地址上,保存的是spsr的值,也就是被中断时的cpu cpsr
	str	lr, [sp, #8]

	# 把cpsr复制到r0
	mrs	r0, cpsr
	# 将r0 的mode bit 修改为SVC_MODE
	eor	r0, r0, #(IRQ_MODE ^ SVC_MODE | PSR_ISETSTATE)
	# 将r0 复制到spsr_cxsf
	msr	spsr_cxsf, r0

	# lr = lr & 0x0f:lr 保存的是spsr,即取低四bit,也就是lr保存的是cpu mode
	and	lr, lr, #0x0f
	# 将sp 复制到r0
	mov	r0, sp
    # lr = *(pc + (lr << 2)),pc 指针加上 lr << 2,也就是__irq_svc的地址,lr 就是__irq_invalid的值
    ldr	lr, [pc, lr, lsl #2]
    # 跳转到__irq_svc执行,并将spsr_cxsf 的值复制到cpsr,即切换到SVC Mode
	movs	pc, lr
ENDPROC(vector_irq)

	.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)

执行完成后,SP_irq栈空间如图所示,并且cpu切换到svc mode,去执行__irq_svc函数,r0作为参数保存了SP_irq的值。
在这里插入图片描述

到此,我们可以了解vector_stub 宏所实现的目的:保存被中断的r0,lr,cpsr到异常栈中,切到svc 模式去处理异常。

三、__irq_svc

好了,vector_stub的事告一段落,接下来就要处理与中断处理有关的逻辑了。linux 将IRQ中断处理分为两种情况:

  1. 当中断产生时,cpu 正在执行用户程序
  2. 当中断产生时,cpu 正在执行内核代码

无论哪种情况下,都需要保存被打断时刻的寄存器信息,但这两种情况所需要的处理有些不同,分别由 __irq_svc 和 __irq_user 来进行处理.

本节会优先分析__irq_svc,简单来说,这个函数先保存上下文,然后执行中断处理函数,在退出异常之前,检查是否需要进行抢占调度。具体代码注释如下:

__irq_svc:
	svc_entry          @ 保存寄存器到svc 的栈
	irq_handler        @ 执行中断处理函数

	ldr	r8, [tsk, #TI_PREEMPT]		@ 获取 preempt count
	ldr	r0, [tsk, #TI_FLAGS]		@ 获取 flags
	teq	r8, #0                       @ 将r8 与 0 进行异或操作,若结果为0则Z标志为1,否则Z标志为0
	movne	r0, #0				   @ 若Z为0,则将r0 强制设置为0,不允许抢占调度
	tst	r0, #_TIF_NEED_RESCHED      @ 将r0 与 _TIF_NEED_RESCHED进行与操作,若结果为0则Z标志为1,否则为0
	blne	svc_preempt            @ 若Z为0,则跳转到svc_preempt 执行

	svc_exit r5, irq = 1			@ 从异常退出,返回到被打断的执行流
ENDPROC(__irq_svc)

3.1、svc_entry

在中断产生时,我们希望保存被打断执行的程序的上下文,也就是所有的寄存器信息,对于arm 来说,就是r0->r15,以及CPSR,这样中断退出后,被打断的程序就能继续执行。

在 vector_stub 中,我们已经保存了R0,程序返回地址LR,程序状态SPSR。那么在svc_entry中,将继续保存剩下的所有寄存器!

svc_entry 将要保存的r0 -> r15 ,cpsr按照下图的排列顺序,保存到当前的内核栈中,kernel 的struct pt_regs 结构体表示内核栈中的寄存器信息:

// 一共有18个reg 的值,其排列如下:低地址为r0
struct pt_regs {
	unsigned long uregs[18];
};

在这里插入图片描述

接下来分析源码,以及最终栈内寄存器的局势图

	.macro	svc_entry

	sub	sp, sp, #(SVC_REGS_SIZE - 4)  @ 将sp减去17个reg 大小,预留出17个reg 的位置

	stmia	sp, {r1 - r12}       @ 将r1->r12 的13个值依次填入预留的位置

	# 接下来是计算一些地址
	ldmia	r0, {r3 - r5}        @ 取出IRQ Stack 中r0,lr,spsr 的值到r3、r4、r5,这些值也要保存到栈中
	add	r7, sp, #S_SP - 4	     @ R7 = SP + 13个reg的大小,R7的值就是栈内r13的地址
	mov	r6, #-1			        @  r6 赋值为-1
	add	r2, sp, #(SVC_REGS_SIZE - 4)   @ R2=SP+17个reg大小,R2的值就是一开始SP的值
	str	r3, [sp, #-4]!		    @ 将r3(也就是被打断的r0)保存到sp - 4的地址
	mov	r3, lr                  @ 将lr 保存到r3

	@ 经过上面的操作,目前的寄存器信息如下:
	@  r2 - sp_svc 进入svc_entry时,SP_SVC的值
	@  r3 - lr_svc 进入svc_entry时,LR_SVC的值
	@  r4 - 退出中断处理后要恢复执行的地址
	@  r5 - 退出中断处理后要恢复执行的cpu 状态
	@  r6 - -1 (see pt_regs definition in ptrace.h)
	@ 将上述寄存器都保存到栈内
	stmia	r7, {r2 - r6}

	@ 似乎是访问权限相关
	get_thread_info tsk                @ 获取当前task的thread_info,保存到tsk,(tsk就是r9)
	uaccess_entry tsk, r0, r1, r2, 1   @ 保存用户空间访问的相关信息到内核栈

	.endm

最终栈内的信息可以概况成下图:

在这里插入图片描述

3.2、svc_exit

与svc_entry 成对的是svc_exit,顾名思义,他的作用应该是恢复保存在栈中的寄存器,最终实现恢复被打断的执行流:

	#在irq_entry中是:svc_exit r5, irq = 1,r5保存的是要恢复的CPSR,irq=1表示中断关闭
	.macro	svc_exit, rpsr, irq = 0
	
	@ 确保中断已经关闭
	.if	\irq != 0
	.else
	disable_irq_notrace
	.endif
	
	@ 从栈恢复用户空间访问信息
	uaccess_exit tsk, r0, r1

	@ 将cpsr保存到spsr_cxsf
	msr	spsr_cxsf, \rpsr
	@ 将栈中的值恢复到r0 -> pc寄存器,^ 符号会将cpsr更新为spsr_cxsf,执行完这个指令后,cpu就会恢复之前的status,并恢复之前的执行流
	ldmia	sp, {r0 - pc}^			@ load r0 - pc, cpsr

	.endm

3.3、irq_handler && svc_preempt

在svc_entry 和 svc_exit 之间,由 irq_handler 负责完成中断具体逻辑的处理,从此会进入linux irq 管理的世界,与平台无关的。

若中断执行完成后,此时符合内核抢占,则会调用svc_prermpt 进行内核抢占调度,这就是下一话的内容了。

四、__irq_usr

当中断产生时,cpu正在执行用户程序,处于user mode时,vector_irq会进入到irq_usr 进行中断处理。这里的逻辑和 __irq_svc 很类似,都是保存寄存器后再进行中断处理,然后返回到被中断的地方继续执行:

__irq_usr:
	usr_entry                  @ 保存寄存器
	irq_handler                @ 中断处理
	get_thread_info tsk        @ 获取thread_info 到r9
	mov	why, #0                @ why 是r8
	b	ret_to_user_from_irq  @ 返回到用户
ENDPROC(__irq_usr)

4.1、usr_entry

usr_entry 和 svc_entry 的目的是一样的,就是将寄存器保存到当前的栈中,寄存器在栈中的排列也和struct pt_regs 一样。

	.macro	usr_entry, trace=1, uaccess=1

	sub	sp, sp, #PT_REGS_SIZE     @ 在栈中预留18个reg 的位置
    stmib	sp, {r1 - r12}        @ 保存r1 - r12 到栈内

	ldmia	r0, {r3 - r5}        @ 从中断栈中取出寄存器
	add	r0, sp, #S_PC			@ r0 指向栈中PC的位置
	mov	r6, #-1			        @ r6 = -1

	str	r3, [sp]		@ 保存之前的r0

	@ 保存剩余的其他寄存器
	@  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
	@
	stmia	r0, {r4 - r6}
    stmdb	r0, {sp, lr}^
	.endm

4.2、ret_to_user_from_irq

在返回用户程序之前,也会做一些检查,查看是否需要调度、处理信号等pending job,最后通过restore_user_regs 返回到用户程序

ENTRY(ret_to_user_from_irq)
	@ 对用户空间访问的一些检查
	ldr	r2, [tsk, #TI_ADDR_LIMIT]
	cmp	r2, #TASK_SIZE
	blne	addr_limit_check_failed
	@ 检查是否需要调度、是否有信号处理、返回用户空间之前是否需要调用callback函数,若需要则进入slow_work_pending处理
	ldr	r1, [tsk, #TI_FLAGS]
	tst	r1, #_TIF_WORK_MASK
	bne	slow_work_pending
no_work_pending:
	arch_ret_to_user r1, lr
	ct_user_enter save = 0

	restore_user_regs fast = 0, offset = 0
ENDPROC(ret_to_user_from_irq)

restore_user_regs 与svc_exit 的逻辑是像似的,即从栈中恢复寄存器,并跳转到被打断的地方继续执行。

	.macro	restore_user_regs, fast = 0, offset = 0
	uaccess_enable r1, isb=0

	mov	r2, sp
	ldr	r1, [r2, #\offset + S_PSR]	@ get calling cpsr r1保存了pt_regs中的spsr,也就是发生中断时的CPSR
	ldr	lr, [r2, #\offset + S_PC]!	@ get pc lr保存了PC值,同时sp移动到了pt_regs中PC的位置
	tst	r1, #PSR_I_BIT | 0x0f 
	bne	1f
	msr	spsr_cxsf, r1			@ save in spsr_svc 赋值给spsr,进行返回用户空间的准备

	ldmdb	r2, {r0 - lr}^			@ get calling r0 - lr

	add	sp, sp, #\offset + PT_REGS_SIZE  @ 将sp_svc恢复到进入中断时的位置
	movs	pc, lr				       @ 恢复用户程序执行,并更新cpsr
	.endm

参考书籍

附录

1、从异常返回时,返回地址的修正;

在这里插入图片描述

相关推荐

  1. Linux中断中断处理

    2024-03-18 11:42:02       33 阅读
  2. 处理器中断处理

    2024-03-18 11:42:02       32 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-03-18 11:42:02       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-03-18 11:42:02       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-03-18 11:42:02       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-03-18 11:42:02       20 阅读

热门阅读

  1. 【Python】Flask上下文管理

    2024-03-18 11:42:02       19 阅读
  2. sparksql的SQL风格编程

    2024-03-18 11:42:02       23 阅读
  3. ES6:可迭代对象(Iterable object)

    2024-03-18 11:42:02       19 阅读
  4. SQL server 构建索引的demo

    2024-03-18 11:42:02       17 阅读
  5. ES6 数组常用方法

    2024-03-18 11:42:02       20 阅读
  6. 【Vue2源码】响应式原理

    2024-03-18 11:42:02       23 阅读
  7. HBase常用命令

    2024-03-18 11:42:02       19 阅读
  8. 安装vscode及插件

    2024-03-18 11:42:02       21 阅读
  9. SpringBoot整合ElasticSearch应用

    2024-03-18 11:42:02       18 阅读
  10. CSS学习

    2024-03-18 11:42:02       17 阅读
  11. lua gc垃圾回收知识记录

    2024-03-18 11:42:02       20 阅读
  12. IOS面试题object-c 131-135

    2024-03-18 11:42:02       17 阅读
  13. 生成动态指定条件的拼接SQL

    2024-03-18 11:42:02       17 阅读