Mit 6.s081 lab4 traps

【投稿赢 iPhone 17】「我的第一个开源项目」故事征集:用代码换C位出道! 10w+人浏览 1.6k人参与

一、 trap基本原理的机制

什么是traps?

traps在计算机的不同领域有着不同的含义,在操作系统领域,os是一种中断机制,当cpu在运行过程中,出现某种特定条件时,如系统调用、除零等非法操作、访问越界,就会触发traps,traps的功能是从用户态切换到内核态,以便获取更高的权限处理这些特定情况。

用更通俗的话讲traps就是cpu为了处理异常情况而设计的。
cpu在执行过程中发生控制流程中断的情况主要有三种

  1. system call
  2. exception
  3. device interrupt

在xv6中使用trap代指上面三种情况总和。

risc-v通过一些控制寄存器来处理traps。

寄存器名称作用
stvec存储trap hander地址,risc-v跳转到stvec中存储的函数处理trap
sepc保存当前的pc内容(后续需要处理trap,pc中的内容会被替换位stvec),用于处理完trap后返回sret返回
scausetrap发生的原因上面三种情况中的某种
sscratch内核在切换到某个进程前,会把这个进程的内核栈地址写进 sscratch。当这个进程触发系统调用或中断时,CPU 一跳进陷阱处理代码,第一条指令就去读 sscratch。读到的是“内核栈地址”,立刻把栈指针 sp 切换过去 → 安全保存现场 → 调用 C 代码。
sstatus状态寄存器其中的SIE位主要控制是否能被设备中断,同时SPP位标识是supervisor mode还是user mode这决定了sret返回那种mode

上面是supervisor mode下的寄存器,还有machine mode 下的用于专门处理时钟中断。
多核cpu的处理器,每个cpu都会有一组自己的这样的处理器,在一个给定的时间内,可能有多个cpu在处理trap。

risc-v的trap处理的硬件行为:

  1. 当trap来临,判断是否是中断并且sstatus寄存器中SIE位是否为0,为0说明中断关闭跳过后面所有步骤,反之,则继续执行后续步骤。
  2. 无论trap的类型是否为中断,都需要关闭中断即设置sstatus的SIE位为0。
  3. 保存当前的pc到sepc。
  4. 将当前cpu运行的mode(user mode or supvisor mode)保存到sstatus的SPP位。
  5. 设置scause。
  6. 切换cpu mode为supervisor mode。
  7. 将stvec的内容复制到pc。
  8. 然后开始执行pc中的指令。

需要注意上面这部分内容都是硬件进行的,cpu从用户态到内核态的切换是在最前面,也是由硬件完成的。

在这里插入图片描述

还有需要注意的是cpu硬件不会切换内核页表,也不会切换栈,更不会保存除了pc以外的任何寄存器。这些都是通过软件进行实现的。
这样设计的目的是让cpu硬件做最少的工作,给软件设计提供灵活性。

二、 traps from kernel space 用户空间的traps

需要明确的一点是在用户空间和内核空间处理traps的方式有所区别
从宏观的角度看用户空间traps处理的流程如下:


xv6中页表切换是由软件实现的,这里会有一个问题,当trap发生时,cpu从用户核态进入内核态,同时调到stvec中的内容,然后开始执行trap handler中的内容,由于xv6硬件不会切换页表,于是需要trap handler实现页表切换,注意此时执行页表切换的代码是在用户页表中执行的,当执行完切换页表操作后,satp换成了内核态的页表,这时trap handler仍要继续执行,这就会出现问题,同一份代码的页表映射不同。

为了解决这个问题xv6操作系统采用trampoline机制。

  • 操作系统在内核空间的物理地址中为trampoline分配了一个物理页面存放trampoline.S代码
  • 采用一种双重映射,在内核页表中,将物理页面映射到高虚拟地址,称之为TRAMPOLINE_ADDR,在每个进程(process)的用户页表中,也将同一物理页面映射到完全相同的虚拟地址TRAMPOLINE_ADDR

在这里插入图片描述
cpu处理完控制寄存器后将pc指向uservec,这时,risc-v的32个通用寄存器还存有被中断的用户代码中的值,为了实现透明性(无感知),我们需要将这些寄存器中的内容存入内存(memory)。但是32个通用寄存器已经耗尽了,这里没有其他通用寄存器供我们使用了。
这里有两个问题?

  1. 用那个寄存器传递通用寄存器中的内容到内存(memory)?
  2. 把通用寄存器中的内容传递到哪里?

这里risc-v提供了一个寄存器sscratch供我们使用来进行寄存器(register)->内存(memory)的操作,寄存器sscratch中存储了一个内核空间的地址指向了一个为进程分配的保存上下文的trapframe。通过csrrw这个汇编指令交换,sscratcha0寄存器中内容进行了交换,a0中就有了目的地址,接就是将通用寄存器中的内容存储到trapframe
trapframe结构体如下:

