实验目的
- 了解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; // Hard-wired zero
uintptr_t ra; // Return address
uintptr_t sp; // Stack pointer
uintptr_t gp; // Global pointer
uintptr_t tp; // Thread pointer
uintptr_t t0; // Temporary
uintptr_t t1; // Temporary
uintptr_t t2; // Temporary
uintptr_t s0; // Saved register/frame pointer
uintptr_t s1; // Saved register
uintptr_t a0; // Function argument/return value
uintptr_t a1; // Function argument/return value
uintptr_t a2; // Function argument
uintptr_t a3; // Function argument
uintptr_t a4; // Function argument
uintptr_t a5; // Function argument
uintptr_t a6; // Function argument
uintptr_t a7; // Function argument
uintptr_t s2; // Saved register
uintptr_t s3; // Saved register
uintptr_t s4; // Saved register
uintptr_t s5; // Saved register
uintptr_t s6; // Saved register
uintptr_t s7; // Saved register
uintptr_t s8; // Saved register
uintptr_t s9; // Saved register
uintptr_t s10; // Saved register
uintptr_t s11; // Saved register
uintptr_t t3; // Temporary
uintptr_t t4; // Temporary
uintptr_t t5; // Temporary
uintptr_t t6; // Temporary
};
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模式
中断处理程序
初始化
// kern/init/init.c
#include <trap.h>
int kern_init(void) {
extern char edata[], end[];
memset(edata, 0, end - edata);
cons_init(); // init the console
const char *message = "(THU.CST) os is loading ...\n";
cprintf("%s\n\n", message);
print_kerninfo();
// grade_backtrace();
//trap.h的函数,初始化中断
idt_init(); // init interrupt descriptor table
//clock.h的函数,初始化时钟中断
clock_init();
//intr.h的函数,使能中断
intr_enable();
// LAB1: CAHLLENGE 1 If you try to do it, uncomment lab1_switch_test()
// user/kernel mode switch test
// lab1_switch_test();
/* do nothing */
while (1)
;
}
// kern/trap/trap.c
void idt_init(void) {
extern void __alltraps(void);
//约定:若中断前处于S态,sscratch为0
//若中断前处于U态,sscratch存储内核栈地址
//那么之后就可以通过sscratch的数值判断是内核态产生的中断还是用户态产生的中断
//我们现在是内核态所以给sscratch置零
write_csr(sscratch, 0);
//我们保证__alltraps的地址是四字节对齐的,将__alltraps这个符号的地址直接写到stvec寄存器
write_csr(stvec, &__alltraps);
}
//kern/driver/intr.c
#include <intr.h>
#include <riscv.h>
/* intr_enable - enable irq interrupt, 设置sstatus的Supervisor中断使能位 */
void intr_enable(void) { set_csr(sstatus, SSTATUS_SIE); }
/* intr_disable - disable irq interrupt */
void intr_disable(void) { clear_csr(sstatus, SSTATUS_SIE); }
在原来的init.c的基础上加入了
idt_init:初始化中断向量表
clock_init:初始化时钟中断
intr_enable:使能中断
处理
// kern/trap/trap.c
/* trap_dispatch - dispatch based on what type of trap occurred */
static inline void trap_dispatch(struct trapframe *tf) {
//scause的最高位是1,说明trap是由中断引起的
if ((intptr_t)tf->cause < 0) {
// interrupts
interrupt_handler(tf);
} else {
// exceptions
exception_handler(tf);
}
}
/* *
* trap - handles or dispatches an exception/interrupt. if and when trap()
* returns,
* the code in kern/trap/trapentry.S restores the old CPU state saved in the
* trapframe and then uses the iret instruction to return from the exception.
* */
void trap(struct trapframe *tf) { trap_dispatch(tf); }
根据RISC-V的scause寄存器的格式,如果最高位是1是中断;如果最高位是0是异常,根据分类的结果交给函数interrupt_handler
和exception_handler
分别处理。
时钟中断
//libs/sbi.c
//当time寄存器(rdtime的返回值)为stime_value的时候触发一个时钟中断
void sbi_set_timer(unsigned long long stime_value) {
sbi_call(SBI_SET_TIMER, stime_value, 0, 0);
}
// kern/driver/clock.c
#include <clock.h>
#include <defs.h>
#include <sbi.h>
#include <stdio.h>
#include <riscv.h>
//volatile告诉编译器这个变量可能在其他地方被瞎改一通,所以编译器不要对这个变量瞎优化
volatile size_t ticks;
//对64位和32位架构,读取time的方法是不同的
//32位架构下,需要把64位的time寄存器读到两个32位整数里,然后拼起来形成一个64位整数
//64位架构简单的一句rdtime就可以了
//__riscv_xlen是gcc定义的一个宏,可以用来区分是32位还是64位。
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
}
// Hardcode timebase
static uint64_t timebase = 100000;
void clock_init(void) {
// sie这个CSR可以单独使能/禁用某个来源的中断。默认时钟中断是关闭的
// 所以我们要在初始化的时候,使能时钟中断
set_csr(sie, MIP_STIP); // enable timer interrupt in sie
//设置第一个时钟中断事件
clock_set_next_event();
// 初始化一个计数器
ticks = 0;
cprintf("++ setup timer interrupts\n");
}
//设置时钟中断:timer的数值变为当前时间 + timebase 后,触发一次时钟中断
//对于QEMU, timer增加1,过去了10^-7 s, 也就是100ns
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触发一次时钟中断。
// kern/trap/trap.c
#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) {
/* blabla 其他case*/
case IRQ_S_TIMER:
clock_set_next_event();//发生这次时钟中断的时候,我们要设置下一次时钟中断
if (++ticks % TICK_NUM == 0) {
print_ticks();
}
break;
/* blabla 其他case*/
}
每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函数即会触发这个例外
在终端运行,查看结果: