泰晓科技 -- 聚焦 Linux - 追本溯源,见微知著!
网站地址:https://tinylab.org

泰晓Linux知识星球:1300+知识点,520+用户
请稍侯

RISC-V 中断子系统分析——CPU 中断处理

Wu Zhangjin 创作于 2022/09/29

Corrector: TinyCorrect v0.1-rc1 - [spaces newline urls] Author: 通天塔 985400330@qq.com Date: 2022/07/12 Revisor: Falcon falcon@tinylab.org Project: RISC-V Linux 内核剖析 Sponsor: PLCT Lab, ISCAS

1 前言

之前两篇文章对中断的硬件实现以及硬件初始化,中断的申请、产生、处理进行了分析,并对 IRQ 的 domain 实现进行了深入分析,但之前的两篇文章都没对 CPU 寄存器、汇编的层面进行分析。

本篇文章将对 CPU 的中断处理流程进行深入分析。

对于中断的处理一直没想好从哪里下手,最后决定从 51 单片机下手,再扩展到 RISC-V 架构的 CPU。

本文代码分析基于 Linux-5.17。

2 51 单片机中断处理

51 单片机是我大学中学的一款单片机,也是我入门编程的一款单片机,今天一起回顾一下,单片机对于中断的处理流程。

STC89C51RC-RD.pdf (stcmcudata.com) 手册中讲了 51 单片机的中断处理流程:

当某中断产生而且被 CPU 响应,主程序被中断,接下来将执行如下操作:

  1. 当前正被执行的指令全部执行完毕;

  2. PC 值被压入栈;

  3. 现场保护;

  4. 阻止同级别其他中断;

  5. 将中断向量地址装载到程序计数器 PC;

  6. 执行相应的中断服务程序。

中断服务程序 ISR 完成和该中断相应的一些操作。ISR 以 RETI(中断返回)指令结束,将 PC 值从栈中取回,并恢复原来的中断设置,之后从主程序的断点处继续执行。 当某中断被响应时,被装载到程序计数器 PC 中的数值称为中断向量,是同该中断源相对应的中断服务程序的起始地址。

通过下一节的表,可以看到中断向量的地址,当物理上的中断产生之后,中断会对中断请求标志位进行置位,MCU 会对中断请求标志位进行响应,从而触发中断的处理流程。

2.1 单片机中断寄存器

下表表示了中断产生时的中断标志位,以及中断产生后,MCU 将要跳到的中断向量地址。下表参考 STC89C51 芯片手册给出。

中断源中断向量地址中断请求标志位
INT00003HIE0
Timer 0000BHTF0
INT10013HIE1
Timer 1001BHTF1
UART0023HRI+TI
Timer2002BHTF2+EXF2
INT20033HIE2
INT3003BHIE3

2.2 汇编代码

汇编代码在第一部分对中断向量表进行了设置,在规定的中断地址设置了跳转代码,使程序发生中断时跳转到对应的中断处理函数,当中断处理完成后,执行恢复指令 RETI。

  1. ;-----------------------------------------
  2. ;interrupt vector table
  3. ORG 0000H
  4. LJMP MAIN
  5. ORG 0003H ;INT0, interrupt 0 (location at 0003H)
  6. LJMP EXINT0
  7. ;-----------------------------------------
  8. ORG 0100H
  9. MAIN:
  10. MOV SP, #7FH ;initial SP
  11. SETB IT0 ;set INT0 interrupt type (1:Falling 0:Low level)
  12. SETB EX0 ;enable INT0 interrupt
  13. SETB EA ;open global interrupt switch
  14. SJMP$
  15. ;-----------------------------------------
  16. ;External interrupt0 service routine
  17. EXINT0:
  18. CPL P0.0
  19. RETI
  20. ;-----------------------------------------
  21. END

以上是一个 51 单片机中断响应后的处理流程。

3 RISC-V CPU 中断处理

3.1 CPU 中断寄存器

RISC-V-Reader-Chinese-v2p1 第十章讲了机器模式下的特权处理,可以拦截和处理异常,当产生异常时,寄存器就会被置位,通过寄存器的值,就可以进一步定位异常原因,并作出响应。文章中也给出了寄存器对应关系。

第 2 卷,特权规范与 20211203 这篇手册中就是被以上译文引用内容。

下表就是产生异常时寄存器的对应关系。

image-20220710155415029

通过上表可知,陷阱(trap)分为中断和异常两种,当产生中断时,Interrupt 寄存器置 1,异常时置 0。

中断共 6 种分别是:

  • 监管模式软中断
  • 机器模式软中断
  • 监管模式定时器中断
  • 机器模式定时器中断
  • 监管模式外部中断
  • 机器模式外部中断

3.2 特权模式下的中断处理

  1. 机器模式,权限最高,可以访问任意内存、寄存器

  2. 用户模式,无法处理中断,有内存隔离

  3. 监管者模式,既能处理中断,又有内存隔离,并且使用内存映射的方式实现了虚拟内存,可供现代操作系统使用。

3.2.1 机器模式(M)下中断处理

先分析机器模式下的中断处理流程。RISC-V-Reader-Chinese-v2p1 10.3 小节,对机器模式下的异常处理所需要的寄存器,以及异常处理流程进行了详细介绍,文章写得非常详细,通俗易懂,这里不再赘述。

以下引自 RISC-V-Reader-Chinese-v2p1

  • 异常指令的 PC 被保存在 mepc 中,PC 被设置为 mtvec。(对于同步异常,mepc 指向导致异常的指令;对于中断,它指向中断处理后应该恢复执行的位置。)

  • 根据异常来源设置 mcause(如图 10.3 所示),并将 mtval 设置为出错的地址或者其它适用于特定异常的信息字。
  • 把控制状态寄存器 mstatus 中的 MIE 位置零以禁用中断,并把先前的 MIE 值保留到 MPIE 中。
  • 发生异常之前的权限模式保留在 mstatus 的 MPP 域中,再把权限模式更改为 M。图 10.5 显示了 MPP 域的编码(如果处理器仅实现 M 模式,则有效地跳过这个步骤)。

这里针对文章中给出的一段时钟中断处理代码再分析。

  1. # save registers
  2. # 交换 a0 与 mscratch 的值,使 a0 保存临时内存空间地址指针
  3. csrrw a0, mscratch, a0 # save a0; set a0 = &temp storage
  4. # 保存其他整数寄存器的值到临时内存中
  5. sw a1,0(a0) # save a1
  6. sw a24(a0) # save a2
  7. sw a38(a0) # save a3
  8. sw a4, 12(a0) # save a4
  9. # decode interrupt cause
  10. # 从 mcause 寄存器中读取异常原因到寄存器 a1
  11. csrr a1,mcause # read exception cause
  12. # 如果不是 interrupt,跳转到 exception
  13. bgez a1,exception # branch if not an interrupt
  14. # 通过掩码确认中断原因
  15. andi a1a10x3f # isolate interrupt cause
  16. # 赋值a2
  17. li a2 7 # a2=timer interrupt cause
  18. # 比较跳转,不相等,则是其他中断
  19. bne a1a2otherInt # branch if not a timer interrupt
  20. # handle timer interrupt by incrementing time comparator
  21. # 赋值 a1
  22. la a1, mtimecmp #a1=&time comparator
  23. # 将时钟比较寄存器读取至 a2、a3
  24. lw a2, 0(a1) # load lower 32 bits of comparator
  25. lw a34(a1) # load upper 32 bits of comparator
  26. # 将 a2+1000,赋值给 a4,
  27. addi a4a21000 # increment lower bits by 1000 cycles
  28. # 比较 a4<a2?,a4 加了 1000,却小于 a2,说明寄存器产生了溢出,向 a3 进 1。
  29. sltu a2 a4, a2 # generate carry-out
  30. add a3 a3 a2 # increment upper bits
  31. # 将 a3、a4 写入定时器比较寄存器,目的是让定时器在 1000 个时钟周期后再中断
  32. sw a3, 4(a1) # store upper 32 bits
  33. sw a4,0(a1) # store lower 32 bits
  34. # restore registers and return
  35. # 恢复现场
  36. lw a412(a0) # restore a4
  37. lw a3, 4(a0) # restore a3
  38. lw a2 4(a0) # restore a2
  39. lw a1,0(a0) # restore a1
  40. csrrw a0, mscratch, a0)# restore a0; mscratch = &temp storage
  41. mret # return from handler