// per-process data for the trap handling code in trampoline.S.
// sits in a page by itself just under the trampoline page in the
// user page table. not specially mapped in the kernel page table.
// the sscratch register points here.
// uservec in trampoline.S saves user registers in the trapframe,
// then initializes registers from the trapframe's
// kernel_sp, kernel_hartid, kernel_satp, and jumps to kernel_trap.
// usertrapret() and userret in trampoline.S set up
// the trapframe's kernel_*, restore user registers from the
// trapframe, switch to the user page table, and enter user space.
// the trapframe includes callee-saved user registers like s0-s11 because the
// return-to-user path via usertrapret() doesn't return through
// the entire kernel call stack.
struct trapframe {
  /*   0 */ uint64 kernel_satp;   // kernel page table
  /*   8 */ uint64 kernel_sp;     // top of process's kernel stack
  /*  16 */ uint64 kernel_trap;   // usertrap()
  /*  24 */ uint64 epc;           // saved user program counter
  /*  32 */ uint64 kernel_hartid; // saved kernel tp
  /*  40 */ uint64 ra;
  /*  48 */ uint64 sp;
  /*  56 */ uint64 gp;
  /*  64 */ uint64 tp;
  /*  72 */ uint64 t0;
  /*  80 */ uint64 t1;
  /*  88 */ uint64 t2;
  /*  96 */ uint64 s0;
  /* 104 */ uint64 s1;
  /* 112 */ uint64 a0;
  /* 120 */ uint64 a1;
  /* 128 */ uint64 a2;
  /* 136 */ uint64 a3;
  /* 144 */ uint64 a4;
  /* 152 */ uint64 a5;
  /* 160 */ uint64 a6;
  /* 168 */ uint64 a7;
  /* 176 */ uint64 s2;
  /* 184 */ uint64 s3;
  /* 192 */ uint64 s4;
  /* 200 */ uint64 s5;
  /* 208 */ uint64 s6;
  /* 216 */ uint64 s7;
  /* 224 */ uint64 s8;
  /* 232 */ uint64 s9;
  /* 240 */ uint64 s10;
  /* 248 */ uint64 s11;
  /* 256 */ uint64 t3;
  /* 264 */ uint64 t4;
  /* 272 */ uint64 t5;
  /* 280 */ uint64 t6;
};

其中通用寄存器有:

字段名说明
kernel_sp对应通用寄存器中的 sp(栈指针)。
ra, sp, gp, tp, t0-t6, s0-s11, a0-a7全部是通用寄存器,直接对应 RISC-V 规范的寄存器。

非通用寄存器有:

字段名说明
kernel_satp特定的 控制状态寄存器satp)。
kernel_trap保存异常处理函数地址,不是寄存器。
epc特定的 控制状态寄存器sepc)。
kernel_hartid一个字段值,代表硬件 Hart ID,通过访问 mhartid CSR 获得。

在保存完通用寄存器内容后将一些其他寄存器内容、cpu_id、usertrap、还有内核页表地址加载到特定寄存器,uservec开始切换页表,将用户页表切换为内核页表。

下面是关于uservec部分的代码解读
首先先明确trampoline的含义,trampoline 是指蹦床、跳板,那么对于操作系统而言,它的蹦床、跳板是用来帮助操作系统在内核态和用户态之间进行切换的,那么这个词就很形象了。
对于这个文件的代码我们可以分成两部分来看。

uservec

其中

.globl uservec

这是一条risc-v的汇编伪指令,用来声明uservec是全局的,可以被其他模块、程序用,汇编伪指令指的是只在汇编阶段起作用,不会被编译前编译为机器指令。

uservec:    
	#
        # trap.c sets stvec to point here, so
        # traps from user space start here,
        # in supervisor mode, but with a
        # user page table.
        #
        # sscratch points to where the process's p->trapframe is
        # mapped into user space, at TRAPFRAME.
        #
        
	# swap a0 and sscratch
        # so that a0 is TRAPFRAME
        csrrw a0, sscratch, a0

csrrw指令就是交换两个寄存器的内容,这里就是交换了a0和sscratch中的内容。

        # save the user registers in TRAPFRAME
        sd ra, 40(a0)
        sd sp, 48(a0)
        sd gp, 56(a0)
        sd tp, 64(a0)
        sd t0, 72(a0)
        sd t1, 80(a0)
        sd t2, 88(a0)
        sd s0, 96(a0)
        sd s1, 104(a0)
        sd a1, 120(a0)
        sd a2, 128(a0)
        sd a3, 136(a0)
        sd a4, 144(a0)
        sd a5, 152(a0)
        sd a6, 160(a0)
        sd a7, 168(a0)
        sd s2, 176(a0)
        sd s3, 184(a0)
        sd s4, 192(a0)
        sd s5, 200(a0)
        sd s6, 208(a0)
        sd s7, 216(a0)
        sd s8, 224(a0)
        sd s9, 232(a0)
        sd s10, 240(a0)
        sd s11, 248(a0)
        sd t3, 256(a0)
        sd t4, 264(a0)
        sd t5, 272(a0)
        sd t6, 280(a0)

