目录
1. 前言
本专题我们开始学习进程管理部分。本文主要参考了《奔跑吧, Linux内核》、ULA、ULK的相关内容。
本专题记录ARM架构下中断是如何管理的,Linux内核中的中断管理机制是如何设计与实现的,以及常用的下半部机制,如软中断、tasklet、workqueue等。本文及后续中断相关笔记均以qemu 5.0.0内嵌平台为例,中断控制器采用GIC-400控制器,支持GIC version2技术规范。本文开始介绍ARM64的中断处理过程,先从底层硬件中断的处理过程开始。
kernel版本:5.10
平台:arm64
注:
为方便阅读,正文标题采用分级结构标识,每一级用一个"-“表示,如:两级为”|- -", 三级为”|- - -“
2. arm64中断基础
Exception levels
The Armv8-A architecture defines a set of Exception levels, EL0 to EL3, where:
- If ELn is the Exception level, increased values of n indicate increased software execution privilege.
- Execution at EL0 is called unprivileged execution.
- EL2 provides support for virtualization.
- EL3 provides support for switching between two Security states, Secure state and Non-secure state
arm64异常分类
- Synchronous exception.
- IRQ.
- FIQ
- SError.
注:其中同步异常又需要根据ESR_ELx查看具体是何种同步异常
arm64中断处理相关寄存器
- The general purpose registers, R0-R30:通用寄存器在处理基本指令集的指令时使用。它包括31个通用寄存器,R0-R30。
这些寄存器可以作为31个64位寄存器X0-X30或31个32位寄存器W0-W30访问。 - The stack pointer registers:栈指针寄存器,每个exception level一个,SP_EL0,SP_EL1必须实现,SP_EL2,SP_EL3根据EL2,EL3是否实现来决定;
- The SIMD and floating-point registers, V0-V31:SIMD and floating-point register bank comprises 32 quadword (128-bit) registers, V0-V31;
- Saved Program Status Registers (SPSRs):每个exception level一个(SPSR_ELx, x=1,2,3),用于保存发生异常时的exception level的PE状态;
- Exception Link Registers (ELRs):异常链接寄存器,保存了异常返回地址,每个exception level一个(ELR_ELx,x=1,2,3);
- Exception Syndrome Register (ESR):保存了异常的原因,每个exception level一个(ESR_ELx, x=1,2,3)
- Vector Base Address Register (VBAR):exception level的向量表基址,每个exception level一个(VBAR_ELx,x=1,2,3);
中断发生时硬件处理过程
- 保存PSTATE 数据到SPSR_ELx,(x = 1,2,3),在返回异常现场的时候,可以使用SPSR_ELx来恢复PE的状态
- PSTATE寄存器里的DAIF域设置为1,相当于提哦是异常,系统错误,IRQ以及FIQ都关闭(这防止了中断嵌套);
- 保存异常进入地址到ELR_ELx,同步异常(und/abt等)是当前地址,而异步异常(irq/fiq等)是下一条指令地址,在返回异常现场的时候,可以使用ELR_ELx来恢复PC值
- 保存异常原因信息到ESR_ELx, ESR_ELx.EC代表Exception Class;
- PE根据目标EL的异常向量表中定义的异常向量地址强制跳转到异常处理程序,跳转到哪个EL使用哪个向量偏移地址又路由关系决定;
- 堆栈指针SP的使用由目标EL决定,(SPSR_ELx.M[0] == 1) ? h(ELx): t(EL0);
注:用户态(EL0)不能处理异常,当异常发生在用户态时,异常级别(EL)会发生切换,默认切换到EL1(内核态),所以大部分的异常都被路由到EL1来处理;
路由规则
在不同的exception level, excution mode下, 发生exception时会被路由到不同的exception level,路由的规则是只会向更高的exception level路由,不能反向。相关规则可参考ARM文档
异常向量表
- 每个exception level都有自己的异常向量表,异常向量表保存了arm64发生各种异常时的不同向量,分别对应不同的exception handler,每个exception handler的大小可以为0x80(128)字节;
- arm64有三个异常向量表基址寄存器VBAR_ELX,分别是VBAR_EL1,VBAR_EL2,VBAR_EL3,它们分别记录了三种不同exception level的向量表基址,通过VBAR_ELx可以找到对应exception level的异常向量表。通过vector base address + offset得到具体的exception handler;
- 前面我们说过根据路由规则会使得从一种excepiton level路由到target exception level,VBAR就是记录了target exception level的异常向量表基址,举例来讲,EL0下发生异常路由到EL1,则会跳转到VBAR_EL1指示的异常向量表,进一步根据异常类型计算VBAR_EL1 + offset执行对应的exception handler.
- 如上表,根据发生异常时的exception level、使用的sp、路由的target exception level,可以将异常类型分为四类:
(1)异常发生在当前级别且使用SP_EL0(EL0级别对应的堆栈指针),即发生异常时不发生异常级别切换,可以简单理解为异常发生在内核态(EL1),且使用EL0级别对应的SP。 这种情况在Linux内核中未进行实质处理,直接进入bad_mode()流程。
(2)异常发生在当前级别且使用SP_ELx(ELx级别对应的堆栈指针,x可能为1、2、3),即发生异常时不发生异常级别切换,可以简单理解为异常发生在内核态(EL1),且使用EL1级别对应的SP。 这是比较常见的场景。
(3)异常发生在更低级别且在异常处理时使用AArch64模式,发生异常时发生异常级别切换。可以简单理解为异常发生在用户态,且进入内核处理异常时,使用的是AArch64执行模式(非AArch32模式)。 这也是比较常见的场景。
(4)异常发生在更低级别且在异常处理时使用AArch32模式,发生异常时发生异常级别切换。可以简单理解为异常发生在用户态,且进入内核处理异常时,使用的是AArch32执行模式(非AArch64模式)。 这种场景基本未做处理。
注:current exception level表示系统中当前最高等级的exception level。假设当前系统只运行Linux内核,不包含虚拟化和安全特性,则current exception level为EL1,“the implemented level immediately lower than the target level”中target level如果为EL1,则the implemented level immediately lower为EL0
3. arm64异常向量表
/*
* Exception vectors.
*/
.pushsection ".entry.text", "ax"
.align 11
SYM_CODE_START(vectors)
kernel_ventry 1, sync_invalid // Synchronous EL1t
kernel_ventry 1, irq_invalid // IRQ EL1t
kernel_ventry 1, fiq_invalid // FIQ EL1t
kernel_ventry 1, error_invalid // Error EL1t
kernel_ventry 1, sync // Synchronous EL1h
kernel_ventry 1, irq // IRQ EL1h
kernel_ventry 1, fiq_invalid // FIQ EL1h
kernel_ventry 1, error // Error EL1h
kernel_ventry 0, sync // Synchronous 64-bit EL0
kernel_ventry 0, irq // IRQ 64-bit EL0
kernel_ventry 0, fiq_invalid // FIQ 64-bit EL0
kernel_ventry 0, error // Error 64-bit EL0
#ifdef CONFIG_COMPAT
kernel_ventry 0, sync_compat, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_compat, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid_compat, 32 // FIQ 32-bit EL0
kernel_ventry 0, error_compat, 32 // Error 32-bit EL0
#else
kernel_ventry 0, sync_invalid, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_invalid, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid, 32 // FIQ 32-bit EL0
kernel_ventry 0, error_invalid, 32 // Error 32-bit EL0
#endif
SYM_CODE_END(vectors)
从如上代码以及链接脚本可以看出异常向量表位于vmlinux的.entry.text段,kernel_ventry 的宏定义简化如下:
/*参数el表示异常级别0/1/2/3; label表示异常类型如:irq/firq等;*/
.macro kernel_ventry, el, label, regsize = 64
/*按2 ^ 7对齐*/
.align 7
/*S_FRAME_SIZE为sizeof(struct pt_regs)大小,用于保存中断现场*/
sub sp, sp, #S_FRAME_SIZE
/*\()表示结束字符,如:el1_irq*/
b el\()\el\()_\label
.endm
此时,如果是EL1发生的中断异常,sp已经由硬件切换到SP_EL1,它指向被中断进程的内核栈,将SP减去S_FRAME_SIZE大小,用于保存进程的CPU上下文,由此可见,被中断进程的上下文是保存在进程自身的内核栈中的。如上对于向量表中以invalid结尾的异常会跳转到inv_entry宏定义,如el0_sync_invalid会执行inv_entry 0, BAD_SYNC,其中inv_entry宏定义如下,最终都会跳转到bad_mode进行处理
/*
* Invalid mode handlers
*/
.macro inv_entry, el, reason, regsize = 64
kernel_entry \el, \regsize
mov x0, sp
mov x1, #\reason
mrs x2, esr_el1
bl bad_mode
ASM_BUG()
.endm
下面将以EL1下的按键中断irq异常为例,说明中断的底层处理流程
4. 内核空间中断异常底层处理流程
SYM_CODE_START_LOCAL_NOALIGN(el1_irq)
/*保存中断上下文*/
kernel_entry 1
gic_prio_irq_setup pmr=x20, tmp=x1
/*开启PSTATE的A/D/S(SError中断/调试异常/FIQ),保持irq关闭,防止中断嵌套*/
enable_da_f
mov x0, sp
bl enter_el1_irq_or_nmi
irq_handler
#ifdef CONFIG_PREEMPTION
ldr x24, [tsk, #TSK_TI_PREEMPT] // get preempt count
alternative_if ARM64_HAS_IRQ_PRIO_MASKING
/*
* DA_F were cleared at start of handling. If anything is set in DAIF,
* we come back from an NMI, so skip preemption
*/
mrs x0, daif
orr x24, x24, x0
alternative_else_nop_endif
/*如果preempt count非0则跳转到1标号,即不能抢占*/
cbnz x24, 1f // preempt count != 0 || NMI return path
bl arm64_preempt_schedule_irq // irq en/disable is done inside
1:
#endif
mov x0, sp
bl exit_el1_irq_or_nmi
/*恢复中断上下文*/
kernel_exit 1
SYM_CODE_END(el1_irq)
以按键中断为例,当发生按键中断,gic中断控制器会将中断路由给arm64 cpu,arm64 cpu会经过一系列硬件自动处理流程(见中断发生时硬件处理过程一节),然后转向target exception level的VBAR_ELx(此处假设为VBAR_EL1),通过VBAR_Elx查询到向量表基地址,通过ESR_ELx可以获取异常原因,得到offset,这样就可以获取到exception handler的地址,对于EL1下发生的中断异常来讲,exception handler就是el1_irq,就会跳转到el1_irq执行。此处由于是内核空间发生的异常,中断返回时会检查是否允许抢占,如果允许抢占则跳转到arm64_preempt_schedule_irq执行一次抢占调度。
注:
如果在内核空间发生的中断异常,中断返回内核空间前会检查是否允许抢占,如果允许抢占,然后再检查是否可以抢占被中断的进程,如果可以抢占被中断的进程则执行抢占;
如果在用户空间发生的中断异常,则返回用户空间前只会检查是否能抢占被中断的进程,不会检查是否允许抢占,因为用户态不会被禁用抢占。
|- -kernel_entry保存中断上下文
kernel_entry宏参数为1表示保存发生在EL1的异常现场;若为0表示保存发生在EL0的异常现场。通过前面对kernel_ventry宏的分析可知,被中断进程的现场上下文会被保存在被中断进程内核栈,这个中断异常的现场主要包括:栈帧、PSTATE、PC、SP以及通用寄存器X-~X29。
|- -irq_handler
/*
* Interrupt handling.
*/
.macro irq_handler
ldr_l x1, handle_arch_irq
mov x0, sp
irq_stack_entry
blr x1
irq_stack_exit
.endm
-
handle_arch_irq:在中断管理基础学习笔记 - 2.中断控制器初始化一节的gic_of_init->set_handle_irq中介绍过,会将handle_arch_irq初始化为gic_handle_irq,这个就作为中断的顶层处理函数,下一节会详细说明;
-
irq_stack_entry:主要目的是切换进程内核栈为irq栈,irq栈在init_IRQ->init_irq_stacks时初始化,每个cpu一个,同时它也会将进程内核栈指针保存到x19,便于恢复;
-
irq_stack_exit:进程内核栈保存在x19 ,这里从irq栈切换为进程的内核栈,前面kernel_entry知道,进程的上下文是保存到进程自身的内核栈,此处恢复了内核栈SP,因此就可以恢复进程上下文了
|- -kernel_exit恢复中断上下文
kernel_exit首先通过进程内核栈中保存的寄存器恢复了进程上下文,最后通过eret来使用ELR_ELx和SPSR_ELx来恢复PC和PSTATE,我们知道中断时会由硬件自动关闭PSTATE的irq,此处将关闭IRQ之前的SPSR_ELx恢复到PSTATE,相当于重新打开了中断IRQ
参考文档
- 奔跑吧,Linux内核
- ARMv8-异常处理
- Arm® Architecture Reference Manual