Computer System II Lab 4 : RV64 时钟中断处理

引言:

时钟中断是操作系统中一个十分重要的概念,一个好的时钟中断,将能使一个CPU发挥两个CPU的功效,大大方便和简化程序的编制,提高系统的效率与可操作性。本实验将完成RV64架构下时钟中断的设置及相关的一些处理。

1 实验目的

2 实验步骤

首先将lab3文件夹中的sbi.h等文件移入lab4工程

2-1 基于lab3的代码更改

按照实验指导,对head.s,vmlinux.lds,test.c等文件进行更改。(在此不表)

2-2 异常处理的开启

这里需要我们去填充head.s

通过学习RV64的特权寄存器手册,明确以下几点内容:

  • 对特权寄存器的操作需要使用特权指令,本实验中主要用到以下几条指令:

1.csrr,读取一个 CSR 的值到通用寄存器。如:csrr t0, mstatus,读取 mstatus 的值到 t0 中。

2.csrw,把一个通用寄存器中的值写入 CSR 中。如:csrw mstatus, t0,将 t0 的值写入 mstatus

3.csrs,把 CSR 中指定的 bit 置 1。如:csrsi mstatus, (1 << 2),将 mstatus 的右起第 3 位置 1。

4.csrc,把 CSR 中指定的 bit 置 0。如:csrci mstatus, (1 << 2),将 mstatus 的右起第 3 位置 0。

5.csrrw,读取一个 CSR 的值到通用寄存器,然后把另一个值写入该 CSR。如:csrrw t0, mstatus, t0,将 mstatus 的值与 t0 的值交换。

6.csrrs,读取一个 CSR 的值到通用寄存器,然后把该 CSR 中指定的 bit 置 1。

7.csrrc,读取一个 CSR 的值到通用寄存器,然后把该 CSR 中指定的 bit 置 0。

  • sie寄存器的第5位为1标志着开启时钟中断
  • 在qemu下1s等于10000000个时钟周期
  • 可以使用rdtime获取当前系统的时间(时钟周期数)
  • sstatus的第1位置1表示开始S态下的响应中断

head.s代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
.extern start_kernel
.section .rodata
str: .string "SPY's Argument!\n"
.section .text.init
.globl _start, _end,_traps
_start:
la a0, _traps
csrw stvec, a0

li a0, 1
slli a0, a0, 5
csrs sie, a0

la sp, stp

li a1, 10000000
rdtime a0
add a0, a1, a0
call sbi_set_timer
# 设置第一次的时钟中断
li a0, 1
slli a0, a0, 1
csrs sstatus, a0
# 也可以写成 csrsi sstatus , (1 << 1)


j start_kernel
.section .stack.entry
.globl .sbp
sbp:
.space 4096
.globl .stp
stp:

需要注意的是我们必须在call函数之前设置sp的值(栈地址)

2-3 实现上下文切换

  • 对于保存CPU的寄存器到栈上和恢复上下文,riscv-v没有push和pop,只能通过sd和ld一个个存,一个个取。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sd sp, -8(sp)
sd ra, -16(sp)
.............
sd t6, -224(sp)
sd tp, -232(sp)
sd gp, -240(sp)

addi sp, sp, -240
.............
.............
ld sp, 232(sp)
ld ra, -16(sp)
.............
ld tp, -232(sp)
ld gp, -240(sp)
  • call trap_handler
1
2
3
csrr a0, scause
csrr a1, sepc
call trap_handler # 传参规范,通过寄存器传参
  • 从trap中返回
1
sret

需要使用S态ret

2-4 实现异常处理函数

要求如下:

1
2
3
4
5
6
7
8
9
10
11
// trap.c 

