MIT 6.S08操作系统实验课 lab4 页表记录

提示:这是操作系统本人对 MIT 6.S081 的 lab4 实验课的笔记,仅供参考。


前言

参考资料:
资料1
资料2


一、理论部分

重要寄存器描述:

  • stvec:内核在这里写入其陷阱处理程序的地址;RISC-V跳转到这里处理陷阱。
  • sepc:当发生陷阱时,RISC-V会在这里保存程序计数器pc(因为pc会被stvec覆盖)。sret(从陷阱返回)指令会将sepc复制到pc。内核可以写入sepc来控制sret的去向。
  • scause: RISC-V在这里放置一个描述陷阱原因的数字。
  • sscratch:内核在这里放置了一个值,这个值在陷阱处理程序一开始就会派上用场。
  • sstatus:其中的SIE位控制设备中断是否启用。如果内核清空SIE,RISC-V将推迟设备中断,直到内核重新设置SIE。SPP位指示陷阱是来自用户模式还是管理模式,并控制sret返回的模式。

上述寄存器都用于在管理模式时处理陷阱,在用户模式下不能读取或写入

当需要强制执行陷阱时,RISC-V硬件对所有陷阱类型(计时器终端除外)执行一下操作:

  1. 如果陷阱是设备中断,并且状态 SIE 位被清空,则不执行以下任何操作。
  2. 清除 SIE 以禁用中断。
  3. 将 pc 复制到 sepc。
  4. 将当前模式(用户或管理)保存在状态的 SPP 位中。
  5. 设置 scause 以反映产生陷阱的原因。
  6. 将模式设置为管理模式。
  7. 将 stvec 复制到 pc。
  8. 在新的 pc 上开始执行。

请注意,CPU不会切换到内核页表,不会切换到内核栈,也不会保存除 pc 之外的任何寄存器。

1.1 从用户空间陷入

从用户空间陷入到内核的函数调用流程为
ecall(用户程序发出系统调用指令) --------->uservec ---------> usertrap ---------> usertrapret ---------> userret
主要查看上述四个函数的视线流程,先我将上述四个函数的注释一次粘贴如下:

.globl 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.  // ssratch指向进程的p->trapframe映射到用户空间的特定位置,在trapframe
        #
        
	# swap a0 and sscratch
        # so that a0 is TRAPFRAME
        csrrw a0, sscratch, a0  // 交换a0和sscratch,这样a0就指向TRAPFRAME了

        # save the user registers in TRAPFRAME
        sd ra, 40(a0)  # 将 ra 寄存器的值保存到 TRAPFRAME 的偏移 40 字节处,下同
        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)

	# save the user a0 in p->trapframe->a0
        csrr t0, sscratch  # 将 sscratch 寄存器的值保存到 t0 寄存器,此时sscarch实际上是原来a0的内容
        sd t0, 112(a0)  # 将 t0 寄存器的值保存到 TRAPFRAME 的偏移 112 处

        # restore kernel stack pointer from p->trapframe->kernel_sp
        ld sp, 8(a0) # 将 TRAPFRAME 的偏移 8 处的值加载到 sp 寄存器

        # make tp hold the current hartid, from p->trapframe->kernel_hartid
        ld tp, 32(a0) # 将 TRAPFRAME 的偏移 32 处的值加载到 tp 寄存器

        # load the address of usertrap(), p->trapframe->kernel_trap
        ld t0, 16(a0) # 将 TRAPFRAME 的偏移 16 处的值加载到 t0 寄存器

        # restore kernel page table from p->trapframe->kernel_satp
        ld t1, 0(a0) # 将 TRAPFRAME 的偏移 0 处的值加载到 t1 寄存器
        csrw satp, t1  # 将 t1 寄存器的值保存到 satp 寄存器, 而 t1是当前内容是kernel_satp
        sfence.vma zero, zero  # 刷新 TLB

        # 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  # 将 PC设置为 t0 寄存器的值,即 usertrap()

