Lec4 中断


RISC-V关键寄存器

RISC-V中内核态寄存器:

  • SIE(Supervisor Interrupt Enable):是否允许类型中断
  • SIP(Supervisor Interrupt Pending):当发生中断时,处理器查看这个寄存器知道当前发生的中断类型
  • SSTATUS(Supervisor Status):1bit,开/关中断

每个CPU都有独立的SIE和SSTATUS寄存器,除了通过SIE单独控制特定的中断,还可以通过SSTATUS控制所有的中断

  • SATP:页表地址
  • STVEC:保存中断向量的地址,是uservec/kernelvec取决于发生中断时程序运行在用户态还是内核态
  • SSCRATCH:保存进程trapframe页的虚拟地址
  • SEPC:发生中断时程序地址置,中断执行完后返回该位置继续执行
  • SCAUSE:发生中断的原因

trap

xv6 会在以下情形进入usertrap:系统调用、用户指令错误、应用程序运行时发生中断(外部设备中断/软中断)

xv6 会在以下情形进入kerneltrap:内核指令错误、内核运行时发生中断(外部设备中断/软中断)

进入trap

fork系统调用为例,用户态程序执行fork系统调用,实际执行如下指令:

  1. 向a7寄存器写入系统调用编号
  2. 执行ecall指令,该指令在硬件层面完成:切换至内核态 + 交换关键寄存器的值(PC→SEPC,STVEC→PC)
.global fork
fork:
 li a7, SYS_fork
 ecall
 ret

STVEC寄存器事先存储了uservec中断向量的地址,交换后PC寄存器指向uservec,此时系统处于内核态但未切换至内核页表,完成以下操作:

  1. 保存用户态上下文至trapframe,包括关键寄存器(SP/SATP) + 32个通用寄存器
  2. 恢复保存在trapframe的内核态上下文:SATP-内核根页表地址/SP-内核栈地址
  3. 跳转至usertrap

trampoline.asm/uservec

# trap.c sets stvec to point here, so traps from user space start here,
# in supervisor mode, but with a user page table.
.globl uservec
uservec:    
        # sscratch stores the address of process's trapframe virtual address
 # swap a0 and sscratch, so that a0 is TRAPFRAME
        csrrw a0, sscratch, a0
        # save the user registers in TRAPFRAME
        sd ra, 40(a0)
        sd sp, 48(a0)
        sd gp, 56(a0)

 # save the user a0 in p->trapframe->a0
        csrr t0, sscratch
        sd t0, 112(a0)
        # restore kernel stack pointer from p->trapframe->kernel_sp
        ld sp, 8(a0)
        # make tp hold the current hartid, from p->trapframe->kernel_hartid
        ld tp, 32(a0)
        # load the address of usertrap(), p->trapframe->kernel_trap
        ld t0, 16(a0)
        # restore kernel page table from p->trapframe->kernel_satp
        ld t1, 0(a0)
        csrw satp, t1
        sfence.vma zero, zero

        # jump to usertrap(), which does not return
        jr t0

从trap返回

usertrap最后调用usertrapret返回用户态,usertrapret硬件返回用户态userret

trampoline.asm/userret:返回至用户态时的上下文恢复操作

.globl userret
userret:
        # userret(TRAPFRAME, pagetable)
        # switch from kernel to user.
        # usertrapret() calls here.
        # a0: TRAPFRAME, in user page table.
        # a1: user page table, for satp.

        # switch to the user page table.
        csrw satp, a1
        sfence.vma zero, zero

        # put the saved user a0 in sscratch, so we
        # can swap it with our a0 (TRAPFRAME) in the last step.
        ld t0, 112(a0)
        csrw sscratch, t0

        # restore all but a0 from TRAPFRAME
        ld ra, 40(a0)
        ld sp, 48(a0)
        ld gp, 56(a0)
        ld tp, 64(a0)
        # ...

 # restore user a0, and save TRAPFRAME in sscratch
        csrrw a0, sscratch, a0
        
        # return to user mode and user pc.
        # usertrapret() set up sstatus and sepc.
        sret

补充:系统调用进入内核态后的流程(syscall)

  • 获取系统调用号:trapframe->a7

  • 从系统调用函数数组获取对应系统调用函数的指针

  • 获取系统调用参数:与普通函数调用存入栈帧不同,syscall 从 trapframe 获取系统调用参数

  • 返回系统调用结果:在trapframe->a0中记录返回值

syscall.c - syscall:

extern uint64 sys_fork(void);
extern uint64 sys_exit(void);
extern uint64 sys_wait(void);
// .....

// 系统调用指针
static uint64 (*syscalls[])(void) = {
    [SYS_fork]    sys_fork,
    [SYS_exit]    sys_exit,
    [SYS_wait]    sys_wait,
    // .....
};

void syscall(void) {
  int num;
  struct proc *p = myproc();

  num = p->trapframe->a7;
  if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    p->trapframe->a0 = syscalls[num]();
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

补充:指令错误

指令错误可参考寄存器:

  • SCAUSE:错误类型

  • SEPC:造成错误的指令的虚拟地址

  • STVAL:对于page fault,表明造成错误的虚拟地址

usertrap

kerneltrap

内核线程调度前,保存如下数据:

  • kernel_satp:存储内核页表的物理地址,切换至内核态时存储在SATP
  • kernel_sp:存储该进程的内核栈的地址,切换至内核态时存储在SP
  • kernel_trap:存储中断处理程序kerneltrap的物理地址,kernelvec 后直接跳转至该函数

基于Page fault 附加功能

Copy on write fork

使用fork 系统调用时,先创建4个新的page,再将父进程page的内容拷贝至4个新的分配给子进程的page中,之后exec会释放这些page,并分配新的page来包含echo相关的内容,原先创建的4个新的page

创建子进程时,与其创建分配拷贝内容到新的物理内存,不如直接共享父进程的物理内存page并设置子进程的PTE指向父进程对应的物理内存page,但父进程和子进程的PTE的标志位都设置成只读的。

父进程和子进程继续运行,当父进程或子进程执行store指令更新一些全局变量时会触发page fault,因为现在在向一个只读PTE写数据。

在得到page fault之后,先分配一个新的page,然后将父进程相应的page拷贝到新page,并将新page映射到子进程的页表中。这时,新分配的page只对子进程可见,相应的PTE设置成可读写,并且重新执行指令。对于触发page fault对应的物理page,因为现在只对父进程可见,相应的PTE对于父进程也变成可读写的了。

释放:当我们释放page时,我们将物理内存page的引用数减1。只有引用数为0时释放物理内存page

Least Recently Used

当系统发生OOM,将部分页中的内容写回到文件系统再收回这个page,回收哪些page:access位为0,回收最近未被访问过的page。此外,dirty位为0,表明当前page被修改过


文章作者: AthenaCrafter
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 AthenaCrafter !
  目录