void trap_handler(unsigned long scause, unsigned long sepc) {
// 通过 `scause` 判断trap类型
// 如果是interrupt 判断是否是timer interrupt
// 如果是timer interrupt 则打印输出相关信息(即 4.6 节中输出的[S] Supervisor Mode Timer Interrupt), 并通过 `clock_set_next_event()` 设置下一次时钟中断
// `clock_set_next_event()` 见 4.5 节
// 其他interrupt / exception 可以直接忽略

# YOUR CODE HERE
}

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
#include<printk.h>
#include"clock.h"
// trap.c

void trap_handler(unsigned long scause, unsigned long sepc) {
if ((scause & 0x8000000000000000) && (scause & 0x7FFFFFFFFFFFFFFF) == 5) {
printk("[S] Supervisor Mode Timer Interrupt\n");
clock_set_next_event();

}
return;
}

需要注意,为了可以调用clock.c里面的函数,我们需要在外部创建一个clock.c的头文件clock.h

  • scause是64位寄存器,通过与操作提取最高位
  • 如果是s态中断,scause的低位应为000101

2-5 实现时钟中断相关函数

要求

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned long get_cycles() {
// 使用 rdtime 编写内联汇编,获取 time 寄存器中 (也就是mtime 寄存器 )的值并返回
# YOUR CODE HERE

}

void clock_set_next_event() {
// 下一次 时钟中断 的时间点
unsigned long next = get_cycles() + TIMECLOCK;

// 使用 sbi_ecall 来完成对下一次时钟中断的设置
# YOUR CODE HERE
}

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include"clock.h"
#include"sbi.h"


unsigned long TIMECLOCK = 10000000;

unsigned long get_cycles() {
unsigned long timer;
__asm__ volatile(
"rdtime %[timer]\n"
:[timer]"=r"(timer)
:
:"memory"
);
return timer;
}

void clock_set_next_event() {
sbi_set_timer(TIMECLOCK+get_cycles());
}

3 编译及测试

运行结果符合预期

4 思考题

4-1 通过查看 RISC-V Privileged Spec 中的 medelegmideleg 解释上面 MIDELEG 值的含义,如果实验中mideleg没有设定为正确的值结果会怎么样呢?

在RISC-V特权架构规范中,medeleg(Machine Exception Delegation Register)和mideleg(Machine Interrupt Delegation Register)是用于配置异常和中断的委托寄存器。它们允许将特定的异常和中断委托给低特权级别的处理模式(例如,委托给用户态)来处理。

mideleg寄存器用于配置需要委托给低特权级别处理的中断。每个中断对应于mideleg寄存器的一个位,如果相应的位被设置为1,则表示将该中断委托给低特权级别的处理模式。

  • 对于MIDELEG寄存器的值0x0000000000000222,可以将其转换为二进制形式:

0000 0000 0000 0000 0000 0000 0010 0010

每个位对应一个中断,从右到左进行编号。对于位为1的中断,表示将该中断委托给低特权级别的处理模式。

根据二进制表示,可以看出以下中断被委托给低特权级别的处理模式:

  1. 位2(从右到左,从0开始计数):软件中断(软中断)

  2. 位9:时钟中断

这意味着软件中断和时钟中断将被委托给低特权级别的处理模式来处理。

  • 对于MEDELEG寄存器的值0x000000000000b109,可以将其转换为二进制形式:

0000 0000 0000 0000 1011 0001 0000 1001

每个位对应一个异常,从右到左进行编号。同样,对于位为1的异常,表示将该异常委托给低特权级别的处理模式。

根据二进制表示,可以看出以下异常被委托给低特权级别的处理模式:

  1. 位0(从右到左,从0开始计数):指令地址对齐异常

  2. 位3:访问权限异常

  3. 位4:非法指令异常

  4. 位5:断点异常

  5. 位6:加载地址对齐异常

  6. 位7:存储地址对齐异常

  7. 位9:时钟中断

  8. 位11:用户态异常

  9. 位12:系统调用异常

这意味着这些异常将被委托给低特权级别的处理模式来处理。

如果在实验中未正确设置mideleg寄存器的值,将会导致中断在低特权级别的处理模式中无法被正确处理。可能会导致以下结果:

  1. 中断被忽略:如果没有正确委托中断给低特权级别的处理模式,那么当中断发生时,处理器可能不会响应中断请求,从而导致中断被忽略。
  2. 中断在高特权级别处理:如果中断没有正确委托给低特权级别的处理模式,处理器可能会在高特权级别(如机器态)下处理中断,而不是在低特权级别(如用户态)下处理。这可能会破坏系统的安全性和隔离性。
  3. 用户程序无法捕获中断:在某些情况下,用户程序可能需要捕获和处理特定的中断。如果中断未正确委托给用户态,用户程序将无法捕获中断并执行相应的处理操作。

4-2 机器启动后 time、cycle 寄存器分别是从 0 开始计时的吗,从 0 计时是否是必要的呢?(有关 mcycle 寄存器的内容可以参考手册)

make debug后用gdb链接,直接输出time和cycle寄存器中的值。(不要c

可以看到,time寄存器位0,cycle寄 器不为0 。

  • 对于time寄存器来说,他需要在最开始的时候归零来记录启动时间,所以从0计时是必要的
  • 对于cycle寄存器来说,从0开始计时没有必要。

4-3 阅读 The RISC-V Instruction Set Manual Volume I: Unprivileged ISA (V20191213) 中第 1.2 章节 RISC-V Software Execution Environments and Harts,谈谈如何在一台不支持乘除法指令扩展的处理器上执行乘除法指令。

(找到对应位置)在1.2中描述了中断的处理过程,在1.6中详细介绍了trap的具体分类。

在指令手册中介绍说,如果我们使用了一个没有被定义编码的指令,那么会导致fatal trap。

也就是说:如果我们使用了mul/div指令在不支持乘除法指令的处理器上,会在此处中断,进入到trap_handler中。此时非法指令的编码会被存储到 mtval。我们只需要在trap_handler中通过其他处理器支持的非特权指令模拟乘除法进行运算即可。

乘法类似于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 参数:a - 乘数, b - 被乘数
# 返回值:乘积存储在寄存器t0中

mul_func:
# 初始化结果为0
li t0, 0

# 迭代乘法
li t1, 0 # 乘数的位索引
loop:
slli t2, t1, 1 # 乘数的位索引左移1位
add t2, t2, a # 将被乘数加到左移后的乘数上

li t3, 1 # 用于检查乘数的最低位是否为1
and t3, t3, t2 # 将乘数与1进行按位与操作

beqz t3, skip # 如果乘数的最低位为0,则跳过累加步骤

add t0, t0, b # 将被乘数累加到结果上

skip:
slli a, a, 1 # 将乘数左移1位
srli t1, t1, 1 # 将位索引右移1位

bnez t1, loop # 如果位索引不为0,则继续循环

# 返回乘积
mv a0, t0
ret

除法类似于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 除法函数
# 参数:a - 被除数, b - 除数
# 返回值:商存储在寄存器t0中

div_func:
# 初始化商和余数为0
li t0, 0 # 商
li t1, 0 # 余数

# 迭代除法
li t2, 64 # 除数和商的位索引
loop:
slli t1, t1, 1 # 将余数左移1位

li t3, 1 # 用于检查被除数的最高位是否为1
and t3, t3, a # 将被除数与1进行按位与操作

or t1, t1, t3 # 将被除数的最高位加到余数上

blt t1, b, skip # 如果余数小于除数,则跳过减法步骤

sub t1, t1, b # 余数减去除数
li t3, 1 # 用于检查商的对应位是否为1
slli t3, t3, t2 # 将1左移对应的位索引
or t0, t0, t3 # 将1加到商的对应位上

skip:
srli t2, t2, 1 # 将位索引右移1位

bnez t2, loop # 如果位索引不为0,则继续循环

# 返回商
mv a0, t0
ret