uCore OS(on RISC-V64)——LAB1:中断机制

实验目的

  1. 了解CPU的中断机制
  2. 了解RISC-v架构是如何支持CPU中断的
  3. 掌握与软件相关的中断处理
  4. 掌握时钟中断管理

实验内容

  1. 跟着实验指导书理解lab1框架代码。
  2. 阅读RISC-V手册有关中断部分。
  3. 完成练习。
  4. 撰写并提交实验报告。

中断相关

寄存器

操作系统一般运行在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_handlerexception_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只恢复了其中的statusepc寄存器。这主要是因为badaddr寄存器和cause寄存器中保存的分别是出错的地址以及出错的原因,当我们处理完这个中断的时候,也就不需要这两个寄存器中保存的值,所以可以不用恢复这两个寄存器。

练习3:触发、捕获、处理异常

在trap.c中的根据cause寄存器进行例外的分类时,在illegal_intruction中输入

cprintf("illegal insttruction at 0x%016llx\n",tf->epc);
tf->epc += 2;

在这里插入图片描述
表明例外的类型和发生例外的地址
在init.c中使用内联汇编使用mret函数即会触发这个例外
在这里插入图片描述
在终端运行,查看结果:
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alfred young

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值