以上代码描述了一个 产生中断->保护现场->确认定时器中断->定时器定时增加 1000 周期->恢复现场 的流程。

通过以上流程,可以看到,当产生中断时,不像 51 单片机一样,直接跳转到了指定地址,而是通过代码逻辑,根据中断产生的原因跳转到指定地址处理中断。

两个中断处理流程的明显不同之处如下:

image-20220711001031881

3.2.2 监管者(S)模式下中断处理

监管者模式是现代操作系统的必须模式,既有中断处理的权限,又有自己的虚拟内存管理机制,能够满足现代操作系统的快速响应要求,也能运行内核的可信代码,保障底层寄存器的安全。

RISC-V 通过异常委托机制,可以使在 S 模式下产生的异常,委托给 S 模式进行处理,而不必切换到 M 模式进行处理,其中 S 模式也有自己中断处理时所用到的寄存器:sepc、stvec、scause、sscratch、stval 和 sstatus。

以下引自 RISC-V-Reader-Chinese-v2p1

  • 发生异常的指令的 PC 被存入 sepc,且 PC 被设置为 stvec。
  • scause 按图 10.3 根据异常类型设置,stval 被设置成出错的地址或者其它特定异常的信息字。
  • 把 sstatus CSR 中的 SIE 置零,屏蔽中断,且 SIE 之前的值被保存在 SPIE 中。
  • 发生例外时的权限模式被保存在 sstatus 的 SPP 域,然后设置当前模式为 S 模式

与上一小节对比可知,S 模式下的异常处理流程与 M 模式下的基本相同,只是使用的寄存器有所不同。

4 Linux 下的 CPU 中断处理

以上完成了 51 单片机和 RISC-V 的 CPU 中断处理流程在汇编级别的中断处理流程梳理。那么在 Linux 系统里,对于中断在汇编级别是如何处理的呢?

通过以上给的分析,可以看到,PC 指针的存储,PC 指针的跳转到 stvec 保存的指针位置,都是由芯片自己来完成的,PC 指针跳转到中断处理函数位置,才是后续程序来掌控的。

首先分析 stvec 的地址是什么时候存进去的,确认存的是什么值。

  1. /* arch/riscv/kernel/head.S: 194 */
  2. .align 2
  3. setup_trap_vector:
  4. /* Set trap vector to exception handler */
  5. la a0, handle_exception
  6. /* MTVEC 和 STVEC 根据内核配置进行宏定义为 TVEC */
  7. csrw CSR_TVEC, a0
  8. /*
  9. * Set sup0 scratch register to 0, indicating to exception vector that
  10. * we are presently executing in kernel.
  11. */
  12. csrw CSR_SCRATCH, zero
  13. ret

在分析 handle_exception 汇编代码之前,先要了解以下寄存器的含义。下图引自 Volume 1, Unprivileged Spec v. 20191213,下图将 32 个通用寄存器和 32 个浮点型寄存器的 API 名称和描述给了出来,方便通过以下描述,理解汇编代码。

image-20220712225920675