保存30个通用寄存器的内容到trapframe

	# save the user a0 in p->trapframe->a0
        csrr t0, sscratch
        sd t0, 112(a0)

将a0中的内容也保存到trapframe中

        # 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)

加载用户进程的sp(内核栈指针)、tp(用户进程的硬件线程)、t0(usertrap()函数地址)、t1(加载内核页表到t1寄存器),ld(Load Doubleword)加载指令 ld rd, offset(rs1)

        csrw satp, t1
        sfence.vma zero, zero

csrw是读一个寄存器中的内容,并将内容写进控制寄存器
sfence.vma zero, zero是刷新页表的命令

        # a0 is no longer valid, since the kernel page
        # table does not specially map p->tf.

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

t0中这是存储的已经是usertrap()函数的入口地址了,jr是 Jump Register(跳转到寄存器地址),属于跳转指令。

总结一下:
uservec主要的作用:保存通用寄存器或者说用户进程的上下文到trapframe,同时切换内核页表,并跳转到用户trap处理程序usertrap()处理trap。

usertrap()

然后uservec就跳到uesrtrap()部分开始执行。

usertrap()的主要功能是确定引起trap的原因处理trap、并返回

  if((r_sstatus() & SSTATUS_SPP) != 0)
    panic("usertrap: not from user mode");

判断是否合法,因为处理usertrap,必须是在用户空间时发生了trap才会到usertrap这个逻辑,其中r_sstatus()的作用就是读取sstatus寄存器的内容,SSTATUS_SPP这一位标识着进入内核态之前cpu是什么状态,如果SSTATUS_SPP=0标识着进入内核态之前是用户态,SSTATUS_SPP=1标识着进入内核态之前是内核态。可以据此判断状态是否合法。

  // send interrupts and exceptions to kerneltrap(),
  // since we're now in the kernel.
  w_stvec((uint64)kernelvec);

修改控制寄存器stvec中的内容,使其指向kernelvec,进行这一步的原因是为了防止嵌套trap时出错,假如我们已经处理了一个trap,这是cpu已经处于内核态,但是在当前trap尚未完成时,又来一个trap,那么这时stvec的指向仍然是uservec,这是会执行上面的逻辑去判断是否是从用户态进入内核态的,但显然是从内核态进入内核态的于是就会出错,所以这里在进入usertrap()之后就将stvec指向kernelvec

这里还有一个需要注意的点是,在我们正在进行w_stvec((uint64)kernelvec);时,来了一个时钟中断那么将会出错,这是xv6不允许的,risv-v硬件会在进入trap时(usertrap()之前)关中断

  struct proc *p = myproc();
  
  // save user program counter.
  p->trapframe->epc = r_sepc();

这里是将陷入trap前的pc保存到trapframe中

if(r_scause() == 8){
    // system call

    if(p->killed)
      exit(-1);

    // sepc points to the ecall instruction,
    // but we want to return to the next instruction.
    p->trapframe->epc += 4;

    // an interrupt will change sstatus &c registers,
    // so don't enable until done with those registers.
    intr_on();

    syscall();
  } else if((which_dev = devintr()) != 0){
    // ok
  } else {
    printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
    printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
    p->killed = 1;
  }

这段代码的逻辑是判断引起trap的原因,如果r_scause()==8就表示是系统调用,如果是系统调用的话就需要从ecall下一条指令接着执行,所以这里给pc+4,intr_on(),这里对于控制寄存器的利用已经结束了所以可以开启中断。
如果引起trap的原因不是系统调用而是设备中断,这里devint(),这是判断是否是设备中断引起的trap。xv6的中断分为两种一种是串口中断devint()=1,并且串口中断的逻辑已经在devint()中实现,另一种是时钟中断,因为xv6是时间片轮转调度,当时钟周期到了,进程要自动放弃cpu,这部分逻辑在条件判断外实现
否则如果是exception,那么就杀死进程

  if(p->killed)
    exit(-1);

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2)
    yield();

上面关于exception的处理是标记了p->killed=0,如果这里p->killed=0就可以退出了,不能在判断里面就直接退出原因是(ai生成):因为这里会漏逻辑,可能系统调用也调用了kill()这个系统调用,这会导致没有吧之前的进程给杀掉。
下面是判断是否是时钟中断导致的trap,如果是就需要调用yield()函数,主动放弃cpu,触发调度器选择另外一个进程。

  usertrapret();

调用usertrapret()处理返回逻辑。

下面是完整的usertrap()的代码