.globl userret
userret:
        # userret(TRAPFRAME, pagetable)
        # switch from kernel to user.
        # usertrapret() calls here.
        # a0: TRAPFRAME, in user page table.  a0存储了TRAPFRAME帧地址
        # a1: user page table, for satp.      a1存储了用户页表

        # switch to the user page table.
        csrw satp, a1  # 将 a1 寄存器的值保存到 satp 寄存器
        sfence.vma zero, zero # 刷新 TLB

        # put the saved user a0 in sscratch, so we
        # can swap it with our a0 (TRAPFRAME) in the last step.
        ld t0, 112(a0)  # 将 TRAPFRAME 的偏移 112 处的值加载到 t0 寄存器
        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)
        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  # 交换 a0 和 sscratch的值,这样sscratch就重新指向了TRAPFRAME
        
        # return to user mode and user pc.
        # usertrapret() set up sstatus and sepc.
        sret  # 返回到用户态

trap.c文件

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);  // 设置 stvec 寄存器,以便在内核中发生中断时跳转到 kernelvec 函数,因为此时我们在内核空间中

  struct proc *p = myproc();
  
  // save user program counter.
  p->trapframe->epc = r_sepc();  // 当发生陷阱时,RISC-V会在sepc寄存器中保存PC的值,因此为保证在恢复执行时能够从正确的位置开始执行,需要保存这个值
  
  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)  // 如果是时钟中断,就让出 CPU
    yield();

  usertrapret(); // 返回到用户空间
}

//
// return to user space
//
void
usertrapret(void)
{
  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(); // 关闭中断

  // send syscalls, interrupts, and exceptions to trampoline.S
  w_stvec(TRAMPOLINE + (uservec - trampoline));

  // 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->kstack是栈底为低地址
  p->trapframe->kernel_trap = (uint64)usertrap;
  p->trapframe->kernel_hartid = r_tp();         // hartid for cpuid()

  // set up the registers that trampoline.S's sret will use
  // to get to user space.
  
  // set S Previous Privilege mode to User.
  unsigned long x = r_sstatus();
  x &= ~SSTATUS_SPP; // clear SPP to 0 for user mode  清除SPP位,以便在用户模式下
  x |= SSTATUS_SPIE; // enable interrupts in user mode,开启中断
  w_sstatus(x);

  // set S Exception Program Counter to the saved user pc. 
  w_sepc(p->trapframe->epc);  // 设置sepc寄存器,以便在恢复执行时能够从正确的位置开始执行,因为从陷阱返回时,处理器会从sepc寄存器中的值作为PC的值

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

  // 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);
}

上述就是从用户空间陷入内核空间一些关键函数的解释。

二、实验

2.1 Backtrace

该实验的目的实现曾经调用函数地址的回溯,只用打印程序地址。
概念理解
栈帧:每个任务(进程)有一个栈(stack),在这个进程中每个函数被调用时分别从这个栈占用一段区域,称为帧(frame)。

在我们的实验中,fp 指向当前栈帧的开始地址,sp 指向当前栈帧的结束地址。 (栈从高地址往低地址生长,所以 fp 虽然是帧开始地址,但是地址比 sp 高)。栈帧布局图如下图所示:
在这里插入图片描述

栈帧中从高到低第一个 8 字节 fp-8 是 return address,也就是当前调用层应该返回到的地址。
栈帧中从高到低第二个 8 字节 fp-16 是 previous address,指向上一层栈帧的 fp 开始地址。
剩下的为保存的寄存器、局部变量等。一个栈帧的大小不固定,但是至少 16 字节。
在 xv6 中,使用一个页来存储栈,如果 fp 已经到达栈页的上界,则说明已经到达栈底。

根据以上信息,我们可以得到如下步骤:
步骤一
riscv.h 中添加获取当前 fp(frame pointer)寄存器的方法:

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

步骤二
在文件kernel/defs.h文件中添加声明

