实验目的
- 了解CPU的中断机制
- 了解RISC-v架构是如何支持CPU中断的
- 掌握与软件相关的中断处理
- 掌握时钟中断管理
实验内容
- 跟着实验指导书理解lab1框架代码。
- 阅读RISC-V手册有关中断部分。
- 完成练习。
- 撰写并提交实验报告。
中断相关
寄存器
操作系统一般运行在RISC-V特权模式下的S模式,这个模式具有的CSR有
名称 | 功能 |
---|
sepc | 指向发生异常的指令 | stvec | 保存发生异常时跳转到的地址 | scause | 指向发生异常的种类 | sscratch | 暂时存放一个字大小的数据 | stval | 保存了陷入的附加信息 | sstatus | 保存全局中断使能 |
特权指令
ecall :通过引发环境调用异常来请求执行环境 ebreak :通过抛出断点异常的方式来请求执行环境 sret :管理员模式例外返回,从管理员模式的例外处理程序中返回 mret :机器模式异常返回,从机器模式异常处理程序返回
上下文处理
中断处理要求执行完中断后寄存器能够恢复为执行中断前的现场。 因此上下文处理就是:
- 将CPU的寄存器(上下文)保存到内存(栈上)。
- 将内存(栈上)恢复到CPU的寄存器(上下文)。
RISC-V用到的寄存器有32个通用寄存器和4个控制状态寄存器, 用结构题将这些寄存器加以组织。
struct pushregs {
uintptr_t zero;
uintptr_t ra;
uintptr_t sp;
uintptr_t gp;
uintptr_t tp;
uintptr_t t0;
uintptr_t t1;
uintptr_t t2;
uintptr_t s0;
uintptr_t s1;
uintptr_t a0;
uintptr_t a1;
uintptr_t a2;
uintptr_t a3;
uintptr_t a4;
uintptr_t a5;
uintptr_t a6;
uintptr_t a7;
uintptr_t s2;
uintptr_t s3;
uintptr_t s4;
uintptr_t s5;
uintptr_t s6;
uintptr_t s7;
uintptr_t s8;
uintptr_t s9;
uintptr_t s10;
uintptr_t s11;
uintptr_t t3;
uintptr_t t4;
uintptr_t t5;
uintptr_t t6;
};
struct trapframe {
struct pushregs gpr;
uintptr_t status;
uintptr_t epc;
uintptr_t badvaddr;
uintptr_t cause;
};
然后将上下文(也就是一个trapframe)保存在内存中。
.macro SAVE_ALL
csrw sscratch, sp
addi sp, sp, -36 * REGBYTES
# save x registers
STORE x0, 0*REGBYTES(sp)
STORE x1, 1*REGBYTES(sp)
STORE x3, 3*REGBYTES(sp)
STORE x4, 4*REGBYTES(sp)
STORE x5, 5*REGBYTES(sp)
STORE x6, 6*REGBYTES(sp)
STORE x7, 7*REGBYTES(sp)
STORE x8, 8*REGBYTES(sp)
STORE x9, 9*REGBYTES(sp)
STORE x10, 10*REGBYTES(sp)
STORE x11, 11*REGBYTES(sp)
STORE x12, 12*REGBYTES(sp)
STORE x13, 13*REGBYTES(sp)
STORE x14, 14*REGBYTES(sp)
STORE x15, 15*REGBYTES(sp)
STORE x16, 16*REGBYTES(sp)
STORE x17, 17*REGBYTES(sp)
STORE x18, 18*REGBYTES(sp)
STORE x19, 19*REGBYTES(sp)
STORE x20, 20*REGBYTES(sp)
STORE x21, 21*REGBYTES(sp)
STORE x22, 22*REGBYTES(sp)
STORE x23, 23*REGBYTES(sp)
STORE x24, 24*REGBYTES(sp)
STORE x25, 25*REGBYTES(sp)
STORE x26, 26*REGBYTES(sp)
STORE x27, 27*REGBYTES(sp)
STORE x28, 28*REGBYTES(sp)
STORE x29, 29*REGBYTES(sp)
STORE x30, 30*REGBYTES(sp)
STORE x31, 31*REGBYTES(sp)
# get sr, epc, badvaddr, cause
# Set sscratch register to 0, so that if a recursive exception
# occurs, the exception vector knows it came from the kernel
csrrw s0, sscratch, x0
csrr s1, sstatus
csrr s2, sepc
csrr s3, sbadaddr
csrr s4, scause
STORE s0, 2*REGBYTES(sp)
STORE s1, 32*REGBYTES(sp)
STORE s2, 33*REGBYTES(sp)
STORE s3, 34*REGBYTES(sp)
STORE s4, 35*REGBYTES(sp)
.endm
在上述汇编中,首先将sp寄存器的值保存在sscratch寄存器中,然后使sp寄存器向低地址生长了36个寄存器长度,用于分别保存32个通用寄存器和4个CSR。
.macro RESTORE_ALL
LOAD s1, 32*REGBYTES(sp)
LOAD s2, 33*REGBYTES(sp)
csrw sstatus, s1
csrw sepc, s2
# restore x registers
LOAD x1, 1*REGBYTES(sp)
LOAD x3, 3*REGBYTES(sp)
LOAD x4, 4*REGBYTES(sp)
LOAD x5, 5*REGBYTES(sp)
LOAD x6, 6*REGBYTES(sp)
LOAD x7, 7*REGBYTES(sp)
LOAD x8, 8*REGBYTES(sp)
LOAD x9, 9*REGBYTES(sp)
LOAD x10, 10*REGBYTES(sp)
LOAD x11, 11*REGBYTES(sp)
LOAD x12, 12*REGBYTES(sp)
LOAD x13, 13*REGBYTES(sp)
LOAD x14, 14*REGBYTES(sp)
LOAD x15, 15*REGBYTES(sp)
LOAD x16, 16*REGBYTES(sp)
LOAD x17, 17*REGBYTES(sp)
LOAD x18, 18*REGBYTES(sp)
LOAD x19, 19*REGBYTES(sp)
LOAD x20, 20*REGBYTES(sp)
LOAD x21, 21*REGBYTES(sp)
LOAD x22, 22*REGBYTES(sp)
LOAD x23, 23*REGBYTES(sp)
LOAD x24, 24*REGBYTES(sp)
LOAD x25, 25*REGBYTES(sp)
LOAD x26, 26*REGBYTES(sp)
LOAD x27, 27*REGBYTES(sp)
LOAD x28, 28*REGBYTES(sp)
LOAD x29, 29*REGBYTES(sp)
LOAD x30, 30*REGBYTES(sp)
LOAD x31, 31*REGBYTES(sp)
# restore sp last
LOAD x2, 2*REGBYTES(sp)
#addi sp, sp, 36 * REGBYTES
.endm
恢复上下文,只需要将CSR中的sstatus寄存器和sepc寄存器恢复,其余CSR不用恢复。 中断入口:
.globl __alltraps
.align(2)
__alltraps:
SAVE_ALL #保存上下文
move a0, sp #传递参数
jal trap #中断处理程序
# sp should be the same as before "jal trap"
.globl __trapret
__trapret:
RESTORE_ALL
# return from supervisor call
sret #从s模式下返回u模式
中断处理程序
初始化
#include <trap.h>
int kern_init(void) {
extern char edata[], end[];
memset(edata, 0, end - edata);
cons_init();
const char *message = "(THU.CST) os is loading ...\n";
cprintf("%s\n\n", message);
print_kerninfo();
idt_init();
clock_init();
intr_enable();
while (1)
;
}
void idt_init(void) {
extern void __alltraps(void);
write_csr(sscratch, 0);
write_csr(stvec, &__alltraps);
}
#include <intr.h>
#include <riscv.h>
void intr_enable(void) { set_csr(sstatus, SSTATUS_SIE); }
void intr_disable(void) { clear_csr(sstatus, SSTATUS_SIE); }
在原来的init.c的基础上加入了 idt_init:初始化中断向量表 clock_init:初始化时钟中断 intr_enable:使能中断
处理
static inline void trap_dispatch(struct trapframe *tf) {
if ((intptr_t)tf->cause < 0) {
interrupt_handler(tf);
} else {
exception_handler(tf);
}
}
void trap(struct trapframe *tf) { trap_dispatch(tf); }
根据RISC-V的scause寄存器的格式,如果最高位是1是中断;如果最高位是0是异常,根据分类的结果交给函数interrupt_handler 和exception_handler 分别处理。
时钟中断
void sbi_set_timer(unsigned long long stime_value) {
sbi_call(SBI_SET_TIMER, stime_value, 0, 0);
}
#include <clock.h>
#include <defs.h>
#include <sbi.h>
#include <stdio.h>
#include <riscv.h>
volatile size_t ticks;
static inline uint64_t get_time(void) {
#if __riscv_xlen == 64
uint64_t n;
__asm__ __volatile__("rdtime %0" : "=r"(n));
return n;
#else
uint32_t lo, hi, tmp;
__asm__ __volatile__(
"1:\n"
"rdtimeh %0\n"
"rdtime %1\n"
"rdtimeh %2\n"
"bne %0, %2, 1b"
: "=&r"(hi), "=&r"(lo), "=&r"(tmp));
return ((uint64_t)hi << 32) | lo;
#endif
}
static uint64_t timebase = 100000;
void clock_init(void) {
set_csr(sie, MIP_STIP);
clock_set_next_event();
ticks = 0;
cprintf("++ setup timer interrupts\n");
}
void clock_set_next_event(void) { sbi_set_timer(get_time() + timebase); }
在clock.c中封装着一个gettime函数对于64位系统可以直接读取,对于32位系统需要分成两个32位整数读取time寄存器的值然后拼接。
然后在clock_init 函数中需要首先将sie寄存器中的时钟使能信号打开,然后设置一个时钟中断信息,并设定timebase = 100000,对于QEMU,模拟出来CPU的主频是10MHz,每个时钟周期也就是100ns,达到timebase共需要10ms,即10ms触发一次时钟中断。
#include<clock.h>
#define TICK_NUM 100
static void print_ticks() {
cprintf("%d ticks\n", TICK_NUM);
#ifdef DEBUG_GRADE
cprintf("End of Test.\n");
panic("EOT: kernel seems ok.");
#endif
}
void interrupt_handler(struct trapframe *tf) {
intptr_t cause = (tf->cause << 1) >> 1;
switch (cause) {
case IRQ_S_TIMER:
clock_set_next_event();
if (++ticks % TICK_NUM == 0) {
print_ticks();
}
break;
}
每100次时钟中断打印一次信息,也就是每1s打印一次100 ticks。
执行流
内核的执行流为: 加电 -> OpenSBI启动 -> 跳转到 0x80200000 (kern/init/entry.S)->进入kern_init()函数(kern/init/init.c) ->调用cprintf()输出一行信息->调用print_kerninfo()打印内核信息->调用idt_init(),初始化sscratch和stvec寄存器->调用clock_init()初始化时钟中断->初始化使能中断->结束
时钟中断的执行流为: 调用clock_init()函数中->调用set_csr()函数将sie中的时钟中断使能打开->调用sbi_set_timer()函数,在time达到timebase时发生中断,进入中断入口->先保存现场,然后通过tail指令进入trap.c执行trap_dispatch()函数->恢复现场->结束。
练习
练习1:描述处理中断异常的流程
以时钟中断为例,调用clock_init()函数中->调用set_csr()函数将sie中的时钟中断使能打开->调用sbi_set_timer()函数,在time达到timebase时发生中断,进入中断入口->先保存现场,然后通过tail指令进入trap.c执行trap_dispatch()函数->恢复现场->结束。
练习2:对于任何中断,都需要保存所有寄存器吗?为什么?
不需要,在恢复上下文的代码中,我们可以看到在恢复现场的时候,对于控制状态寄存器的四个寄存器status ,epc ,badaddr ,cause 只恢复了其中的status 和epc 寄存器。这主要是因为badaddr寄存器和cause寄存器中保存的分别是出错的地址以及出错的原因,当我们处理完这个中断的时候,也就不需要这两个寄存器中保存的值,所以可以不用恢复这两个寄存器。
练习3:触发、捕获、处理异常
在trap.c中的根据cause寄存器进行例外的分类时,在illegal_intruction中输入
cprintf("illegal insttruction at 0x%016llx\n",tf->epc);
tf->epc += 2;
表明例外的类型和发生例外的地址 在init.c中使用内联汇编使用mret函数即会触发这个例外 在终端运行,查看结果:
|