//
// handle an interrupt, exception, or system call from user space.
// called from trampoline.S
//
void
usertrap(void)
{
  int which_dev = 0;

  if((r_sstatus() & SSTATUS_SPP) != 0)
    panic("usertrap: not from user mode");

  // send interrupts and exceptions to kerneltrap(),
  // since we're now in the kernel.
  w_stvec((uint64)kernelvec);

  struct proc *p = myproc();
  
  // save user program counter.
  p->trapframe->epc = r_sepc();
  
  if(r_scause() == 8){
    // system call

    if(p->killed)
      exit(-1);

    // sepc points to the ecall instruction,
    // but we want to return to the next instruction.
    p->trapframe->epc += 4;

    // an interrupt will change sstatus &c registers,
    // so don't enable until done with those registers.
    intr_on();

    syscall();
  } else if((which_dev = devintr()) != 0){
    // ok
  } else {
    printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
    printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
    p->killed = 1;
  }

  if(p->killed)
    exit(-1);

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2)
    yield();

  usertrapret();
}

usertrapret()

在处理完trap后,需要返回用户空间,返回的第一步这里便是usertrap()调用的第一个函数usertrapret(),这里需要设置risc-v的控制寄存器(control register)为后面从用户空间的trap做准备。

  struct proc *p = myproc();

  // we're about to switch the destination of traps from
  // kerneltrap() to usertrap(), so turn off interrupts until
  // we're back in user space, where usertrap() is correct.
  intr_off();

一旦涉及到对控制寄存器的写就需要关中断,于是usertrapret()这里上来就先进行了关中断操作

  w_stvec(TRAMPOLINE + (uservec - trampoline));

这是向stvec(Supervisor Trap Vector Base Address Register)中写入uservec的地址,TRAMPOLINE - trampoline + uservec = TRAMPOLINE + (uservec - trampoline)是uservec的起始地址。

  // set up trapframe values that uservec will need when
  // the process next re-enters the kernel.
  p->trapframe->kernel_satp = r_satp();         // kernel page table
  p->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stack
  p->trapframe->kernel_trap = (uint64)usertrap;
  p->trapframe->kernel_hartid = r_tp();         // hartid for cpuid()

上面这段代码主要是在对trapframe进行设置,以便下次进入内核,第一行主要是设置当前进程的trapframe的内核页表,第二行是设置trapframe的内核的栈指针,因为risc-v中栈是向下生长的,p->kstack是内核栈的起始物理页(低地址部分),所以栈顶是在p->kstack+PGSIZE(高地址)。保存下次trap应跳转的trap处理c函数。保存当前cpu的硬件线程。

  // set S Previous Privilege mode to User.
  unsigned long x = r_sstatus();
  x &= ~SSTATUS_SPP; // clear SPP to 0 for user mode
  x |= SSTATUS_SPIE; // enable interrupts in user mode
  w_sstatus(x);

将SSTATUS_SPP置为0,这一位为0表示之前cpu的状态是用户态

// set S Exception Program Counter to the saved user pc.
  w_sepc(p->trapframe->epc);

p->trapframe->epc中保存了发生trap时用户执行的指令,为了能够恢复原来的状态。

  // tell trampoline.S the user page table to switch to.
  uint64 satp = MAKE_SATP(p->pagetable);

构造一个符合 RISC-V satp 寄存器格式的值,告诉 trampoline 汇编代码:返回用户态时应切换到哪个页表。

  // jump to trampoline.S at the top of memory, which 
  // switches to the user page table, restores user registers,
  // and switches to user mode with sret.
  uint64 fn = TRAMPOLINE + (userret - trampoline);
  ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);

这段代码是跳转到trampoline.S中的userret函数
比较难读的是这段的语法
首先fn是trampoline.S中的userret函数的入口地址
下面一段代码是一个典型的函数指针调用的语法
(void (*)(uint64, uint64)fn)这是讲fn的地址强制转换为一个如下的函数:
void my_function(unint64 arg1, unint64 arg2);

userret

.globl userret

同样这是声明userret为全局

        ld t0, 112(a0)
        csrw sscratch, t0

a0+112中的内容加载到t0,同时将t0中的内容写入sscratch,这时应该是a0的值,注意此时a0中仍然存储的是trapframe的地址

# restore all but a0 from TRAPFRAME
        ld ra, 40(a0)
        ld sp, 48(a0)
        ld gp, 56(a0)
        ld tp, 64(a0)
        ld t0, 72(a0)
        ld t1, 80(a0)
        ld t2, 88(a0)
        ld s0, 96(a0)
        ld s1, 104(a0)
        ld a1, 120(a0)
        ld a2, 128(a0)
        ld a3, 136(a0)
        ld a4, 144(a0)
        ld a5, 152(a0)
        ld a6, 160(a0)
        ld a7, 168(a0)
        ld s2, 176(a0)
        ld s3, 184(a0)
        ld s4, 192(a0)
        ld s5, 200(a0)
        ld s6, 208(a0)
        ld s7, 216(a0)
        ld s8, 224(a0)
        ld s9, 232(a0)
        ld s10, 240(a0)
        ld s11, 248(a0)
        ld t3, 256(a0)
        ld t4, 264(a0)
        ld t5, 272(a0)
        ld t6, 280(a0)

将寄存器中的内容都恢复

	# restore user a0, and save TRAPFRAME in sscratch
        csrrw a0, sscratch, a0

交换sscratch寄存器和a0中的内容,恢复sscratch为trapframe,同时a0恢复原来存储的值。

        # return to user mode and user pc.
        # usertrapret() set up sstatus and sepc.
        sret

sret完成几个功能1. 恢复中断使能状态 2. 切换cpu特权级 3. 跳转执行用户程序trap之前的指令

至此一次在用户态下的trap就完成了。

三、 调用system calls

在学习过user态下的trap后就可将系统调用的过程完全打通了,这里再回顾一下exec系统调用的完整流程。
这里从initcode.S开始,start代码段initargv的地址加载进寄存器a0、a1,同时把exec的系统调用好加载进寄存器a7,然后调用ecall(执行ecall指令,硬件会自动将cpu状态由用户态转为内核态)引起trap,然后会去到uservec,uservec再调用usertrap(),在usertrap()会识别出该trap是系统调用(system call),然后调用syscall(),然后syscall(),根据trap->frame中保存的寄存器a7判断系统调用号发现a7中存储的是SYS_EXEC,在syscalls函数数组中index到sys_exec函数,然后执行sys_exec(),再执行完sys_exec()会将其返回值放到p->trapframe->a0,因为后面需要根据这个返回值判断系统调用是否成功执行,系统调用通常返回负数表明执行出错,0或者正数标识成功执行,如果系统 调用号(system call number)非法会print error并返回-1。

# Initial process that execs /init.
# This code runs in user space.

#include "syscall.h"

# exec(init, argv)
.globl start
start:
        la a0, init
        la a1, argv
        li a7, SYS_exec
        ecall

# for(;;) exit();
exit:
        li a7, SYS_exit
        ecall
        jal exit

# char init[] = "/init\0";
init:
  .string "/init\0"

# char *argv[] = { init, 0 };
.p2align 2
argv:
  .long init
  .long 0

四、 系统调用参数 system call arguments

系统调用是用户

五、 Traps from kernel space 内核空间的Traps

xv6硬件在配置trap registers根据是user code还是kernel稍有不同,当cpu正在运行kernel code时,stvec指向的时kernel/kernelvec.S,由于用户已经处在内核态了,关于页表的处理就可以省略了,只需将栈指针移动256个字节为即将保存的32个8bytes寄存器准备空间,kernelvec会将32个寄存器存放到stack中,以便在trap被处理后能后恢复现场。

        addi sp, sp, -256

移动栈指针sp,为32个寄存器提供存储空间。


 // save the registers.
        sd ra, 0(sp)
        sd sp, 8(sp)
        sd gp, 16(sp)
        sd tp, 24(sp)
        sd t0, 32(sp)
        sd t1, 40(sp)
        sd t2, 48(sp)
        sd s0, 56(sp)
        sd s1, 64(sp)
        sd a0, 72(sp)
        sd a1, 80(sp)
        sd a2, 88(sp)
        sd a3, 96(sp)
        sd a4, 104(sp)
        sd a5, 112(sp)
        sd a6, 120(sp)
        sd a7, 128(sp)
        sd s2, 136(sp)
        sd s3, 144(sp)
        sd s4, 152(sp)
        sd s5, 160(sp)
        sd s6, 168(sp)
        sd s7, 176(sp)
        sd s8, 184(sp)
        sd s9, 192(sp)
        sd s10, 200(sp)
        sd s11, 208(sp)
        sd t3, 216(sp)
        sd t4, 224(sp)
        sd t5, 232(sp)
        sd t6, 240(sp)

保存寄存器,直接将寄存器的值保存到栈中是有意义的,因为这个栈是属于该线程的,这里的sp指针依然是被中断线程的sp

	// call the C trap handler in trap.c
        call kerneltrap

调用kerneltrap()函数处理trap

需要注意的是内核态下的traps只有两种情况

  1. 设备中断(device interrupt)
  2. 异常(exception)

因为系统调用是为用户态设计的功能,内核态本来就有系统调用所有的权限。

// interrupts and exceptions from kernel code go here via kernelvec,
// on whatever the current kernel stack is.
void 
kerneltrap()
{
  int which_dev = 0;
  uint64 sepc = r_sepc();
  uint64 sstatus = r_sstatus();
  uint64 scause = r_scause();
  
  if((sstatus & SSTATUS_SPP) == 0)
    panic("kerneltrap: not from supervisor mode");
  if(intr_get() != 0)
    panic("kerneltrap: interrupts enabled");

  if((which_dev = devintr()) == 0){
    printf("scause %p\n", scause);
    printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
    panic("kerneltrap");
  }

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2 && myproc() != 0 && myproc()->state == RUNNING)
    yield();

  // the yield() may have caused some traps to occur,
  // so restore trap registers for use by kernelvec.S's sepc instruction.
  w_sepc(sepc);
  w_sstatus(sstatus);
}

