Trap 简介
控制流(Control Flow)和 Trap
- 控制流(Control Flow) 从给处理器加电开始,直到你断电为止,程序计数器假设一个值的序列 $$a_0,a_1,\dotsb,a_{n-1}$$ 每个$a_k$都是指令的地址,每次从$a_{k}$到$a_{k+1}$的过渡称为控制转移,而这样的控制转移序列叫做处理器的控制流。
- 异常控制流(Exceptional Control Flow, ECF)
系统也必须能够对系统状态的变化做出反应,这些系统状态不是被内部程序变量捕获的,而且也不一定要和程序的执行相关。比如,一个硬件定时器定期产生信号,这个事件必须得到处理。包到达网络适配器后,必须存放在内存中。程序向磁盘请求数据,然后休眠,直到被通知说数据已就绪。现代系统通过使控制流发生突变来对这些情况做出反应。我们把这些突变称为异常控制流。
- exception
- interrupt
RISC-V 把 ECF
统称为 Trap
。
RISC-V Trap 处理中涉及的寄存器
寄存器 | 全称 | 用途说明 |
---|---|---|
mtvec | Machine Trap-Vector Base-Address | 它保存发生异常时处理器需要跳转到的地址。 |
mepc | Machine Exception Program Counter | 当 trap 发生时,hart 会将发生 trap 所对应的指令的地址值(pc)保存在 mepc 中。 |
mcause | Machine Cause | 当 trap 发生时,hart 会设置该寄存器通知我们 trap 发生的原因。 |
mtval | Machine Trap Value | 它保存了 exception 发生时的附加信息:譬如访问地址出错时的地址信息、或者执行非法指令时的指令本身,对于其他异常,它的值为 0。 |
mstatus | Machine Status | 用于跟踪和控制 hart 的当前操作状态(特别地,包括关闭和打开全局中断)。 |
mscratch | Machine Scratch | Machine 模式下专用寄存器,我们可以自己定义其用法,譬如用该寄存器保存当前在 hart 上运行的 task 的上下文(context)的地址。 |
mtvec(Machine Trap-Vector Base-Address)
WARL: Write Any Values, Read Legal Values
BASE:trap 入口函数的基地址,必须保证四字节对齐;
MODE:进一步用于控制入口函数的地址配置方式:
Direct,所有异常和中断发生后,PC都跳转到BASE指定的地址处;
Responsive Image 通常中断处理函数内部会有
switch case
条件语句,通过不同的中断采用不同的处理方式。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; }
Vectored,异常的处理方式同上,但是中断的入口地址以数组方式排列;
trap_vector: # save context(registers). csrrw t6, mscratch, t6 # swap t6 and mscratch reg_save t6 # Save the actual t6 register, which we swapped into # mscratch mv t5, t6 # t5 points to the context of current task csrr t6, mscratch # read t6 back from mscratch sw t6, 120(t5) # save t6 with t5 as base # Restore the context pointer into mscratch csrw mscratch, t5 # call the C trap handler in trap.c csrr a0, mepc csrr a1, mcause call trap_handler # trap_handler will return the return address via a0. csrw mepc, a0 # restore context(registers). csrr t6, mscratch reg_restore t6 # return to whatever we were doing before trap. mret
MODE可取值如下:
Responsive Image
采用Vectored
方式效率更高。
mepc(Machine Exception Program Counter)
当trap
发生时,pc
会被替换为 mtvec
设定的地址,同时hart
会设置mepc
为当前指令或者下一条指令的地址(处理异常时,mepc 为当前指令的地址,处理中断时,mepc 为下一条指令的地址)。
当我们需要退出trap
时可以调用特殊的 mret
指令,该指令会将mepc
中的值恢复到pc
中(实现返回的效果);
在处理 trap
的程序中我们可以修改 mepc
的值达到改变mret
返回地址的目的。
mcause(Machine Cause)
当 trap
发生时,hart
会设置该寄存器通知我们 trap
发生的原因。
最高位 Interrupt
为 1 时标识了当前 trap
为interrupt
,否则是exception
。
剩余的 Exception Code
用于标识具体的interrupt
或者exception
的种类。
mtval(Machine Trap Value)
当 trap
发生时,除了通过mcause
可以获取exception
的种类 code
值外,hart
还提供了 mtval
来提供exception
的其他信息来辅助我们执行更进一步的操作。
具体的辅助信息由特定的硬件实现定义,RISC-V 规范没有定义具体的值。但规范定义了一些行为,譬如访问地址出错时的地址信息、或者执行非法指令时的指令本身等。
mstatus(Machine Status)
寄存器各个位可以大致分为以下三类,其中x
可以为U,S,M
。表示用户模式以及两种特权模式。
xIE
(x=M/S/U): 分别用于打开(1)或者关闭(0)M/S/U 模式下的全局中断。当trap
发生时,hart
会自动将xIE
设置为 0。xPIE
(x=M/S/U):当trap
发生时用于保存trap
发生之前的xIE
值。xPP
(x=M/S):当trap
发生时用于保存trap
发生之前的权限级别值。注意没有UPP
。因为异常只会从低权限向高权限跳转,通常低权限如user
模式,会被置于上方,高权限如内核一般都会画在下方,这也解释了异常,中断处理为什么叫trap
,因为是向下陷入的过程。其他标志位涉及内存访问权限、虚拟内存控制等,暂不考虑。
Trap 处理流程
主要为 Exception,下一章详解 Interrupt。
初始化
将trap
的基地址写入寄存器,
Top Half
- 把
mstatus
的MIE
值复制到MPIE
中,清除mstatus
中的MIE
标志位,效果是中断被禁止。 - 设置
mepc
,同时PC
被设置为mtvec
。(需要注意的是,对于exception
,mepc
指向导致异常的指令;对于interrupt
,它指向被中断的指令的下一条指令的位置。) - 根据
trap
的种类设置mcause
,并根据需要为mtval
设置附加信息。 - 将
trap
发生之前的权限模式保存在mstatus
的MPP
域中,再把hart
权限模式更改为M
(也就是说无论在任何Level
下触发trap
,hart
首先切换到Machine
模式)。
Bottom Half
- 保存(save)当前控制流的上下文信息(利用
mscratch
); - 调用 C 语言的
trap handler
; - 从
trap handler
函数返回,mepc
的值有可能需要调整; - 恢复(restore)上下文的信息;
- 执行
MRET
指令返回到trap
之前的状态。
trap_vector:
# save context(registers).
csrrw t6, mscratch, t6 # swap t6 and mscratch
reg_save t6
# Save the actual t6 register, which we swapped into
# mscratch
mv t5, t6 # t5 points to the context of current task
csrr t6, mscratch # read t6 back from mscratch
sw t6, 120(t5) # save t6 with t5 as base
# Restore the context pointer into mscratch
csrw mscratch, t5
# call the C trap handler in trap.c
csrr a0, mepc
csrr a1, mcause
call trap_handler
# trap_handler will return the return address via a0.
csrw mepc, a0
# restore context(registers).
csrr t6, mscratch
reg_restore t6
# return to whatever we were doing before trap.
mret
退出 trap:编程调用 MRET 指令
针对不同权限级别下如何退出 trap 有各自的返回指令xRET
(x = M/S/U)。以在 M
模式下执行mret
指令为例,会执行如下操作:
- 当前
Hart 的权限级别 = mstatus.MPP
;mstatus.MPP = U
(如果 hart 不支持 U 则为 M) mstatus.MIE = mstatus.MPIE
;mstatus.MPIE = 1
pc = mepc
中断
中断分类
本地(Local)中断
- 软中断software interrupt
- 定时器中断 timer interrupt
全局(Global)中断
- 外部中断 externel interrupt
RISC-V 中断编程中涉及的寄存器
寄存器 | 全称 | 用途说明 |
---|---|---|
mie | Machine Interrupt Enable | 用于进一步控制(打开和关闭)software interrupt/timer interrupt/external interrupt |
mip | Machine Interrupt Pending | 它列出目前已发生等待处理的中断。 |
mie(Machine Interrupt Enable)
打开(1)或者关闭(0)M/S/U 模式下对应的 External/Timer/Software 中断。
mip(Machine Interrupt Pending)
获取当前 M/S/U 模式下对应的 External/Timer/Software 中断是否发生。
中断处理流程
中断处理
- 把
mstatus
的MIE
值复制到MPIE
中,清除mstatus
中的 MIE 标志位,效果是中断被禁止。 - 当前的
PC
的下一条指令地址被复制到mepc
中,同时PC
被设置为mtvec
。注意如果我们设置mtvec.MODE = vetcored
,PC =mtvec.BASE + 4 × exception-code
。 - 根据
interrupt
的种类设置mcause
,并根据需要为mtval
设置附加信息。 - 将
trap
发生之前的权限模式保存在mstatus
的MPP
域中,再把hart
权限模式更改为M
。
退出中断
以在 M 模式下执行 mret 指令为例,会执行如下操作:
- 当前 Hart 的权限级别 = mstatus.MPP; mstatus.MPP= U(如果 hart 不支持 U 则为 M)
- mstatus.MIE = mstatus.MPIE; mstatus.MPIE = 1
- pc = mepc
PLIC(Platform-Level Interrupt Controller)
PLIC 简介
HART 只能处理一个中断,PLIC 相当于一个控制中心,它通过中断类型,优先级等等来选出一个需要处理的中断。协调多个中断,服务一个 HART。
enum {
UART0_IRQ = 10, //Interrupt Source ID
RTC_IRQ = 11,
VIRTIO_IRQ = 1, /* 1 to 8 */
VIRTIO_COUNT = 8,
PCIE_IRQ = 0x20, /* 32 to 35 */
VIRTIO_NDEV = 0x35 /* Arbitrary maximum number of interrupts */
};
- Interrupt Source ID 范围:1 ~ 53(0x35)
- 0 预留不用
PLIC
本身也是一个外设,RISC-V 规范规定,PLIC
的寄存器编址采用内存映射(memory map)方式。每个寄存器的宽度为 32-bit。
具体寄存器编址采用 base + offset
的格式,且 base
由各个特定platform
自己定义。针对 QEMU-virt
,其 PLIC
的设计参考了FU540-C000
,base
为 0x0c000000
。
static const MemMapEntry virt_memmap[] = {
[VIRT_DEBUG] = { 0x0, 0x100 },
[VIRT_MROM] = { 0x1000, 0xf000 },
[VIRT_TEST] = { 0x100000, 0x1000 },
[VIRT_RTC] = { 0x101000, 0x1000 },
[VIRT_CLINT] = { 0x2000000, 0x10000 },
[VIRT_ACLINT_SSWI] = { 0x2F00000, 0x4000 },
[VIRT_PCIE_PIO] = { 0x3000000, 0x10000 },
[VIRT_PLIC] = { 0xc000000, VIRT_PLIC_SIZE(VIRT_CPUS_MAX * 2) },
[VIRT_UART0] = { 0x10000000, 0x100 },
[VIRT_VIRTIO] = { 0x10001000, 0x1000 },
[VIRT_FW_CFG] = { 0x10100000, 0x18 },
[VIRT_FLASH] = { 0x20000000, 0x4000000 },
[VIRT_PCIE_ECAM] = { 0x30000000, 0x10000000 },
[VIRT_PCIE_MMIO] = { 0x40000000, 0x40000000 },
[VIRT_DRAM] = { 0x80000000, 0x0 },
};
PLIC 编程接口 - 寄存器
Priority
功能:设置某一路中断源的优先级
内存映射地址:BASE + (interrupt-id) * 4
- 每个
PLIC
中断源对应一个寄存器,用于配置该中断源的优先级。 QEMU-virt
支持 7 个优先级。0 表示对该中断源禁用中断。其余优先级,1 最低,7 最高。- 如果两个中断源优先级相同,则根据中断源的 ID 值进一步区分优先级,ID 值越小的优先级越高。
Pending
功能:用于指示某一路中断源是否发生
内存映射地址:BASE + 0x1000 + ((interrupt-id) / 32) * 4
- 每个
PLIC
包含 2 个 32 位的Pending
寄存器,因为总共有 54 个中断源,每一个bit
对应一个中断源,如果为 1 表示该中断源上发生了中断(进入Pending
状态),有待hart
处理,否则表示该中断源上当前无中断发生。 Pending
寄存器中断的Pending
状态可以通过claim
方式清除。- 第一个
Pending
寄存器的第 0 位对应不存在的 0 号中断源,其值永远为 0。
Enable
功能:针对某个 hart
开启或者关闭某一路中断源
内存映射地址:BASE + 0x2000 + (hart) * 0x80
- 每个
Hart
有 2 个Enable
寄存器(Enable1
和Enable2
)用于针对该Hart
启动或者关闭某路中断源。 - 每个中断源对应
Enable
寄存器的一个bit
,其中Enable1
负责控制 1 ~ 31 号中断源;Enable2
负责控制32 ~ 53
号中断源。将对应的bit
位设置为 1 表示使能该中断源,否则表示关闭该中断源。
Threshold
功能:针对某个 hart 设置中断源优先级的阈值
内存映射地址:BASE + 0x200000 + (hart) * 0x1000
- 每个
Hart
有 1 个Threshold
寄存器用于设置中断优先级的阈值。 - 所有小于或者等于(<=)该阈值的中断源即使发生了也会被
PLIC
丢弃。特别地,当阈值为 0 时允许所有中断源上发生的中断;当阈值为 7 时丢弃所有中断源上发生的中断。
Claim/Complete
功能:如下
内存映射地址:BASE + 0x200004 + (hart) * 0x1000
Claim
和Complete
是同一个寄存器,每个Hart
一个。- 对该寄存器执行读操作称之为
Claim
,即获取当前发生的最高优先级的中断源ID
。Claim
成功后会清除对应的Pending
位。 - 对该寄存器执行写操作称之为
Complete
。所谓Complete
指的是通知PLIC
对该路中断的处理已经结束。
void external_interrupt_handler()
{
int irq = plic_claim(); //
if (irq == UART0_IRQ){
uart_isr();
} else if (irq) {
printf("unexpected interrupt irq = %d\n", irq);
}
if (irq) {
plic_complete(irq); //
}
}
CLINT (Core Local INTerruptor)
定时器中断,属于本地中断的一种,由芯片内部CLINT
设备产生的中断。
- RISC-V 规范规定,CLINT 的寄存器编址采用内存映射(memory map)方式。
- 具体寄存器编址采用
base + offset
的格式,且base
由各个特定platform
自己定义。针对QEMU-virt
,其 CLINT 的设计参考了SFIVE
,base
为0x2000000
。
CLINT 编程接口 - 寄存器 (Timer 部分)
mtime
功能:real-time
计数器(counter)
内存映射地址:BASE + 0xbff8
- 由晶振产生,系统全局唯一,在
RV32
和RV64
上都是 64-bit。系统必须保证该计数器的值始终按照一个固定的频率递增。 - 上电复位时,硬件负责将
mtime
的值恢复为 0。
mtimecmp
功能:定时器比较寄存器
内存映射地址:BASE + 0x4000 + (hart) * 8)
每个
hart
一个mtimecmp
寄存器,64-bit。上电复位时,系统不负责设置 mt`imecmp 的初值。
当
mtime >= mtimecmp
时,CLINT
会产生一个timer
中断。如果要使能该中断需要保证全局中断打开并且mie.MTIE
标志位置1
。当
timer
中断发生时,hart
会设置mip.MTIP
,程序可以在mtimecmp
中写入新的值清除mip.MTIP
。
时钟节拍 tick
- 操作系统中最小的时间单位;
Tick
的单位(周期)由硬件定时器的周期决定 (通常为 1 ~ 100ms);Tick
周期越小,也就是1s
内产生的中断越多,系统的精度越高,但开销越大。