本系列参考: 学习开发一个RISC-V上的操作系统 - 汪辰 - 2021春 整理而来,主要作为xv6操作系统学习的一个前置基础。
课程代码和环境搭建教程参考github仓库: https://github.com/plctlab/riscv-operating-system-mooc/blob/main/howto-run-with-ubuntu1804_zh.md
前置知识:
本文主要参考: RISC-V手册整理而来。
高权限模式通常可以使用权限较低的模式的所用功能,并且它们还有一些低权限模式下不可用的额外功能,例如:
特权架构添加了很少的指令,作为替代,几个新的控制状态寄存器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,且低有效位标识了中断或异常的具体原因。只有在实现了监管者模式时才能处理监管者模式中断和页面错误异常。
有三种标准的中断源:软件、时钟和外部来源。
不同的硬件平台具有不同的内存映射并且需要中断控制器的不同特性,因此用于发出和消除这些中断的机制因平台而异。所有 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指向trap处理函数的入口地址:
注意: 在MODE=Vectored向量的模式下,vector table 存放的 interrupt handler 的地址,在 interrupt 发生时先进行一次间接寻址得到对应的 interrupt handler 的地址,再将其设定为 pc 的值,这是不正确的。
当发生trap时,会跳转到异常控制流处执行,而mepc负责保存异常控制流函数的返回地址:
mstatus
是RISC-V架构中的一个机器级别(M-Mode)的CSR寄存器,用于管理和控制处理器的状态。
mstatus
寄存器是一个32位的寄存器,包含了多个位字段,每个位字段都有特定的含义和功能。以下是mstatus
寄存器中一些常见的位字段:
MIE
(Machine Interrupt Enable):该位控制是否允许处理器接受中断。当MIE
为1时,处理器可以响应中断请求;当MIE
为0时,处理器将禁止中断。
MPIE
(Machine Previous Interrupt Enable):该位记录上一级特权级别(S-Mode或U-Mode)的中断使能状态。在特权级别切换时,这个位的值会被保存和恢复。
MPP
(Machine Previous Privilege):该位字段指示上一级特权级别,用于保存处理器从异常或中断返回时的特权级别。可能的值包括M-Mode(3’b11)、S-Mode(3’b01)和 U-Mode(3’b00)。
MIE
(Machine Exception Enable):该位用于控制是否允许处理器接受异常。当MIE
为1时,处理器可以响应异常;当MIE
为0时,处理器将禁止异常。
mstatus
寄存器还包含其他位字段,用于管理处理器的特权级别、虚拟化、调试模式等。具体的位字段定义和功能可以在RISC-V的官方规范中找到。
通过读取和写入mstatus
寄存器,可以控制和监控处理器的状态,例如启用或禁用中断、设置特权级别、处理异常等。这对于操作系统、异常处理程序和其他特权级别的软件非常重要。
注意:
首先明确一点:
mie
是RISC-V架构中的一个机器级别(M-Mode)的CSR寄存器,用于控制和管理处理器的中断使能状态。
mie
寄存器是一个32位的寄存器,每个位对应一个特定类型的中断。以下是mie
寄存器中一些常见的位字段:
MSIE
(Machine Software Interrupt Enable):该位控制是否允许处理器接受机器级别软件中断(MSI)。当MSIE
为1时,处理器可以接受并响应MSI;当MSIE
为0时,处理器将禁止MSI。
MTIE
(Machine Timer Interrupt Enable):该位控制是否允许处理器接受机器级别定时器中断。当MTIE
为1时,处理器可以接受并响应定时器中断;当MTIE
为0时,处理器将禁止定时器中断。
MEIE
(Machine External Interrupt Enable):该位控制是否允许处理器接受机器级别外部中断。当MEIE
为1时,处理器可以接受并响应外部中断;当MEIE
为0时,处理器将禁止外部中断。
通过读取和写入mie
寄存器,可以控制处理器接受和屏蔽不同类型的中断。中断发生时,处理器会根据mie
寄存器中相应的位来决定是否触发中断处理程序。
需要注意的是,mie
寄存器的设置可能会受到其他控制寄存器(如mstatus
寄存器)中相关位字段的影响。例如,如果mstatus
寄存器的MIE
位为0,即使mie
寄存器中某个中断使能位为1,该中断也不会触发。
中断处理和中断控制是处理器中重要的功能,它们可以用于处理异步事件、外部设备的输入、定时器等。mie
寄存器在这个过程中起到了关键的作用,它允许软件控制中断的使能和屏蔽,以适应特定的需求和应用场景。
mip
是RISC-V架构中的一个机器级别(M-Mode)的CSR寄存器,用于表示和管理处理器的中断请求状态。
mip
寄存器是一个32位的寄存器,每个位对应一个特定类型的中断请求。以下是mip
寄存器中一些常见的位字段:
MSIP
(Machine Software Interrupt Pending):该位表示机器级别软件中断(MSI)的中断请求状态。当MSIP
为1时,表示有一个MSI中断请求待处理;当MSIP
为0时,表示没有MSI中断请求。
MTIP
(Machine Timer Interrupt Pending):该位表示机器级别定时器中断的中断请求状态。当MTIP
为1时,表示有一个定时器中断请求待处理;当MTIP
为0时,表示没有定时器中断请求。
MEIP
(Machine External Interrupt Pending):该位表示机器级别外部中断的中断请求状态。当MEIP
为1时,表示有一个外部中断请求待处理;当MEIP
为0时,表示没有外部中断请求。
通过读取mip
寄存器,可以查询处理器当前的中断请求状态,以了解是否有中断待处理。在处理器响应中断时,处理器会根据mip
寄存器中相应的位来确定中断的来源。
另外,可以通过写入mip
寄存器的特定位,手动触发或清除中断请求,以模拟中断的发生或结束。这在某些调试或测试场景下可能很有用。
需要注意的是,mip
寄存器的状态可能会受到其他控制寄存器(如mie
寄存器)中相关位字段的影响。例如,如果mie
寄存器的相应位为0,即使mip
寄存器中某个中断请求位为1,该中断也不会被触发。
中断请求和处理是处理器中重要的功能,它们用于异步事件的处理、外部设备的输入、定时器的触发等。mip
寄存器提供了一种机制,使软件能够检查和处理中断请求,以响应相关的事件和中断源。
mscratch
是 RISC-V 架构中的一个控制和状态寄存器(Control and Status Register),用于保存机器模式下的临时数据或上下文相关的信息。它的作用是提供一个通用的、临时的存储位置,供软件使用。
具体而言,mscratch
寄存器通常用于以下情况:
上下文切换:当处理器从一个上下文切换到另一个上下文时,可以将当前的 mscratch
寄存器的值保存到保存的上下文中。在切换到新的上下文后,可以将先前保存的 mscratch
寄存器的值恢复,以便继续使用其中的数据。
异步事件处理:当处理器在处理中断或异常时,可能需要保存一些临时数据,以便在恢复正常执行后继续使用。mscratch
寄存器提供了一个方便的位置来存储这些临时数据,以避免污染其他重要的寄存器。
调试和跟踪:在调试和跟踪应用程序时,mscratch
寄存器可以用于存储调试器或跟踪工具的临时数据,例如断点信息、调试状态等。
需要注意的是,mscratch
寄存器的使用是由软件决定的,它没有特定的预定义用途。软件可以根据需要将 mscratch
寄存器用于临时存储和处理数据。然而,由于 mscratch
寄存器的值可能会被上下文切换或其他操作修改,因此软件在使用 mscratch
寄存器时应注意保存和恢复其中的数据。
总结:mscratch
寄存器是 RISC-V 架构中的一个控制和状态寄存器,用于保存机器模式下的临时数据或上下文相关的信息。它可以用于上下文切换、异步事件处理、调试和跟踪等情况,提供一个通用的临时存储位置供软件使用。
处理器在 M 模式下运行时,只有在全局中断使能位 mstatus.MIE 置 1 时才会产生中断。
此外,每个中断类型在控制状态寄存器 mie 中都有自己的使能位:
当一个 hart 发生异常时,硬件自动经历如下的状态转换:
(对于同步异常,mepc指向导致异常的指令;对于中断,它指向中断处理后应该恢复执行的位置。)
下面以一个时钟中断处理程序为例,进行讲解:
注意:
- mtimecmp 是 RISC-V 架构中的一个特殊寄存器,用于设置定时器中断比较值。
- 在 RISC-V 架构中,mtimecmp 是一个 64 位的计时器比较寄存器,用于与 mtime 寄存器进行比较。mtime 是一个 64 位的计时器寄存器,用于存储系统的时钟计数值。
- 通过设置 mtimecmp 的值,可以实现定时器中断的触发。当 mtime 的计数值达到或超过 mtimecmp 的值时,会触发定时器中断。
- 具体使用方式如下:
- 将定时器中断的触发时间设定为一个期望的时刻,将该时刻的计数值存储到 mtimecmp 寄存器中。
- 系统会持续运行,并且 mtime 寄存器会不断递增。
- 当 mtime 的计数值达到或超过 mtimecmp 的值时,定时器中断会被触发。
- 在中断处理程序中,可以执行相应的中断处理逻辑。
- 通过设置不同的 mtimecmp 值,可以实现不同的定时器中断触发时机,从而实现定时任务或周期性的中断处理。
mscratch通常用来执行当前执行线程的环境上下文空间,如果是linux可能就是task_struct结构体在内存中的位置了。
有时需要在处理异常的过程中转到处理更高优先级的中断。mepc,mcause,mtval 和 mstatus 这些控制寄存器只有一个副本,处理第二个中断的时候,如果软件不进行一些帮助的话,这些寄存器中的旧值会被破坏,导致数据丢失。
可抢占的中断处理程序可以在启用中断之前把这些寄存器保存到内存中的栈,然后在退出之前,禁用中断并从栈中恢复寄存器。
除了上面介绍的 mret 指令之外,M 模式还提供了另外一条指令:
wfi(Wait For Interrupt)
: wfi 通知处理器目前没有任何有用的工作,它应该进入低功耗模式,直到任何使能有效的中断等待处理,即mie&mip ≠ 0
。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));
}
发生异常后,操作系统会给我们一次机会,让异常处理程序尝试解决异常,然后重新执行因发生异常而执行失败的指令。
# save all General-Purpose(GP) registers to context
# struct context *base = &ctx_task;
# base->ra = ra;
# ......
.macro reg_save base
sw 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
# restore all General-Purpose(GP) registers from the context
# struct context *base = &ctx_task;
# ra = base->ra;
# ......
.macro reg_restore base
lw 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;
}
调试:
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!
}
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 模式。
这些不可信的代码还必须被限制只能访问自己那部分内存:
(PMP,Physical Memory Protection)
的功能,允许 M 模式指定 U 模式可以访问的内存地址。此部分内容详细可以参考RISC-V手册的10.4章节
上一节中描述的 PMP 方案对嵌入式系统的实现很有吸引力,因为它以相对较低的成本提供了内存保护,但它的一些缺点限制了它在通用计算中的使用:
更复杂的 RISC-V 处理器用和几乎所有通用架构相同的方式处理这些问题:
默认情况下,发生所有异常(不论在什么权限模式下)的时候,控制权都会被移交到 M 模式的异常处理程序。但是 Unix 系统中的大多数异常都应该进行 S 模式下的系统调用。
M 模式的异常处理程序可以将异常重新导向 S 模式,但这些额外的操作会减慢大多数异常的处理速度。因此,RISC-V 提供了一种异常委托机制。通过该机制可以选择性地将中断和同步异常交给 S 模式处理,而完全绕过 M 模式。
mideleg(Machine Interrupt Delegation,机器中断委托)
CSR 控制将哪些中断委托给 S 模式。与 mip 和 mie 一样,mideleg 中的每个位对应于mip和mie中相同的异常。
例如:
委托给 S 模式的任何中断都可以被 S 模式的软件屏蔽。sie(Supervisor Interrupt Enable,监管者中断使能)
和 sip(Supervisor Interrupt Pending,监管者中断待处理)
CSR是 S 模式的控制状态寄存器,他们是 mie 和 mip 的子集。
M 模式还可以通过 medeleg CSR 将同步异常委托给 S 模式。
请注意,无论委派设置是怎样的,发生异常时控制权都不会移交给权限更低的模式。在 M 模式下发生的异常总是在 M 模式下处理。在 S 模式下发生的异常,根据具体的委派设置,可能由 M 模式或 S 模式处理,但永远不会由 U 模式处理。
S 模式有几个异常处理 CSR:sepc、stvec、scause、sscratch、stval 和 sstatus
,它们执行与 上面描述的 M 模式 CSR 相同的功能。
监管者异常返回指令 sret 与 mret 的行为相同,但它作用于 S 模式的异常处理 CSR,而不是 M 模式的 CSR。
S 模式处理异常的行为已和 M 模式非常相似。如果 hart 接受了异常并且把它委派给了S 模式,则硬件会原子地经历几个类似的状态转换,其中用到了 S 模式而不是 M 模式的 CSR:
S 模式提供了一种传统的虚拟内存系统,它将内存划分为固定大小的页来进行地址转换和对内存内容的保护。
RISC-V 的分页方案以 SvX 的模式命名,其中 X 是以位为单位的虚拟地址的长度。
上图显示了 Sv32 页表项(page-table entry,PTE)
的布局,从左到右分别包含如下所述的域:
如果这三个位都是 0,那么这个页表项是指向下一级页表的指针,否则它是页表树的一个叶节点。
PPN 域包含物理页号,这是物理地址的一部分
。若这个页表项是一个叶节点,那么 PPN 是转换后物理地址的一部分
。否则 PPN 给出下一节页表的地址
。RV64 支持多种分页方案,但我们只介绍最受欢迎的一种,Sv39:
图 10.11 显示了 Sv39 页表项的布局。它和 Sv32 完全相同,只是 PPN 字段被扩展到了44 位,以支持 56 位的物理地址,或者说 2^
26 GiB 大小的物理地址空间。
一个叫 satp(Supervisor Address Translation and Protection,监管者地址转换和保护)
的 S 模式控制状态寄存器控制了分页系统。如下图所示,satp 有三个域:
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 和更多级页表。
如果所有取指,load和 store 操作都导致多次页表访问,那么分页会大大地降低性能!所有现代的处理器都用地址转换缓存(通常称为 TLB,全称为 Translation Lookaside Buffer)
来减少这种开销。
为了降低这个缓存本身的开销,大多数处理器不会让它时刻与页表保持一致。这意味着如果操作系统修改了页表,那么这个缓存会变得陈旧而不可用。
S 模式添加了另一条指令来解决这个问题。这条 sfence.vma
会通知处理器,软件可能已经修改了页表,于是处理器可以相应地刷新转换缓存。
它需要两个可选的参数,这样可以缩小缓存刷新的范围:
补充说明:
- 多处理器中的地址转换缓存一致性sfence.vma 仅影响执行当前指令的 hart 的地址转换硬件。
- 当 hart 更改了另一个 hart 正在使用的页表时,前一个 hart 必须用处理器间中断来通知后一个 hart,他应该执行 sfence.vma
指令。- 这个过程通常被称为 TLB 击落
前提说明:
虚地址到物理地址转换的完整算法:
RISC-V 特权架构的模块化特性满足了各种系统的需求。
十分精简的机器模式以低成本的特征支持裸机嵌入式应用。附加的用户模式和物理内存保护功能共同支持了更复杂的嵌入式系统中的多任务处理。
最后,监管者模式和基于页面的虚拟内存提供了运行现代操作系统所必需的灵活性。