两种情况一种是设备中断,一种是异常,异常的话使用panic即可,如果是设备中断就处理设备中断的两种情况,一种是串口,中断的具体逻辑在devintr()中实现了,如果是时钟中断就放弃cpu。
最后两行是因为yield()进行调度其他进程后可能会产生trap,会修改sepc、sstatus中的内容,所以这里将原来保存的值重写写回寄存器中。、

在执行完kerneltrap()后,会返回到kernelvec.S中继续执行后面的内容。

        // restore registers.
        ld ra, 0(sp)
        ld sp, 8(sp)
        ld gp, 16(sp)
        // not this, in case we moved CPUs: ld tp, 24(sp)
        ld t0, 32(sp)
        ld t1, 40(sp)
        ld t2, 48(sp)
        ld s0, 56(sp)
        ld s1, 64(sp)
        ld a0, 72(sp)
        ld a1, 80(sp)
        ld a2, 88(sp)
        ld a3, 96(sp)
        ld a4, 104(sp)
        ld a5, 112(sp)
        ld a6, 120(sp)
        ld a7, 128(sp)
        ld s2, 136(sp)
        ld s3, 144(sp)
        ld s4, 152(sp)
        ld s5, 160(sp)
        ld s6, 168(sp)
        ld s7, 176(sp)
        ld s8, 184(sp)
        ld s9, 192(sp)
        ld s10, 200(sp)
        ld s11, 208(sp)
        ld t3, 216(sp)
        ld t4, 224(sp)
        ld t5, 232(sp)
        ld t6, 240(sp)

将栈中保存的内容放回寄存器中

addi sp, sp, 256

回调sp栈指针

        // return to whatever we were doing in the kernel.
        sret

调用sret指令,回到原来的状态继续执行被trap打断前的代码,sret的功能前面已经说过不再赘述。

六、 RISC-V assembly (easy)

七、 Backtrace (moderate)

这里是实现一个我们经常用来debug的工具backtrace。
首先需要在defs.h中声明backtrace,由于lab建议把backtrace实现在printf.c,所以我们直接把backtrace声明在printf文件的函数附近

void            backtrace(void);

在这里插入图片描述
其次我们还需要实习一个能读取栈顶指针fp的工具,这里lab的hint已经帮我们实现了,我们需要把这个文件放到kernel/riscv.h:下,因为fp指针就存放在s0寄存器中。

static inline uint64
r_fp()
{
  uint64 x;
  asm volatile("mv %0, s0" : "=r" (x) );
  return x;
}

栈的布局如下:
在这里插入图片描述
我们需要打印的是Return Address,同时还需要前一个栈的fp。
我们通过循环完成倒着遍历所有栈,这里hint中告诉了我们循环终止条件,可以通过PGROUNDDOWN(fp) and PGROUNDUP(fp)这两个函数来完成,如果fp不在PGROUNDDOWN(fp) and PGROUNDUP(fp)范围内则说明所有栈已经遍历完了。

xv6中每个进程的内核栈(kernel stack)被分配恰好 4KB(即一个页)的空间,并且该栈的起始地址是页对齐的(PAGE-aligned)。

kernel/printf.c中完成backtrace函数

void 
backtrace(void)
{
  uint64 fp = r_fp();
  uint64 top = PGROUNDUP(fp);
  uint64 bottom = PGROUNDDOWN(fp);
  while(fp >= bottom && fp <= top) 
  {
    printf("%p\n", *((uint64 *)(fp - 8)));
    fp = *((uint64 *)(fp - 16));
  }
}

最后用bttest进行测试

make qemu

在这里插入图片描述
然后运行bttest会出现下面截图的内容,关闭qemu(ctrl+a 松开 按x)

bttest

在这里插入图片描述
将上面的地址复制并和这个命令结合,在终端运行,如果出现和下面截图类似的情况则说明没问题。

addr2line -e kernel/kernel
0x00000000800020cc
0x0000000080001fa6
0x0000000080001c90
0x0000000000000012

在这里插入图片描述

八、 Alarm (hard)

这个实验的让我们实现的东西是一个类似中断的效果,利用cpu clock每个一定的tick周期性地调用一个函数。
可以根据test0|test1、2的顺序一步一步按照hint实现。
首先是test0.
由于需要实现user program,所以需要在用户代码下面进行声明注册。
首先在user/user.h中声明两个函数,这两个函数是通过system call完成功能的所以把他放在所有system calls的声明附近

 int sigalarm(int ticks, void (*handler)());
int sigreturn(void)

