中断管理基础学习笔记 - 5.1 ARM64底层中断处理

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:

  1. If ELn is the Exception level, increased values of n indicate increased software execution privilege.
  2. Execution at EL0 is called unprivileged execution.
  3. EL2 provides support for virtualization.
  4. EL3 provides support for switching between two Security states, Secure state and Non-secure state

arm64异常分类

  1. Synchronous exception.
  2. IRQ.
  3. FIQ
  4. SError.
    注:其中同步异常又需要根据ESR_ELx查看具体是何种同步异常

arm64中断处理相关寄存器

  1. The general purpose registers, R0-R30:通用寄存器在处理基本指令集的指令时使用。它包括31个通用寄存器,R0-R30。
    这些寄存器可以作为31个64位寄存器X0-X30或31个32位寄存器W0-W30访问。
  2. The stack pointer registers:栈指针寄存器,每个exception level一个,SP_EL0,SP_EL1必须实现,SP_EL2,SP_EL3根据EL2,EL3是否实现来决定;
  3. The SIMD and floating-point registers, V0-V31:SIMD and floating-point register bank comprises 32 quadword (128-bit) registers, V0-V31;
  4. Saved Program Status Registers (SPSRs):每个exception level一个(SPSR_ELx, x=1,2,3),用于保存发生异常时的exception level的PE状态;
  5. Exception Link Registers (ELRs):异常链接寄存器,保存了异常返回地址,每个exception level一个(ELR_ELx,x=1,2,3);
  6. Exception Syndrome Register (ESR):保存了异常的原因,每个exception level一个(ESR_ELx, x=1,2,3)
  7. Vector Base Address Register (VBAR):exception level的向量表基址,每个exception level一个(VBAR_ELx,x=1,2,3);

中断发生时硬件处理过程

  1. 保存PSTATE 数据到SPSR_ELx,(x = 1,2,3),在返回异常现场的时候,可以使用SPSR_ELx来恢复PE的状态
  2. PSTATE寄存器里的DAIF域设置为1,相当于提哦是异常,系统错误,IRQ以及FIQ都关闭(这防止了中断嵌套);
  3. 保存异常进入地址到ELR_ELx,同步异常(und/abt等)是当前地址,而异步异常(irq/fiq等)是下一条指令地址,在返回异常现场的时候,可以使用ELR_ELx来恢复PC值
  4. 保存异常原因信息到ESR_ELx, ESR_ELx.EC代表Exception Class;
  5. PE根据目标EL的异常向量表中定义的异常向量地址强制跳转到异常处理程序,跳转到哪个EL使用哪个向量偏移地址又路由关系决定;
  6. 堆栈指针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文档

异常向量表

在这里插入图片描述

  1. 每个exception level都有自己的异常向量表,异常向量表保存了arm64发生各种异常时的不同向量,分别对应不同的exception handler,每个exception handler的大小可以为0x80(128)字节;
  2. arm64有三个异常向量表基址寄存器VBAR_ELX,分别是VBAR_EL1,VBAR_EL2,VBAR_EL3,它们分别记录了三种不同exception level的向量表基址,通过VBAR_ELx可以找到对应exception level的异常向量表。通过vector base address + offset得到具体的exception handler;
  3. 前面我们说过根据路由规则会使得从一种excepiton level路由到target exception level,VBAR就是记录了target exception level的异常向量表基址,举例来讲,EL0下发生异常路由到EL1,则会跳转到VBAR_EL1指示的异常向量表,进一步根据异常类型计算VBAR_EL1 + offset执行对应的exception handler.
  4. 如上表,根据发生异常时的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
  1. handle_arch_irq:在中断管理基础学习笔记 - 2.中断控制器初始化一节的gic_of_init->set_handle_irq中介绍过,会将handle_arch_irq初始化为gic_handle_irq,这个就作为中断的顶层处理函数,下一节会详细说明;

  2. irq_stack_entry:主要目的是切换进程内核栈为irq栈,irq栈在init_IRQ->init_irq_stacks时初始化,每个cpu一个,同时它也会将进程内核栈指针保存到x19,便于恢复;

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

参考文档

  1. 奔跑吧,Linux内核
  2. ARMv8-异常处理
  3. Arm® Architecture Reference Manual
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值