void            backtrace(void);

步骤三
kernel/printf.c中添加以下代码:

// printf.c

void backtrace() {
  uint64 fp = r_fp();
  while(fp != PGROUNDUP(fp)) { // 如果已经到达栈底
    uint64 ra = *(uint64*)(fp - 8); // return address
    printf("%p\n", ra);
    fp = *(uint64*)(fp - 16); // previous fp
  }
}

步骤四
kernel/sysproc.c文件中函数sys_sleep开头调用一次backtrace()

// sysproc.c
uint64
sys_sleep(void)
{
  int n;
  uint ticks0;

  backtrace(); // 添加这一行代码

  if(argint(0, &n) < 0)
    return -1;
  
  // ......

  return 0;
}

最后便是编译运行,使用make qemu启动系统,在终端输入bttest命令即可。

2.2 Alarm

该实验主要是实现函数定时调用。理解如何调用处理程序是主要问题。PC即程序计数器的过程是这样的:

  1. ecall指令中将PC保存到SEPC
  2. 在usertrap中将SEPC保存到p->trapframe->epc
  3. p->trapframe->epc加4指向下一条指令
  4. 执行系统调用
  5. 在usertrapret中将SEPC改写为p->trapframe->epc中的值
  6. 在sret中将PC设置为SEPC的值

可见执行系统调用后返回到用户空间继续执行的指令地址是由p->trapframe->epc决定的,因此在usertrap中主要就是完成它的设置工作。

具体实现步骤可参考该文章:参考博客

第一步,注册系统调用

  1. Makefile文件中的UPROGS项中添加“$U/_alarmtest\”。如下图(在Makefile文件中加入该项后,系统启动后,在终端才能运行alarmtest命令):
$U/_alarmtest\

在这里插入图片描述
2. 在user/user.h文件中添加系统调用原型 “int sigalarm(int ticks, void (*handler)());”和"int sigreturn(void);",如下图:

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

在这里插入图片描述
3. 添加存根,在文件user/usys.pl文件中添加存根,仿照其文件内容,添加“entry("sigalarm");”和"entry("sigreturn");",如下图:

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

在这里插入图片描述
4. 添加系统调用编号,在文件kernel/syscall.h文件中添加,需要添加一个未被使用的系统调用编号,我添加为“#define SYS_sigalarm 22”和"#define SYS_sigreturn 23"。如下图:

#define SYS_sigalarm  22
#define SYS_sigreturn  23

在这里插入图片描述
5. 在kernel/syscall.c文件中添加如下代码

extern uint64 sys_sigalarm(void);
extern uint64 sys_sigreturn(void);

在这里插入图片描述

[SYS_sigalarm] sys_sigalarm,
[SYS_sigreturn] sys_sigreturn,

在这里插入图片描述

第二步,具体实现

  1. 首先在文件kernel/proc.h文件中struct proc结构体中添加如下字段
int alarm_interval; // 时钟周期,0 为禁用
void (*alarm_handler)(); //时钟回调处理函数
int alarm_ticks;  //  距离下一次重新调用 alarm_handler 的剩余时钟周期数
struct trapframe *alarm_trapframe;  // 时钟中断时刻的 trapframe,用于中断处理完成后恢复原程序的正常执行
int alarm_goingoff; //是否已经有一个时钟回调正在执行且还未返回(用于防止在 alarm_handler 中途闹钟到期再次调用 alarm_handler,导致 alarm_trapframe 被覆盖)

如下图:
在这里插入图片描述

  1. 在文件/kernel/defs.h中添加如下函数声明:
int             sigalarm(int, void(*handler)());
int             sigreturn();

如下图:
在这里插入图片描述

  1. /kernel/trap.c中添加如下代码
int sigalarm(int ticks, void(*handler)()) {
 // 设置 myproc 中的相关属性
 struct proc *p = myproc();
 p->alarm_interval = ticks;
 p->alarm_handler = handler;
 p->alarm_ticks = ticks;
 return 0;
}

int sigreturn() {
 // 将 trapframe 恢复到时钟中断之前的状态,恢复原本正在执行的程序流
 struct proc *p = myproc();
 *p->trapframe = *p->alarm_trapframe;
 p->alarm_goingoff = 0;
 return 0;
}

如下图:
在这里插入图片描述

  1. 在文件/kernel/proc.c文件中添加如下代码
uint64 sys_sigalarm(void) {
 int n;
 uint64 fn;
 if(argint(0, &n) < 0)
   return -1;
 if(argaddr(1, &fn) < 0)
   return -1;
 
 return sigalarm(n, (void(*)())(fn));
}

uint64 sys_sigreturn(void) {
   return sigreturn();
}

如下图:
在这里插入图片描述
5. 在/kernel/proc.c文件函数allocproc中添加如下代码:

// Allocate a trapframe page for alarm_trapframe.
  if((p->alarm_trapframe = (struct trapframe *)kalloc()) == 0){
    release(&p->lock);
    return 0;
  }

  p->alarm_interval = 0;
  p->alarm_handler = 0;
  p->alarm_ticks = 0;
  p->alarm_goingoff = 0;

如下图所示:
在这里插入图片描述

  1. kernel/trap.c文件函数usertrap中添加如下代码:
 // give up the CPU if this is a timer interrupt.
  if(which_dev == 2) {
    if(p->alarm_interval != 0) { // 如果设定了时钟事件
      if(--p->alarm_ticks <= 0) { // 时钟倒计时 -1 tick,如果已经到达或超过设定的 tick 数
        if(!p->alarm_goingoff) { // 确保没有时钟正在运行
          p->alarm_ticks = p->alarm_interval;
          // jump to execute alarm_handler
          *p->alarm_trapframe = *p->trapframe; // backup trapframe
          p->trapframe->epc = (uint64)p->alarm_handler;
          p->alarm_goingoff = 1;
        }
        // 如果一个时钟到期的时候已经有一个时钟处理函数正在运行,则会推迟到原处理函数运行完成后的下一个 tick 才触发这次时钟
      }
    }
    yield();
  }

如下图:
在这里插入图片描述
经过上述步骤,输入make编译系统,make qemu启动系统,在终端输入命令alarmtest便可以进行测试。


  • 7
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
项目:使用AngularJs编写的简单 益智游戏(附源代码)  这是一个简单的 javascript 项目。这是一个拼图游戏,也包含一个填字游戏。这个游戏玩起来很棒。有两个不同的版本可以玩这个游戏。你也可以玩填字游戏。 关于游戏 这款游戏的玩法很简单。如上所述,它包含拼图和填字游戏。您可以通过移动图像来玩滑动拼图。您还可以选择要在滑动面板中拥有的列数和网格数。 另一个是填字游戏。在这里你只需要找到浏览器左侧提到的那些单词。 要运行此游戏,您需要在系统上安装浏览器。下载并在代码编辑器中打开此项目。然后有一个 index.html 文件可供您修改。在命令提示符中运行该文件,或者您可以直接运行索引文件。使用 Google Chrome 或 FireFox 可获得更好的用户体验。此外,这是一款多人游戏,双方玩家都是人类。 这个游戏包含很多 JavaScript 验证。这个游戏很有趣,如果你能用一点 CSS 修改它,那就更好了。 总的来说,这个项目使用了很多 javascript 和 javascript 库。如果你可以添加一些具有不同颜色选项的级别,那么你一定可以利用其库来提高你的 javascript 技能。 演示: 该项目为国外大神项目,可以作为毕业设计的项目,也可以作为大作业项目,不用担心代码重复,设计重复等,如果需要对项目进行修改,需要具备一定基础知识。 注意:如果装有360等杀毒软件,可能会出现误报的情况,源码本身并无病毒,使用源码时可以关闭360,或者添加信任。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值