在这里插入图片描述
然后需要在user.pl添加这两个函数,让脚本能自动帮我们生成能跳转到对应系统调用的汇编指令

entry("sigalarm");
entry("sigreturn");

在这里插入图片描述
添加后会生成如下汇编代码在user/usys.S
在这里插入图片描述
kernel/syscall.h中添加宏定义

#define SYS_sigalarm 22
#define SYS_sigreturn 23

在这里插入图片描述
kernel/syscall.c中添加这两个系统调用,以便a7中能index到对应的syscall,需要注意xv6中系统调用的实现是通过寄存器传递参数的,所以函数声明这里的参数都是void。
在这里插入图片描述
在这里插入图片描述
至此实现一个系统调用(system call)准备工作就做完了,中间有段时间没碰lab了,前面的东西网了,刚好复习一下
后面就是实现这两个系统调用。
我们将这两个系统调用都实现在kernel/sysproc.c中,因为这两个系统调用是和进程相关的。
先把hint放在这
在这里插入图片描述
现在已经完成了前面的几项内容,在test0中,sys_sigreturn()让我们实现只返回0。

uint64 
sys_sigreturn(void)
{
  return 0;
}

下一项是实现sys_sigalarm()要将alarm intervalpoint to the handler function存储到proc struct中,第一步肯定是先要将用户程序那边传递的参数读出来,然后再把这些参数存到prco struct中,当然要现在proc struct先定义存储这些内容的变量。

uint64 
sys_sigalarm(void)
{
  uint64  handler; // accept ticks argument
  int ticks;
  if(argint(0, &ticks) < 0) // get the first argument
    return -1;
  
  if(argaddr(1, &handler) < 0)
    return -1;

  struct proc *p = myproc();
  p->handler = (void (*)(void)) handler;
  p->ticks = ticks;

  return 0;
}

kernel/proc.h,定义两个存储参数的量。

// Per-process state
struct proc {
  struct spinlock lock;

  // p->lock must be held when using these:
  enum procstate state;        // Process state
  void *chan;                  // If non-zero, sleeping on chan
  int killed;                  // If non-zero, have been killed
  int xstate;                  // Exit status to be returned to parent's wait
  int pid;                     // Process ID

  // wait_lock must be held when using this:
  struct proc *parent;         // Parent process

  // these are private to the process, so p->lock need not be held.
  uint64 kstack;               // Virtual address of kernel stack
  uint64 sz;                   // Size of process memory (bytes)
  pagetable_t pagetable;       // User page table
  struct trapframe *trapframe; // data page for trampoline.S
  struct context context;      // swtch() here to run process
  struct file *ofile[NOFILE];  // Open files
  struct inode *cwd;           // Current directory
  char name[16];               // Process name (debugging)
  int ticks;                 //save ticks
  void (*handler)();           // alarmtest.c handler function
};

为了实现我们需要的在alarm interval调用一次handler fuction,我们还需要记录距离上次调用handler fuction已经过了多少个cpu tick,在proc struct中添加一项tick_count

  int tick_count;            //trace the count since last call

下面我们希望处理的逻辑是在每个时钟中断的时候,我们对于启动了sys_sigalarm的进程,我们需要进程处理判断是否需要调用handler function,时钟中断发生是trap,我们对于这部分逻辑的实现就在kernel/trap.c中,由于sys_sigalarm是从用户态进入的,按照hint,我们实现逻辑在usertrap()function中。

具体逻辑首先判断这个进程是否开启了sigalarm,根据就是ticks是否为0,如果开启了,那么现在时钟数应该+1,同时判断是否慢足alarm interval,如果满足就调用handler,同时将已经过去的tick_count置位0

// 时钟中断
  if(which_dev == 2)
  {  
    if(p->ticks != 0) 
    {
      p->tick_count += 1;
      if(p->tick_count == p->ticks)
      {
        p->tick_count = 0;
        //准备调用handler
        p->trapframe->epc = (uint64) p->handler;
      }
    }
  }

至此test0的所有逻辑就完成了,可以进行测试。
在test0中我们并没有处理在调用完handler之后的如何返回原来上下文的问题,test1、test2需要我们实现完整版。
首先对于当前进程的所有寄存器的内容我们都是需要保存的,当发生alarm系统调用后,所有的寄存器内容被存进进程proc strcut->trapframe中了,而我们需要跳转到handler function,这是通过修改proc struct -> trapframe->epc实现的(描述可能有点混乱,后面我会画个图),这就导致发生alarm系统调用前的pc丢失,所以这里需要将trapframe中内容保存,以便能恢复系统调用前处继续执行后面的代码。
这里我们在proc struct在定义一个lasttrapframe用于存储trapframe

  struct trapframe *lasttrapframe; // save register to return alarm

同时在trap.c中将trapframe保存到lasttrapframe中,这里有个bug让我找了很久,复制内容时,不要用memcpy,要用memove

  // 时钟中断
  if(which_dev == 2)
  {  
    if(p->ticks != 0) 
    {
      p->tick_count += 1;
      if(p->tick_count == p->ticks)
      {
        p->tick_count = 0;
        //准备调用handler  
        memmove(p->lasttrapframe, p->trapframe, sizeof(struct trapframe)); //保存现场
        p->trapframe->epc = (uint64) p->handler;
      }
    }
  }

同时在sys_sigreturn处理返回恢复现场的逻辑,这里是handler function在最后会调用sys_sigreturn

uint64 
sys_sigreturn(void)
{
  struct proc *p = myproc();
  memmove(p->trapframe, p->lasttrapframe, sizeof(struct trapframe));
  return 0;
}

注意最后test2有一个hint,在上一个alarm处理完之前,不能让下一个alarm开始。在这里插入图片描述
完整版
kernel/proc.h

// Per-process state
struct proc {
  struct spinlock lock;

  // p->lock must be held when using these:
  enum procstate state;        // Process state
  void *chan;                  // If non-zero, sleeping on chan
  int killed;                  // If non-zero, have been killed
  int xstate;                  // Exit status to be returned to parent's wait
  int pid;                     // Process ID

  // wait_lock must be held when using this:
  struct proc *parent;         // Parent process

  // these are private to the process, so p->lock need not be held.
  uint64 kstack;               // Virtual address of kernel stack
  uint64 sz;                   // Size of process memory (bytes)
  pagetable_t pagetable;       // User page table
  struct trapframe *trapframe; // data page for trampoline.S
  struct context context;      // swtch() here to run process
  struct file *ofile[NOFILE];  // Open files
  struct inode *cwd;           // Current directory
  char name[16];               // Process name (debugging)
  int ticks;                 //save ticks
  void (*handler)();           // alarmtest.c handler function
  int tick_count;            //trace the count since last call
  struct trapframe *lasttrapframe; // save register to return alarm
  int alarm_going;
};

kernel/sysproc.c

uint64 
sys_sigalarm(void)
{
  uint64  handler; // accept ticks argument
  int ticks;
  if(argint(0, &ticks) < 0) // get the first argument
    return -1;
  
  if(argaddr(1, &handler) < 0)
    return -1;

  struct proc *p = myproc();
  p->handler = (void (*)(void)) handler;
  p->ticks = ticks;
  p->tick_count = 0;
  // p->alarm_going = 0;
  return 0;
}

uint64 
sys_sigreturn(void)
{
  struct proc *p = myproc();
  memmove(p->trapframe, p->lasttrapframe, sizeof(struct trapframe));
  p->alarm_going=0;
  return 0;
}

kernel/trap.c

  // 时钟中断
  if(which_dev == 2)
  {  
    if(p->ticks != 0) 
    {
      p->tick_count += 1;
      if(p->tick_count == p->ticks)
      {
        p->tick_count = 0;
        //准备调用handler
        if(p->alarm_going == 0) 
        {
          p->alarm_going = 1;
          memmove(p->lasttrapframe, p->trapframe, sizeof(struct trapframe)); //保存现场
          p->trapframe->epc = (uint64) p->handler;
        }
        
      }
    }
  }

还有一点在创建进程时,需要给lasttrapframe分配空间,以及在进程销毁时需要释放空间,还需要对struct proc中的内容进行初始化。
kernel/proc.c

// Look in the process table for an UNUSED proc.
// If found, initialize state required to run in the kernel,
// and return with p->lock held.
// If there are no free procs, or a memory allocation fails, return 0.
static struct proc*
allocproc(void)
{
  struct proc *p;

  for(p = proc; p < &proc[NPROC]; p++) {
    acquire(&p->lock);
    if(p->state == UNUSED) {
      goto found;
    } else {
      release(&p->lock);
    }
  }
  return 0;

found:
  p->pid = allocpid();
  p->state = USED;
  p->alarm_going=0;
  p->ticks = 0;
  p->tick_count = 0;
  p->handler=0;
  // Allocate a trapframe page.
  if((p->trapframe = (struct trapframe *)kalloc()) == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  if((p->lasttrapframe = (struct trapframe *)kalloc()) == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  // An empty user page table.
  p->pagetable = proc_pagetable(p);
  if(p->pagetable == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  // Set up new context to start executing at forkret,
  // which returns to user space.
  memset(&p->context, 0, sizeof(p->context));
  p->context.ra = (uint64)forkret;
  p->context.sp = p->kstack + PGSIZE;

  return p;
}

九、 完整代码

至此lab4的所有内容都已经完成~
完整版代码放在github仓库的traps branch下,如果对你有帮助,不妨点个小小的🌟🌟🌟star🌟🌟🌟
完整代码链接
在这里插入图片描述

~~🎉🎉🎉🎉🎉🎉完结撒花🎉🎉🎉🎉🎉🎉🎉
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值