内核在初始化异常向量时,就指定了 handle_exception 作为产生异常时的跳转地址。

  1. /* arch/riscv/kernel/entry.S: 21 */
  2. ENTRY(handle_exception)
  3. /*
  4. * If coming from userspace, preserve the user thread pointer and load
  5. * the kernel thread pointer. If we came from the kernel, the scratch
  6. * register will contain 0, and we should continue on the current TP.
  7. * 如果来自用户空间,则使用 csrrw 指令,进行一次指针替换 */
  8. csrrw tp, CSR_SCRATCH, tp
  9. /* 比较 tp,跳转到保存上下文 */
  10. bnez tp, _save_context
  11. /* 另一种情况,重新装载 tp 和 sp */
  12. _restore_kernel_tpsp:
  13. csrr tp, CSR_SCRATCH
  14. REG_S sp, TASK_TI_KERNEL_SP(tp)
  15. #ifdef CONFIG_VMAP_STACK
  16. addi sp, sp, -(PT_SIZE_ON_STACK)
  17. srli sp, sp, THREAD_SHIFT
  18. andi sp, sp, 0x1
  19. bnez sp, handle_kernel_stack_overflow
  20. REG_L sp, TASK_TI_KERNEL_SP(tp)
  21. #endif
  22. /* 保存上下文 */
  23. _save_context:
  24. REG_S sp, TASK_TI_USER_SP(tp)
  25. REG_L sp, TASK_TI_KERNEL_SP(tp)
  26. addi sp, sp, -(PT_SIZE_ON_STACK)
  27. REG_S x1, PT_RA(sp)
  28. ...
  29. REG_S x31, PT_T6(sp)
  30. /*
  31. * Disable user-mode memory access as it should only be set in the
  32. * actual user copy routines.
  33. *
  34. * Disable the FPU to detect illegal usage of floating point in kernel
  35. * space.
  36. */
  37. li t0, SR_SUM | SR_FS
  38. /* 读取 CSR 中断相关寄存器 */
  39. REG_L s0, TASK_TI_USER_SP(tp)
  40. csrrc s1, CSR_STATUS, t0
  41. csrr s2, CSR_EPC
  42. csrr s3, CSR_TVAL
  43. csrr s4, CSR_CAUSE
  44. csrr s5, CSR_SCRATCH
  45. REG_S s0, PT_SP(sp)
  46. REG_S s1, PT_STATUS(sp)
  47. REG_S s2, PT_EPC(sp)
  48. REG_S s3, PT_BADADDR(sp)
  49. REG_S s4, PT_CAUSE(sp)
  50. REG_S s5, PT_TP(sp)
  51. /*
  52. * Set the scratch register to 0, so that if a recursive exception
  53. * occurs, the exception vector knows it came from the kernel
  54. * 对 CSR_SCRATCH 清零,如果下次出现异常,则知道异常来自内核
  55. */
  56. csrw CSR_SCRATCH, x0
  57. /* Load the global pointer 加载全局指针 */
  58. .option push
  59. .option norelax
  60. la gp, __global_pointer$
  61. .option pop
  62. #ifdef CONFIG_TRACE_IRQFLAGS
  63. call __trace_hardirqs_off
  64. #endif
  65. #ifdef CONFIG_CONTEXT_TRACKING
  66. /* If previous state is in user mode, call context_tracking_user_exit. */
  67. li a0, SR_PP
  68. and a0, s1, a0
  69. bnez a0, skip_context_tracking
  70. call context_tracking_user_exit
  71. skip_context_tracking:
  72. #endif
  73. /*
  74. * MSB of cause differentiates between
  75. * interrupts and exceptions
  76. * 根据异常原因进行中断和异常处理的跳转
  77. */
  78. bge s4, zero, 1f
  79. la ra, ret_from_exception
  80. /* Handle interrupts 处理中断 */
  81. move a0, sp /* pt_regs */
  82. la a1, generic_handle_arch_irq
  83. jr a1
  84. ...
  85. END(handle_exception)

整个汇编函数非常长,该函数实现了包括中断、异常、syscall、保护现场、恢复现场等操作,这里不再一点点分析。

5 小结

本文通过对比分析 51 单片机的中断处理流程,理清了 RISC-V 架构的中断处理流程,并且在 Linux 内核代码中找到了相应的寄存器配置,而且进一步对中断处理函数的汇编部分进行了初步分析。

这一系列的 3 篇文章讲清楚了,在 RISC-V 架构下 Linux 如何从汇编到最终驱动处理中断的全流程。

至此已经分析完毕 RISC-V 的中断处理流程,后续会继续分析中断的其他部分。

6 参考资料



Read Album:

Read Related:

Read Latest: