RISC-V 学习篇之特权架构下的中断异常处理

  • 控制流和Trap
  • 特权架构
  • 简单的嵌入式系统的机器模式
    • 机器模式下的异常处理
      • mtvec(Machine Trap-Vector Base-Address)
      • mepc(Machine Exception Program Counter)
      • mcause(Machine Cause)
      • mtval(Machine Trap Value )
      • mstatus(Machine Status)
        • 有关如何确定当前特别级别的问题
      • mie(Machine Interrupt Enable)
      • mip(Machine Interrupt Pending)
      • mscratch(Machine Scratch)
      • Trap处理流程
        • 注意
        • 图解Trap流程
  • 嵌入式系统中的用户模式和进程隔离
  • 现代操作系统的监管者模式
  • 基于页面的虚拟内存
    • 页表基址寄存器
    • TLB
    • 完整虚拟地址翻译过程
  • 小结

本系列参考: 学习开发一个RISC-V上的操作系统 – 汪辰 – 2021春 整理而来,主要作为xv6操作系统学习的一个前置基础。

课程代码和环境搭建教程参考github仓库: https://github.com/plctlab/riscv-operating-system-mooc/blob/main/howto-run-with-ubuntu1804_zh.md

前置知识:

  • RVOS环境搭建-01
  • RVOS操作系统内存管理简单实现-02
  • RVOS操作系统协作式多任务切换实现-03

本文主要参考: RISC-V手册整理而来。


控制流和Trap


特权架构

RISC-V支持的特权级模式组合如下所示:

  • 最简单嵌入式系统只支持Machine模式
  • 安全的嵌入式系统支持Machine模式和User模式
  • 支持虚拟内存概念的类Unix系统,需要支持Machine,User和Supervisor三种模式

高权限模式通常可以使用权限较低的模式的所用功能,并且它们还有一些低权限模式下不可用的额外功能,例如:

  • 处理中断和执行 I/O 的功能
  • 处理器通常大部分时间都运行在权限最低的模式下
  • 处理中断和异常时会将控制权移交到更高权限的模式

RISC-V 提供的特权指令有如下几个:

  • sret : 监管者模式下的异常和中断返回
  • mret : 机器模式下的异常和中断返回
  • sfence.vma : 刷新虚拟内存映射(tlb)
  • wfi :使处理器暂停执行,并进入低功耗的等待状态,使处理器暂停执行,并进入低功耗的等待状态

特权架构添加了很少的指令,作为替代,几个新的控制状态寄存器CSR显示了附加的功能。


简单的嵌入式系统的机器模式

机器模式(缩写为 M 模式,M-mode)是 RISC-V 中 hart(hardware thread,硬件线程)可以执行的最高权限模式。

在 M 模式下运行的 hart 对内存,I/O 和一些对于启动和配置系统来说必要的底层功能有着完全的使用权。因此它是唯一所有标准 RISC-V 处理器都必须实现的权限模式。实际上简单的 RISC-V 微控制器仅支持 M 模式。

机器模式最重要的特性是拦截和处理异常(不寻常的运行时事件)的能力

RISC-V 将异常分为两类:

  • 一类是同步异常,这类异常在指令执行期间产生,如访问了无效的存储器地址或执行了具有无效操作码的指令时。
  • 另一类是中断,它是与指令流异步的外部事件,比如鼠标的单击。

在 M 模式运行期间可能发生的同步例外有五种:

  • 访问错误异常: 当物理内存的地址不支持访问类型时发生(例如: 尝试写入 ROM)。
  • 断点异常: 在执行 ebreak 指令,或者地址或数据与调试触发器匹配时发生。
  • 环境调用异常: 在执行 ecall 指令时发生。
  • 非法指令异常: 在译码阶段发现无效操作码时发生。
  • 非对齐地址异常: 在有效地址不能被访问大小整除时发生。

中断时 mcause 的最高有效位置 1,同步异常时置 0,且低有效位标识了中断或异常的具体原因。只有在实现了监管者模式时才能处理监管者模式中断和页面错误异常。

有三种标准的中断源:软件、时钟和外部来源。

  • 软件中断通过向内存映射寄存器中存数来触发,并通常用于由一个 hart 中断另一个 hart(在其他架构中称为处理器间中断机制)。
  • 当 hart 的时间比较器(一个名为 mtimecmp 的内存映射寄存器)大于实时计数器mtime 时,会触发时钟中断。
  • 外部中断由平台级中断控制器(大多数外部设备连接到这个中断控制器)引发。

不同的硬件平台具有不同的内存映射并且需要中断控制器的不同特性,因此用于发出和消除这些中断的机制因平台而异。所有 RISC-V 系统的共同问题是如何处理异常和屏蔽中断,这是下一节的主题。


机器模式下的异常处理

八个控制状态寄存器(CSR)是机器模式下异常处理的必要部分:

  • mtvec(Machine Trap Vector)它保存发生异常时处理器需要跳转到的地址。
  • mepc(Machine Exception PC)它指向发生异常的指令。
  • mcause(Machine Exception Cause)它指示发生异常的种类。
  • mie(Machine Interrupt Enable)它指出处理器目前能处理和必须忽略的中断。
  • mip(Machine Interrupt Pending)它列出目前正准备处理的中断。
  • mtval(Machine Trap Value)它保存了陷入(trap)的附加信息:访问地址出错时的地址信息、或者执行非法指令时的指令本身,对于其他异常,它的值为 0。
  • mscratch(Machine Scratch) 暂存一个字大小的数据,例如: 使用该寄存器保存当前hart上运行的task上下文(context)地址。
  • mstatus(Machine Status)它保存全局中断使能,以及许多其他的状态,如: 关闭和打开全局中断。

下图展示了上面八个状态控制寄存器相互配合工作的场景:

上图描述的是单个Hart内部的中断流程

下面我们来详细解释每个状态寄存器的作用。


mtvec(Machine Trap-Vector Base-Address)

mtvec指向trap处理函数的入口地址:

注意: 在MODE=Vectored向量的模式下,vector table 存放的 interrupt handler 的地址,在 interrupt 发生时先进行一次间接寻址得到对应的 interrupt handler 的地址,再将其设定为 pc 的值,这是不正确的。

  • 正确的解释是,在 vectored mode 下,中断发生时直接设置 pc = BASE + 4*cause。中断向量表中的并非是 interrupt handler 的地址,而是若干 j 指令,j 指令负责跳转到对应的 interrupt handler 然后开始执行。这一点 riscv 可能和其他 ISA 的中断向量表不一样。
  • https://starfivetech.com/uploads/sifive-interrupt-cookbook-v1p2.pdf 这一个链接中的 P13/P14 给出了一个很直观的例子,riscv CLINT 模式的中断向量表存放的是一系列的 j 指令。

mepc(Machine Exception Program Counter)

当发生trap时,会跳转到异常控制流处执行,而mepc负责保存异常控制流函数的返回地址:

  • 如果发生的是异常,那么mepc指向的是发生异常的指令地址
  • 如果发生的是中断,那么mepc指向的是发生中断的指令的下一条指令地址

mcause(Machine Cause)



mtval(Machine Trap Value )


一般情况下,mtval 寄存器的用途如下:

  • 异常原因:对于异常,mtval 寄存器存储导致异常的具体原因或异常代码。
    • 对于页故障(Page Fault)异常,mtval 可以存储导致页故障的虚拟地址;
    • 对于未对齐访问(Misaligned Access)异常,mtval 可以存储引发异常的内存访问地址。
  • 中断原因:对于中断,mtval 寄存器可能存储与中断相关的具体信息。
    • 对于外部中断,mtval 可以存储中断源的标识符或其他相关数据。
  • 自定义用途:在某些情况下,mtval 寄存器可以由操作系统或处理器实现者自定义,并用于特定目的。这取决于具体的 RISC-V 实现和特权级别。

mstatus(Machine Status)


mstatus是RISC-V架构中的一个机器级别(M-Mode)的CSR寄存器,用于管理和控制处理器的状态。

mstatus寄存器是一个32位的寄存器,包含了多个位字段,每个位字段都有特定的含义和功能。以下是mstatus寄存器中一些常见的位字段:

  1. MIE(Machine Interrupt Enable):该位控制是否允许处理器接受中断。当MIE为1时,处理器可以响应中断请求;当MIE为0时,处理器将禁止中断。

  2. MPIE(Machine Previous Interrupt Enable):该位记录上一级特权级别(S-Mode或U-Mode)的中断使能状态。在特权级别切换时,这个位的值会被保存和恢复。

  3. MPP(Machine Previous Privilege):该位字段指示上一级特权级别,用于保存处理器从异常或中断返回时的特权级别。可能的值包括M-Mode(3’b11)、S-Mode(3’b01)和 U-Mode(3’b00)。

  4. MIE(Machine Exception Enable):该位用于控制是否允许处理器接受异常。当MIE为1时,处理器可以响应异常;当MIE为0时,处理器将禁止异常。

mstatus寄存器还包含其他位字段,用于管理处理器的特权级别、虚拟化、调试模式等。具体的位字段定义和功能可以在RISC-V的官方规范中找到。

通过读取和写入mstatus寄存器,可以控制和监控处理器的状态,例如启用或禁用中断、设置特权级别、处理异常等。这对于操作系统、异常处理程序和其他特权级别的软件非常重要。

  • 如果大家仔细思考会发现,mstatus寄存器中并没有提供两位来存储当前特别级别,这是因为RISC-V出于安全考虑,并没有向用户暴露可以访问当前特别级别的寄存器,但是硬件层面确实需要做出相关支持。
  • 如果想要知道当前所处特别级别,可以试着触发一个权级的指令来测试你所在的权级。
  • 如果当前使用QEMU运行RV系统,还可以:
  • 如果不对QEMU进行设置,系统默认以M模式进行启动,具备最高的特权级别,可以访问和控制所有的资源和寄存器。

注意:

  • trap=interrupt+exception , 我们可以利用mstatus寄存器的MIE位来控制全局中断是否打开,但是注意异常是不能屏蔽的
  • 当我们设置MIE位为0的时候,是关闭了全局中断,那么当中断发生时,hart会忽略本次中断
  • 而对于异常,由于其不能被屏蔽,所以只要发生exception,hart就会跳到mtevc寄存器的指向的trap_handler,进入我们的异常处理流程

有关如何确定当前特别级别的问题

首先明确一点:

  • We have mstatus.mPP. that holdS the previous privilege mode. Current privilege mode is not visible to software

原因:

  • How to determine the current execution privilege mode
  • How the RISC-V HW can determine the privilege level” />mie(Machine Interrupt Enable)

    mie是RISC-V架构中的一个机器级别(M-Mode)的CSR寄存器,用于控制和管理处理器的中断使能状态。

    mie寄存器是一个32位的寄存器,每个位对应一个特定类型的中断。以下是mie寄存器中一些常见的位字段:

    1. MSIE(Machine Software Interrupt Enable):该位控制是否允许处理器接受机器级别软件中断(MSI)。当MSIE为1时,处理器可以接受并响应MSI;当MSIE为0时,处理器将禁止MSI。

    2. MTIE(Machine Timer Interrupt Enable):该位控制是否允许处理器接受机器级别定时器中断。当MTIE为1时,处理器可以接受并响应定时器中断;当MTIE为0时,处理器将禁止定时器中断。

    3. MEIE(Machine External Interrupt Enable):该位控制是否允许处理器接受机器级别外部中断。当MEIE为1时,处理器可以接受并响应外部中断;当MEIE为0时,处理器将禁止外部中断。

    通过读取和写入mie寄存器,可以控制处理器接受和屏蔽不同类型的中断。中断发生时,处理器会根据mie寄存器中相应的位来决定是否触发中断处理程序。

    需要注意的是,mie寄存器的设置可能会受到其他控制寄存器(如mstatus寄存器)中相关位字段的影响。例如,如果mstatus寄存器的MIE位为0,即使mie寄存器中某个中断使能位为1,该中断也不会触发。

    中断处理和中断控制是处理器中重要的功能,它们可以用于处理异步事件、外部设备的输入、定时器等。mie寄存器在这个过程中起到了关键的作用,它允许软件控制中断的使能和屏蔽,以适应特定的需求和应用场景。


    mip(Machine Interrupt Pending)

    mip是RISC-V架构中的一个机器级别(M-Mode)的CSR寄存器,用于表示和管理处理器的中断请求状态。

    mip寄存器是一个32位的寄存器,每个位对应一个特定类型的中断请求。以下是mip寄存器中一些常见的位字段:

    1. MSIP(Machine Software Interrupt Pending):该位表示机器级别软件中断(MSI)的中断请求状态。当MSIP为1时,表示有一个MSI中断请求待处理;当MSIP为0时,表示没有MSI中断请求。

    2. MTIP(Machine Timer Interrupt Pending):该位表示机器级别定时器中断的中断请求状态。当MTIP为1时,表示有一个定时器中断请求待处理;当MTIP为0时,表示没有定时器中断请求。

    3. MEIP(Machine External Interrupt Pending):该位表示机器级别外部中断的中断请求状态。当MEIP为1时,表示有一个外部中断请求待处理;当MEIP为0时,表示没有外部中断请求。

    通过读取mip寄存器,可以查询处理器当前的中断请求状态,以了解是否有中断待处理。在处理器响应中断时,处理器会根据mip寄存器中相应的位来确定中断的来源。

    另外,可以通过写入mip寄存器的特定位,手动触发或清除中断请求,以模拟中断的发生或结束。这在某些调试或测试场景下可能很有用。

    需要注意的是,mip寄存器的状态可能会受到其他控制寄存器(如mie寄存器)中相关位字段的影响。例如,如果mie寄存器的相应位为0,即使mip寄存器中某个中断请求位为1,该中断也不会被触发。

    中断请求和处理是处理器中重要的功能,它们用于异步事件的处理、外部设备的输入、定时器的触发等。mip寄存器提供了一种机制,使软件能够检查和处理中断请求,以响应相关的事件和中断源。


    mscratch(Machine Scratch)

    mscratch 是 RISC-V 架构中的一个控制和状态寄存器(Control and Status Register),用于保存机器模式下的临时数据或上下文相关的信息。它的作用是提供一个通用的、临时的存储位置,供软件使用。

    具体而言,mscratch 寄存器通常用于以下情况:

    1. 上下文切换:当处理器从一个上下文切换到另一个上下文时,可以将当前的 mscratch 寄存器的值保存到保存的上下文中。在切换到新的上下文后,可以将先前保存的 mscratch 寄存器的值恢复,以便继续使用其中的数据。

    2. 异步事件处理:当处理器在处理中断或异常时,可能需要保存一些临时数据,以便在恢复正常执行后继续使用。mscratch 寄存器提供了一个方便的位置来存储这些临时数据,以避免污染其他重要的寄存器。

    3. 调试和跟踪:在调试和跟踪应用程序时,mscratch 寄存器可以用于存储调试器或跟踪工具的临时数据,例如断点信息、调试状态等。

    需要注意的是,mscratch 寄存器的使用是由软件决定的,它没有特定的预定义用途。软件可以根据需要将 mscratch 寄存器用于临时存储和处理数据。然而,由于 mscratch 寄存器的值可能会被上下文切换或其他操作修改,因此软件在使用 mscratch 寄存器时应注意保存和恢复其中的数据。

    总结:mscratch 寄存器是 RISC-V 架构中的一个控制和状态寄存器,用于保存机器模式下的临时数据或上下文相关的信息。它可以用于上下文切换、异步事件处理、调试和跟踪等情况,提供一个通用的临时存储位置供软件使用。


    Trap处理流程

    处理器在 M 模式下运行时,只有在全局中断使能位 mstatus.MIE 置 1 时才会产生中断。

    此外,每个中断类型在控制状态寄存器 mie 中都有自己的使能位:

    • 例如: mie[7]对应于 M 模式中的时钟中断。控制状态寄存器mip 具有相同的布局,并且它指示当前待处理的中断。
    • 将所有三个控制状态寄存器合在一起考虑,如果 mstatus.MIE = 1,mie[7] = 1,且 mip[7] = 1,则可以处理机器的时钟中断。

    当一个 hart 发生异常时,硬件自动经历如下的状态转换:

    • 异常指令的 PC 被保存在 mepc 中,PC 被设置为 mtvec。(对于同步异常,mepc指向导致异常的指令;对于中断,它指向中断处理后应该恢复执行的位置。)
    • 根据异常来源设置 mcause ,并将 mtval 设置为出错的地址或者其它适用于特定异常的信息字。
    • 把控制状态寄存器 mstatus 中的 MIE 位置零以禁用中断,并把先前的 MIE 值保留到 MPIE 中。
    • 发生异常之前的权限模式保留在 mstatus 的 MPP 域中,再把权限模式更改为M。

    下面以一个时钟中断处理程序为例,进行讲解:

    • 该时钟中断处理程序只对时间比较器执行了递增操作,然后继续执行之前的任务
    • 更实际的时钟中断处理程序可能会调用调度程序,从而在任务直接进行切换
    • 我们这个时钟中断处理程序是非抢占的,因此在处理程序的过程中中断会被禁用

    • 第一部分保存了五个寄存器,把 a0 保存在 mscratch 中,a1 到 a4 保存在内存中

    函数调用过程中,有关寄存器使用的编程约定如下:

    • 上面这段汇编的作用:
      • 检查 mcause 来读取异常的类别。
      • 检查trap陷入是否是一个中断(通过 bgez 指令), 如果 mcause<0 则是中断,反之则是同步异常
      • 通过使用掩码(0x3f)和按位与操作,从mcause中取出中断的种类。
      • 与预期的中断类型进行比较,以确定是否是特定类型的中断。如果不是特定类型的中断,则可以跳转到其他处理逻辑(otherInt)。
      • 检查 mcause 的低位是否等于 7,如果是,就是 M 模式的时钟中断。

    • 上面这段汇编的作用:
      • 如果确定是时钟中断,就给时间比较器加上 1000 个时钟周期,于是下一个时钟中断会发生在大约 1000 个时钟周期之后。

    注意:

    • mtimecmp 是 RISC-V 架构中的一个特殊寄存器,用于设置定时器中断比较值。
    • 在 RISC-V 架构中,mtimecmp 是一个 64 位的计时器比较寄存器,用于与 mtime 寄存器进行比较。mtime 是一个 64 位的计时器寄存器,用于存储系统的时钟计数值。
    • 通过设置 mtimecmp 的值,可以实现定时器中断的触发。当 mtime 的计数值达到或超过 mtimecmp 的值时,会触发定时器中断。
    • 具体使用方式如下:
      • 将定时器中断的触发时间设定为一个期望的时刻,将该时刻的计数值存储到 mtimecmp 寄存器中。
      • 系统会持续运行,并且 mtime 寄存器会不断递增。
      • 当 mtime 的计数值达到或超过 mtimecmp 的值时,定时器中断会被触发。
      • 在中断处理程序中,可以执行相应的中断处理逻辑。
    • 通过设置不同的 mtimecmp 值,可以实现不同的定时器中断触发时机,从而实现定时任务或周期性的中断处理。

    • 最后一段恢复了 a0 到 a4 和 mscratch,然后用 mret 指令返回。

    注意
    • 为避免覆盖整数寄存器中的内容,中断处理程序先在最开始用 mscratch 和整数寄存器(例如 a0)中的值交换。
    • 通常,软件会让 mscratch 包含指向附加临时内存空间的指针,处理程序用该指针来保存其主体中将会用到的整数寄存器。

    mscratch通常用来执行当前执行线程的环境上下文空间,如果是linux可能就是task_struct结构体在内存中的位置了。

    • 在主体执行之后,中断程序会恢复它保存到内存中的寄存器,然后再次使用 mscratch 和 a0 交换,将两个寄存器恢复到它们在发生异常之前的值。
    • 最后,处理程序用 mret 指令(M 模式特有的指令)返回。
    • mret 将 PC 设置为 mepc,通过将 mstatus 的 MPIE 域复制到 MIE 来恢复之前的中断使能设置,并将权限模式设置为 mstatus 的 MPP 域中的值。
    • 这基本是前一段中描述的逆操作。

    有时需要在处理异常的过程中转到处理更高优先级的中断。mepc,mcause,mtval 和 mstatus 这些控制寄存器只有一个副本,处理第二个中断的时候,如果软件不进行一些帮助的话,这些寄存器中的旧值会被破坏,导致数据丢失。

    可抢占的中断处理程序可以在启用中断之前把这些寄存器保存到内存中的栈,然后在退出之前,禁用中断并从栈中恢复寄存器。

    除了上面介绍的 mret 指令之外,M 模式还提供了另外一条指令:

    • wfi(Wait For Interrupt): wfi 通知处理器目前没有任何有用的工作,它应该进入低功耗模式,直到任何使能有效的中断等待处理,即mie&mip ≠ 0
    • RISC-V 处理器以多种方式实现该指令,包括到中断待处理之前都停止时钟。
    • 有的时候只把这条指令当作 nop 来执行。
    • 因此,wfi 通常在循环内使用。

    图解Trap流程

    给出一个简单的Trap处理Demo:

    • Trap Handler初始化阶段
    void trap_init(){/* * 设置mtvec寄存器指向我们写好的trap handler处理函数地址处 * set the trap-vector base-address for machine-mode */w_mtvec((reg_t)trap_vector);}/* Machine-mode interrupt vector */static inline void w_mtvec(reg_t x){//"csrw"指令用于将一个通用寄存器的值写入到指定的CSR(控制和状态寄存器)中//在这里,我们将给定的值x写入到mtvec寄存器中。asm volatile("csrw mtvec, %0" : : "r" (x));}

    trap_vector处理函数是写在汇编中的一段逻辑:

    • trap处理的上半部分(上下文的保存)

    发生异常后,操作系统会给我们一次机会,让异常处理程序尝试解决异常,然后重新执行因发生异常而执行失败的指令。


    • trap handler处理函数调用(下半部分)
    1. 保存(save)当前控制流的上下文信息到mscratch指向的当前进程在内存中的Context空间
    2. 调用c语言的trap handler
    3. 从trap handler函数返回,mepc的值需要调整
    4. 恢复(restore)上下文信息
    5. 执行MRET指令返回到trap之前的状态

      几个问题解答一下:
    • 为什么非要交换mscratch和t6呢?不能直接将mscratch传入reg_save宏吗?
      • 关键在于CSR寄存器不能直接使用存储器访问指令(如sd和ld)进行读取和写入,CSR寄存器的访问需要使用特定的指令进行读取和写入操作。
    • reg_save宏定义怎么保存通用寄存器(GP)的值保存到上下文中的呢?
    # save all General-Purpose(GP) registers to context# struct context *base = &ctx_task;# base->ra = ra;# .......macro reg_save basesw ra, 0(\base)sw sp, 4(\base)sw gp, 8(\base)sw tp, 12(\base)sw t0, 16(\base)sw t1, 20(\base)sw t2, 24(\base)sw s0, 28(\base)sw s1, 32(\base)sw a0, 36(\base)sw a1, 40(\base)sw a2, 44(\base)sw a3, 48(\base)sw a4, 52(\base)sw a5, 56(\base)sw a6, 60(\base)sw a7, 64(\base)sw s2, 68(\base)sw s3, 72(\base)sw s4, 76(\base)sw s5, 80(\base)sw s6, 84(\base)sw s7, 88(\base)sw s8, 92(\base)sw s9, 96(\base)sw s10, 100(\base)sw s11, 104(\base)sw t3, 108(\base)sw t4, 112(\base)sw t5, 116(\base)# we don't save t6 here, due to we have used# it as base, we have to save t6 in an extra step# outside of reg_save.endm

    • reg_restore怎么恢复上下文的呢?
    # restore all General-Purpose(GP) registers from the context# struct context *base = &ctx_task;# ra = base->ra;# .......macro reg_restore baselw ra, 0(\base)lw sp, 4(\base)lw gp, 8(\base)lw tp, 12(\base)lw t0, 16(\base)lw t1, 20(\base)lw t2, 24(\base)lw s0, 28(\base)lw s1, 32(\base)lw a0, 36(\base)lw a1, 40(\base)lw a2, 44(\base)lw a3, 48(\base)lw a4, 52(\base)lw a5, 56(\base)lw a6, 60(\base)lw a7, 64(\base)lw s2, 68(\base)lw s3, 72(\base)lw s4, 76(\base)lw s5, 80(\base)lw s6, 84(\base)lw s7, 88(\base)lw s8, 92(\base)lw s9, 96(\base)lw s10, 100(\base)lw s11, 104(\base)lw t3, 108(\base)lw t4, 112(\base)lw t5, 116(\base)lw t6, 120(\base).endm


    我们这里采用的是Direct模式,即由一个统一的异常处理函数作为入口地址:

    reg_t trap_handler(reg_t epc, reg_t cause){reg_t return_pc = epc;reg_t cause_code = cause & 0xfff;//处理中断if (cause & 0x80000000) {/* Asynchronous trap - interrupt */switch (cause_code) {case 3:uart_puts("software interruption!\n");break;case 7:uart_puts("timer interruption!\n");break;case 11:uart_puts("external interruption!\n");break;default:uart_puts("unknown async exception!\n");break;}} else { //处理异常/* Synchronous trap - exception */printf("Sync exceptions!, code = %d\n", cause_code);panic("OOPS! What can I do!");//return_pc += 4;}return return_pc;}


    • trap返回

    调试:

    • 启动入口
    void start_kernel(void){uart_init();uart_puts("Hello, RVOS!\n");page_init();//新增trap模块初始化---设置mtvec指向trap_vector处理函数入口地址trap_init();sched_init();os_main();schedule();uart_puts("Would not go here!\n");while (1) {}; // stop here!}
    • 在初始化的user_task0任务中,执行会产生异常的指令
    void user_task0(void){uart_puts("Task 0: Created!\n");while (1) {uart_puts("Task 0: Running...\n");//执行会产生异常的指令trap_test();task_delay(DELAY);task_yield();}}
    • 产生异常的操作
    void trap_test(){/* * Synchronous exception code = 7 * Store/AMO access fault */*(int *)0x00000000 = 100;/* * Synchronous exception code = 5 * Load access fault *///int a = *(int *)0x00000000;uart_puts("Yeah! I'm return back from trap!\n");}

    我们这里测试三种情况:

    输出结果和预期符合: trap返回后,会重新执行产生异常的指令,所以产生了死循环

    void panic(char *s){printf("panic: ");printf(s);printf("\n");while(1){};}

    输出结果与预期相符合: painc函数的调用会使得程序一直卡住trap处理函数中,并且此时处于关中断状态



    嵌入式系统中的用户模式和进程隔离

    虽然机器模式对于简单的嵌入式系统已经足够,但它仅适用于那些整个代码库都可信的情况,因为 M 模式可以自由地访问硬件平台。

    更常见的情况是,不能信任所有的应用程序代码,因为不能事先得知这一点,或者它太大,难以证明正确性。

    因此,RISC-V 提供了保护系统免受不可信的代码危害的机制,并且为不受信任的进程提供隔离保护。

    必须禁止不可信的代码执行特权指令(如 mret)和访问特权控制状态寄存器(如 mstatus),因为这将允许程序控制系统。这样的限制很容易实现,只要加入一种额外的权限模式:用户模式(U 模式)

    • 这种模式拒绝使用这些功能,并在尝试执行 M 模式指令或 访问 CSR 的时候产生非法指令异常

    其它时候,U 模式和 M 模式的表现十分相似。通过将 mstatus.MPP 设置为 U,然后执行 mret 指令,软件可以从 M 模式进入 U 模式。如果在 U 模式下发生异常,则把控制移交给 M 模式。

    这些不可信的代码还必须被限制只能访问自己那部分内存:

    • 实现了 M 和 U 模式的处理器具有一个叫做物理内存保护(PMP,Physical Memory Protection)的功能,允许 M 模式指定 U 模式可以访问的内存地址。
    • PMP 包括几个地址寄存器(通常为 8 到 16 个)和相应的配置寄存器。这些配置寄存器可以授予或拒绝读、写和执行权限。
    • 当处于 U 模式的处理器尝试取指或执行 load 或 store 操作时,将地址和所有的 PMP 地址寄存器比较。
    • 如果地址大于等于 PMP 地址 i,但小于 PMP 地址 i+1,则 PMP i+1 的配置寄存器决定该访问是否可以继续,如果不能将会引发访问异常。

    此部分内容详细可以参考RISC-V手册的10.4章节


    现代操作系统的监管者模式

    上一节中描述的 PMP 方案对嵌入式系统的实现很有吸引力,因为它以相对较低的成本提供了内存保护,但它的一些缺点限制了它在通用计算中的使用:

    • 由于 PMP 仅支持固定数量的内存区域,因此无法对它进行扩展从而适应复杂的应用程序。
    • 而且由于这些区域必须在物理存储中连续,因此系统可能产生存储碎片化的问题。
    • 另外,PMP 不能有效地支持对辅存的分页。

    更复杂的 RISC-V 处理器用和几乎所有通用架构相同的方式处理这些问题:

    • 使用基于页面的虚拟内存。这个功能构成了监管者模式(S 模式)的核心,这是一种可选的权限模式,旨在支持现代类 Unix 操作系统,如 Linux,FreeBSD 和 Windows。
    • S 模式比 U 模式权限更高,但比 M 模式低。与 U 模式一样,S 模式下运行的软件不能使用 M 模式的 CSR 和指令,并且受到 PMP 的限制。

    默认情况下,发生所有异常(不论在什么权限模式下)的时候,控制权都会被移交到 M 模式的异常处理程序。但是 Unix 系统中的大多数异常都应该进行 S 模式下的系统调用。

    M 模式的异常处理程序可以将异常重新导向 S 模式,但这些额外的操作会减慢大多数异常的处理速度。因此,RISC-V 提供了一种异常委托机制。通过该机制可以选择性地将中断和同步异常交给 S 模式处理,而完全绕过 M 模式。

    mideleg(Machine Interrupt Delegation,机器中断委托)CSR 控制将哪些中断委托给 S 模式。与 mip 和 mie 一样,mideleg 中的每个位对应于mip和mie中相同的异常。

    例如:

    • mideleg[5]对应于 S 模式的时钟中断,如果把它置位,S 模式的时钟中断将会移交 S 模式的异常处理程序,而不是 M 模式的异常处理程序。

    委托给 S 模式的任何中断都可以被 S 模式的软件屏蔽。sie(Supervisor Interrupt Enable,监管者中断使能)sip(Supervisor Interrupt Pending,监管者中断待处理)CSR是 S 模式的控制状态寄存器,他们是 mie 和 mip 的子集。

    • 它们有着和 M 模式下相同的布局,但在 sie 和 sip 中,只有与由 mideleg 委托的中断对应的位才能读写。
    • 那些没有被委派的中断对应的位始终为零

    M 模式还可以通过 medeleg CSR 将同步异常委托给 S 模式

    • 该机制类似于刚才提到的中断委托,但 medeleg 中的位对应的不再是中断,而是同步异常编码。
    • 例如,置上 medeleg[15]便会把 store page fault(store 过程中出现的缺页)委托给 S 模式。

    请注意,无论委派设置是怎样的,发生异常时控制权都不会移交给权限更低的模式。在 M 模式下发生的异常总是在 M 模式下处理。在 S 模式下发生的异常,根据具体的委派设置,可能由 M 模式或 S 模式处理,但永远不会由 U 模式处理。

    S 模式有几个异常处理 CSR:sepc、stvec、scause、sscratch、stval 和 sstatus,它们执行与 上面描述的 M 模式 CSR 相同的功能。

    下图显示了 sstatus 寄存器的布局:

    监管者异常返回指令 sret 与 mret 的行为相同,但它作用于 S 模式的异常处理 CSR,而不是 M 模式的 CSR。

    S 模式处理异常的行为已和 M 模式非常相似。如果 hart 接受了异常并且把它委派给了S 模式,则硬件会原子地经历几个类似的状态转换,其中用到了 S 模式而不是 M 模式的 CSR:

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

    基于页面的虚拟内存

    S 模式提供了一种传统的虚拟内存系统,它将内存划分为固定大小的页来进行地址转换和对内存内容的保护。

    • 启用分页的时候,大多数地址(包括 load 和 store 的有效地址和 PC 中的地址)都是虚拟地址。
    • 要访问物理内存,它们必须被转换为真正的物理地址,这通过遍历一种称为页表的高基数树实现。
    • 页表中的叶节点指示虚地址是否已经被映射到了真正的物理页面,如果是,则指示了哪些权限模式和通过哪种类型的访问可以操作这个页。
    • 访问未被映射的页或访问权限不足会导致页错误例外(page fault exception)。

    RISC-V 的分页方案以 SvX 的模式命名,其中 X 是以位为单位的虚拟地址的长度。

    • RV32 的分页方案 Sv32 支持 4GiB 的虚址空间,这些空间被划分为 2^10个 4 MiB 大小的巨页。每个巨页被进一步划分为 2^
      10个 4 KiB 大小的基页(分页的基本单位)。
    • 因此,Sv32 的 页表是基数为 2^10的两级树结构。
    • 页表中每个项的大小是四个字节,因此页表本身的大小是 4 KiB。
    • 页表的大小和每个页的大小完全相同,这样的设计简化了操作系统的内存分配。


    上图显示了 Sv32 页表项(page-table entry,PTE)的布局,从左到右分别包含如下所述的域:

    • V 位决定了该页表项的其余部分是否有效(V = 1 时有效)。若 V = 0,则任何遍历到此页表项的虚址转换操作都会导致页错误。
    • R、W 和 X 位分别表示此页是否可以读取、写入和执行。如果这三个位都是 0,那么这个页表项是指向下一级页表的指针,否则它是页表树的一个叶节点。
    • U 位表示该页是否是用户页面。若 U = 0,则 U 模式不能访问此页面,但 S 模式可以。若 U = 1,则 U 模式下能访问这个页面,而 S 模式不能。
    • G 位表示这个映射是否对所有虚址空间有效,硬件可以用这个信息来提高地址转换的性能。这一位通常只用于属于操作系统的页面。
    • A 位表示自从上次 A 位被清除以来,该页面是否被访问过。
    • D 位表示自从上次清除 D 位以来页面是否被弄脏(例如被写入)。
    • RSW 域留给操作系统使用,它会被硬件忽略。
    • PPN 域包含物理页号,这是物理地址的一部分若这个页表项是一个叶节点,那么 PPN 是转换后物理地址的一部分否则 PPN 给出下一节页表的地址

    RV64 支持多种分页方案,但我们只介绍最受欢迎的一种,Sv39:

    • Sv39 使用和 Sv32 相同的 4 KiB 大的基页。
    • 页表项的大小变成 8 个字节,所以它们可以容纳更大的物理地址。
    • 为了保证页表大小和页面大小一致,树的基数相应地降到 2^9,树也变为三层。
    • Sv39 的 512 GiB 地址空间划分为 2^9个 1 GiB 大小的吉页。
    • 每个吉页被进一步划分为 2^9 个巨页。
    • 在Sv39 中这些巨页大小为 2 MiB,比 Sv32 中略小。
    • 每个巨页再进一步分为 2^9个 4 KiB 大小的基页。

    图 10.11 显示了 Sv39 页表项的布局。它和 Sv32 完全相同,只是 PPN 字段被扩展到了44 位,以支持 56 位的物理地址,或者说 2^
    26 GiB 大小的物理地址空间。


    页表基址寄存器

    一个叫 satp(Supervisor Address Translation and Protection,监管者地址转换和保护)的 S 模式控制状态寄存器控制了分页系统。如下图所示,satp 有三个域:

    • MODE 域可以开启分页并选择页表级数,图 10.13 展示了它的编码。
    • ASID(Address Space Identifier,地址空间标识符)域是可选的,它可以用来降低上下文切换的开销。
    • 最后,PPN 字段保存了根页表的物理地址,它以 4 KiB 的页面大小为单位。

    通常 M 模式的程序在第一次进入 S 模式之前会把零写入 satp 以禁用分页,然后 S 模式的程序在初始化页表以后会再次进行satp 寄存器的写操作。


    当在 satp 寄存器中启用了分页时,S 模式和 U 模式中的虚拟地址会以从根部遍历页表的方式转换为物理地址。图 10.14 描述了这个过程:

    • satp.PPN 给出了一级页表的基址,VA[31:22]给出了一级页号,因此处理器会读取位于地址(satp. PPN × 4096 + VA[31: 22] × 4)的页表项。
    • PTE 包含二级页表的基址,VA[21:12]给出了二级页号,因此处理器读取位于地址(PTE. PPN × 4096 + VA[21: 12] × 4)的叶节点页表项。
    • 叶节点页表项的 PPN 字段和页内偏移(原始虚址的最低 12 个有效位)组成了最终结果:物理地址就是(LeafPTE. PPN × 4096 + VA[11: 0])

    注意: stap中存放的页表基址和页表项中存放的页表基址都是物理地址,而非虚拟地址。

    随后处理器会进行物理内存的访问。Sv39 的转换过程几乎和 Sv32 相同,区别在于其具有较大的 PTE 和更多级页表。


    TLB

    如果所有取指,load和 store 操作都导致多次页表访问,那么分页会大大地降低性能!所有现代的处理器都用地址转换缓存(通常称为 TLB,全称为 Translation Lookaside Buffer)来减少这种开销。

    为了降低这个缓存本身的开销,大多数处理器不会让它时刻与页表保持一致。这意味着如果操作系统修改了页表,那么这个缓存会变得陈旧而不可用。

    S 模式添加了另一条指令来解决这个问题。这条 sfence.vma 会通知处理器,软件可能已经修改了页表,于是处理器可以相应地刷新转换缓存。

    它需要两个可选的参数,这样可以缩小缓存刷新的范围:

    • 一个位于rs1,它指示了页表哪个虚址对应的转换被修改了;
    • 另一个位于 rs2,它给出了被修改页表的进程的地址空间标识符(ASID)。
    • 如果两者都是 x0,便会刷新整个转换缓存

    补充说明:

    • 多处理器中的地址转换缓存一致性sfence.vma 仅影响执行当前指令的 hart 的地址转换硬件。
    • 当 hart 更改了另一个 hart 正在使用的页表时,前一个 hart 必须用处理器间中断来通知后一个 hart,他应该执行 sfence.vma
      指令。
    • 这个过程通常被称为 TLB 击落

    完整虚拟地址翻译过程

    前提说明:

    • va是输入的虚拟地址
    • pa是输出的物理地址
    • PAGESIZE是常数2^12
    • Sv32中,LEVELS=2 且 PTESIZE=4 字节
    • Sv39中,LEVELS=3 且 PTESIZE=8 字节

    虚地址到物理地址转换的完整算法:


    小结

    RISC-V 特权架构的模块化特性满足了各种系统的需求。

    十分精简的机器模式以低成本的特征支持裸机嵌入式应用。附加的用户模式和物理内存保护功能共同支持了更复杂的嵌入式系统中的多任务处理。

    最后,监管者模式和基于页面的虚拟内存提供了运行现代操作系统所必需的灵活性。