Xv6 Lab4: Traps

Lab4:Traps

本实验探索如何使用陷阱实现系统调用。您将首先使用栈做一个热身练习,然后实现一个用户级陷阱处理的示例。

**Attention:**开始编码之前,请阅读xv6手册的第4章和相关源文件:

  • kernel/trampoline.S:涉及从用户空间到内核空间再到内核空间的转换的程序集
  • kernel/trap.c:处理所有中断的代码

要启动实验,请切换到traps分支:

$ git fetch
$ git checkout traps
$ make clean

RISC-V assembly (easy)

理解一点RISC-V汇编是很重要的,你应该在6.004中接触过。xv6仓库中有一个文件user/call.c。执行make fs.img编译它,并在user/call.asm中生成可读的汇编版本。

阅读call.asm中函数gfmain的代码。RISC-V的使用手册在参考页上。以下是您应该回答的一些问题(将答案存储在answers-traps.txt文件中):

  1. 哪些寄存器保存函数的参数?例如,在mainprintf的调用中,哪个寄存器保存13?
  2. main的汇编代码中对函数f的调用在哪里?对g的调用在哪里(提示:编译器可能会将函数内联)
  3. printf函数位于哪个地址?
  4. mainprintfjalr之后的寄存器ra中有什么值?
  5. 运行以下代码。
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);

程序的输出是什么?这是将字节映射到字符的ASCII码表

输出取决于RISC-V小端存储的事实。如果RISC-V是大端存储,为了得到相同的输出,你会把i设置成什么?是否需要将57616更改为其他值?

这里有一个小端和大端存储的描述和一个更异想天开的描述

  1. 在下面的代码中,“y=”之后将打印什么(注:答案不是一个特定的值)?为什么会发生这种情况?
printf("x=%d y=%d", 3);

Backtrace (moderate)

回溯(Backtrace)通常对于调试很有用:它是一个存放于栈上用于指示错误发生位置的函数调用列表。

kernel/printf.c中实现名为backtrace()的函数。在sys_sleep中插入一个对此函数的调用,然后运行bttest,它将会调用sys_sleep。你的输出应该如下所示:

backtrace:
0x0000000080002cda
0x0000000080002bb6
0x0000000080002898

bttest退出qemu后。在你的终端:地址或许会稍有不同,但如果你运行addr2line -e kernel/kernel(或riscv64-unknown-elf-addr2line -e kernel/kernel),并将上面的地址剪切粘贴如下:

$ addr2line -e kernel/kernel
0x0000000080002de2
0x0000000080002f4a
0x0000000080002bfc
Ctrl-D

你应该看到类似下面的输出:

kernel/sysproc.c:74
kernel/syscall.c:224
kernel/trap.c:85

编译器向每一个栈帧中放置一个帧指针(frame pointer)保存调用者帧指针的地址。你的backtrace应当使用这些帧指针来遍历栈,并在每个栈帧中打印保存的返回地址。

提示:

  • kernel/defs.h中添加backtrace的原型,那样你就能在sys_sleep中引用backtrace
  • GCC编译器将当前正在执行的函数的帧指针保存在s0寄存器,将下面的函数添加到kernel/riscv.h
static inline uint64
r_fp()
{
  uint64 x;
  asm volatile("mv %0, s0" : "=r" (x) );
  return x;
}

并在backtrace中调用此函数来读取当前的帧指针。这个函数使用内联汇编来读取s0

  • 这个课堂笔记中有张栈帧布局图。注意返回地址位于栈帧帧指针的固定偏移(-8)位置,并且保存的帧指针位于帧指针的固定偏移(-16)位置

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • XV6在内核中以页面对齐的地址为每个栈分配一个页面。你可以通过PGROUNDDOWN(fp)PGROUNDUP(fp)(参见kernel/riscv.h)来计算栈页面的顶部和底部地址。这些数字对于backtrace终止循环是有帮助的。

一旦你的backtrace能够运行,就在kernel/printf.cpanic中调用它,那样你就可以在panic发生时看到内核的backtrace

Alarm (hard)

YOUR JOB

在这个练习中你将向XV6添加一个特性,在进程使用CPU的时间内,XV6定期向进程发出警报。这对于那些希望限制CPU时间消耗的受计算限制的进程,或者对于那些计算的同时执行某些周期性操作的进程可能很有用。更普遍的来说,你将实现用户级中断/故障处理程序的一种初级形式。例如,你可以在应用程序中使用类似的一些东西处理页面故障。如果你的解决方案通过了alarmtestusertests就是正确的。

你应当添加一个新的sigalarm(interval, handler)系统调用,如果一个程序调用了sigalarm(n, fn),那么每当程序消耗了CPU时间达到n个“滴答”,内核应当使应用程序函数fn被调用。当fn返回时,应用应当在它离开的地方恢复执行。在XV6中,一个滴答是一段相当任意的时间单元,取决于硬件计时器生成中断的频率。如果一个程序调用了sigalarm(0, 0),系统应当停止生成周期性的报警调用。

你将在XV6的存储库中找到名为user/alarmtest.c的文件。将其添加到Makefile。注意:你必须添加了sigalarmsigreturn系统调用后才能正确编译(往下看)。

alarmtesttest0中调用了sigalarm(2, periodic)来要求内核每隔两个滴答强制调用periodic(),然后旋转一段时间。你可以在user/alarmtest.asm中看到alarmtest的汇编代码,这或许会便于调试。当alarmtest产生如下输出并且usertests也能正常运行时,你的方案就是正确的:

$ alarmtest
test0 start
........alarm!
test0 passed
test1 start
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
test1 passed
test2 start
................alarm!
test2 passed
$ usertests
...
ALL TESTS PASSED
$

当你完成后,你的方案也许仅有几行代码,但如何正确运行是一个棘手的问题。我们将使用原始存储库中的alarmtest.c版本测试您的代码。你可以修改alarmtest.c来帮助调试,但是要确保原来的alarmtest显示所有的测试都通过了。

test0: invoke handler (调用处理程序)

首先修改内核以跳转到用户空间中的报警处理程序,这将导致test0打印“alarm!”。不用担心输出“alarm!”之后会发生什么;如果您的程序在打印“alarm!”后崩溃,对于目前来说也是正常的。以下是一些提示

  • 您需要修改Makefile以使alarmtest.c被编译为xv6用户程序。
  • 放入user/user.h的正确声明是:
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);
  • 更新user/usys.pl(此文件生成user/usys.S)、kernel/syscall.hkernel/syscall.c以允许alarmtest调用sigalarmsigreturn系统调用。
  • 目前来说,你的sys_sigreturn系统调用返回应该是零。
  • 你的sys_sigalarm()应该将报警间隔和指向处理程序函数的指针存储在struct proc的新字段中(位于kernel/proc.h)。
  • 你也需要在struct proc新增一个新字段。用于跟踪自上一次调用(或直到下一次调用)到进程的报警处理程序间经历了多少滴答;您可以在proc.callocproc()中初始化proc字段。
  • 每一个滴答声,硬件时钟就会强制一个中断,这个中断在kernel/trap.c中的usertrap()中处理。
  • 如果产生了计时器中断,您只想操纵进程的报警滴答;你需要写类似下面的代码
if(which_dev == 2) ...
  • 仅当进程有未完成的计时器时才调用报警函数。请注意,用户报警函数的地址可能是0(例如,在user/alarmtest.asm中,periodic位于地址0)。
  • 您需要修改usertrap(),以便当进程的报警间隔期满时,用户进程执行处理程序函数。当RISC-V上的陷阱返回到用户空间时,什么决定了用户空间代码恢复执行的指令地址?
  • 如果您告诉qemu只使用一个CPU,那么使用gdb查看陷阱会更容易,这可以通过运行
make CPUS=1 qemu-gdb
  • 如果alarmtest打印“alarm!”,则您已成功。
test1/2: resume interrupted code(恢复被中断的代码)

alarmtest打印“alarm!”后,很可能会在test0test1中崩溃,或者alarmtest(最后)打印“test1 failed”,或者alarmtest未打印“test1 passed”就退出。要解决此问题,必须确保完成报警处理程序后返回到用户程序最初被计时器中断的指令执行。必须确保寄存器内容恢复到中断时的值,以便用户程序在报警后可以不受干扰地继续运行。最后,您应该在每次报警计数器关闭后“重新配置”它,以便周期性地调用处理程序。

作为一个起始点,我们为您做了一个设计决策:用户报警处理程序需要在完成后调用sigreturn系统调用。请查看alarmtest.c中的periodic作为示例。这意味着您可以将代码添加到usertrapsys_sigreturn中,这两个代码协同工作,以使用户进程在处理完警报后正确恢复。

提示:

  • 您的解决方案将要求您保存和恢复寄存器——您需要保存和恢复哪些寄存器才能正确恢复中断的代码?(提示:会有很多)
  • 当计时器关闭时,让usertrapstruct proc中保存足够的状态,以使sigreturn可以正确返回中断的用户代码。
  • 防止对处理程序的重复调用——如果处理程序还没有返回,内核就不应该再次调用它。test2测试这个。
  • 一旦通过test0test1test2,就运行usertests以确保没有破坏内核的任何其他部分。

可选的挑战部分

  • backtrace()中打印函数的名称和行号,而不仅仅是数字化的地址。(hard)

My Record

1. RISC-V assembly

相关问题

这里将 cat.asm的相关部分放在下面

0000000000000000 <g>:
#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int g(int x) {
   0:	1141                	addi	sp,sp,-16
   2:	e422                	sd	s0,8(sp)
   4:	0800                	addi	s0,sp,16
  return x+3;
}
   6:	250d                	addiw	a0,a0,3
   8:	6422                	ld	s0,8(sp)
   a:	0141                	addi	sp,sp,16
   c:	8082                	ret

000000000000000e <f>:

int f(int x) {
   e:	1141                	addi	sp,sp,-16
  10:	e422                	sd	s0,8(sp)
  12:	0800                	addi	s0,sp,16
  return g(x);
}
  14:	250d                	addiw	a0,a0,3
  16:	6422                	ld	s0,8(sp)
  18:	0141                	addi	sp,sp,16
  1a:	8082                	ret

000000000000001c <main>:

void main(void) {
  1c:	1141                	addi	sp,sp,-16
  1e:	e406                	sd	ra,8(sp)
  20:	e022                	sd	s0,0(sp)
  22:	0800                	addi	s0,sp,16
  printf("%d %d\n", f(8)+1, 13);
  24:	4635                	li	a2,13
  26:	45b1                	li	a1,12
  28:	00000517          	auipc	a0,0x0
  2c:	7b050513          	addi	a0,a0,1968 # 7d8 <malloc+0xea>
  30:	00000097          	auipc	ra,0x0
  34:	600080e7          	jalr	1536(ra) # 630 <printf>
  exit(0);
  38:	4501                	li	a0,0
  3a:	00000097          	auipc	ra,0x0
  3e:	27e080e7          	jalr	638(ra) # 2b8 <exit>
  1. 哪些寄存器包含函数参数?例如在mainprintf的调用中,哪个寄存器保存13?
a0-a7 包含函数参数    a2存储13
  1. main的汇编代码中对函数f的调用在哪里?对g的调用在哪里(提示:编译器可能会将函数内联)
在C代码中,main函数调用f,f调用g;而在生成的汇编代码中,main函数进行了内联优化处理,从代码li a1,12可以看出,main直接计算出了结果并存储
  1. printf函数位于哪个地址?
0x630
  1. mainprinfjalr之后的寄存器ra中有什么值?

auipc(Add Upper Immediate to PC):auipc rd imm,将高位立即数加到PC上,从下面的指令格式可以看出,该指令将20位的立即数左移12位之后(右侧补0)加上PC的值,将结果保存到dest位置,图中为rd寄存器

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

下面来看jalr (jump and link register):jalr rd, offset(rs1)跳转并链接寄存器。jalr指令会将当前PC+4保存在rd中,然后跳转到指定的偏移地址offset(rs1)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

来看XV6的代码:

30: 00000097       auipc ra,0x0
34: 600080e7       jalr  1536(ra) # 630 <printf>

第一行代码:00000097H=00...0 0000 1001 0111B,对比指令格式,可见imm=0,dest=00001,opcode=0010111,对比汇编指令可知,auipc的操作码是0010111,ra寄存器代码是00001。这行代码将0x0左移12位(还是0x0)加到PC(当前为0x30)上并存入ra中,即ra中保存的是0x30

第2行代码:600080e7H=0110 0...0 1000 0000 1110 0111B,可见imm=0110 0000 0000,rs1=00001,funct3=000,rd=00001,opcode=1100111,rs1和rd的知识码都是00001,即都为寄存器ra。这对比jalr的标准格式有所不同,可能是此两处使用寄存器相同时,汇编中可以省略rd部分。

ra中保存的是0x30,加上0x600后为0x630,即printf的地址,执行此行代码后,将跳转到printf函数执行,并将PC+4=0X34+0X4=0X38保存到ra中,供之后返回使用。

  1. 运行一下代码
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);

程序的输出是什么?这是将字节映射到字符的ASCII码表

输出取决于RISC-V小端存储的事实。如果RISC-V是大端存储,为了得到相同的输出,你会把i设置成什么?是否需要将57616更改为其他值?

这里有一个小端和大端存储的描述和一个更异想天开的描述

57616=0xE110,0x00646c72小端存储为72-6c-64-00,对照ASCII码表

72:r 
6c:l 
64:d 
00:充当字符串结尾标识

因此输出为:HE110 World

若为大端存储,i应改为0x726c6400,不需改变57616

  1. 在下面的代码中,“y=”之后将打印什么(注:答案不是一个特定的值)?为什么会发生这种情况?
printf("x=%d y=%d", 3);

​ 原本需要两个参数,却只传入了一个,因此y=后面打印的结果取决于之前a2中保存的数据

2. Backtrace
  1. 首先先在kernel/defs.h中添加定义

    // printf.c
    void            printf(char*, ...);
    void            panic(char*) __attribute__((noreturn));
    void            printfinit(void);
    void            backtrace();
    
  2. 根据提示,在kernel/riscv.h中增加r_fp()的实现,用于读取寄存器s0

    //r_fp()用来读取寄存器s0
    static inline uint64
    r_fp()
    {
      uint64 x;
      asm volatile("mv %0, s0" : "=r" (x) );
      return x;
    }
    
  3. kernel/printf.c中增加backtrace()的实现

    void 
    backtrace()
    {
      printf("backtrace:\n");
    
      //读取当前帧指针
      uint64 fp = r_fp();
      while(PGROUNDUP(fp) - PGROUNDDOWN(fp) == PGSIZE)
      {
        //调用的函数的返回地址保存在固定偏移-8的位置
        uint64 ret_addr = *(uint64*)(fp - 8);
        printf("%p\n",ret_addr);
        //前一个帧指针保存在固定偏移-16的位置
        fp = *(uint64*)(fp - 16);
      }
    }
    
  4. kernel/sysproc.csys_sleep()函数中调用backtrace()

    uint64
    sys_sleep(void)
    {
      int n;
      uint ticks0;
    
      if(argint(0, &n) < 0)
        return -1;
      acquire(&tickslock);
      ticks0 = ticks;
      while(ticks - ticks0 < n){
        if(myproc()->killed){
          release(&tickslock);
          return -1;
        }
        sleep(&ticks, &tickslock);
      }
      release(&tickslock);
      backtrace();
      return 0;
    }
    

运行结果:

  • make qemu启动xv6,然后执行bttestbttest调用sleep(system call),然后依次调用trap.c , syscall.c , sysproc.c
xv6 kernel is booting

hart 1 starting
hart 2 starting
init: starting sh
$ bttest
backtrace:
0x0000000080002d66
0x0000000080002bc8
0x00000000800028b2
  • 退出qemu,执行addr2line(提示中给的代码)
root@shimo-rin-virtual-machine:/home/shimo_rin/桌面/xv6/xv6-lab/xv6-labs-2020# addr2line -e kernel/kernel
0x0000000080002d66
0x0000000080002bc8
0x00000000800028b2/home/shimo_rin/桌面/xv6/xv6-lab/xv6-labs-2020/kernel/sysproc.c:74
/home/shimo_rin/桌面/xv6/xv6-lab/xv6-labs-2020/kernel/syscall.c:140
/home/shimo_rin/桌面/xv6/xv6-lab/xv6-labs-2020/kernel/trap.c:76
3. Alarm
test0

先理解下述在过程中:进行函数调用时,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中主要就是完成p->trapframe->epc的设置工作。

以下是实验步骤:

  1. 首先先修改Makefile,(因为我们需要用老师给的测试程序测试实验结果)

    UPROGS=\
    	$U/_cat\
    	$U/_echo\
    	$U/_forktest\
    	$U/_grep\
    	$U/_init\
    	$U/_kill\
    	$U/_ln\
    	$U/_ls\
    	$U/_mkdir\
    	$U/_rm\
    	$U/_sh\
    	$U/_stressfs\
    	$U/_usertests\
    	$U/_grind\
    	$U/_wc\
    	$U/_zombie\
    	$U/_alarmtest\		//添加该行
    
  2. 修改user/usys.pl (用以实现系统调用)

    entry("fork");
    entry("exit");
    entry("wait");
    entry("pipe");
    entry("read");
    entry("write");
    entry("close");
    entry("kill");
    entry("exec");
    entry("open");
    entry("mknod");
    entry("unlink");
    entry("fstat");
    entry("link");
    entry("mkdir");
    entry("chdir");
    entry("dup");
    entry("getpid");
    entry("sbrk");
    entry("sleep");
    entry("uptime");
    	// 添加如下两行
    entry("sigalarm");
    entry("sigreturn");
    
  3. 修改user/user.h(都是Lab2中的添加新的系统调用的操作)

    // system calls
    
    ...
    
    int getpid(void);
    char* sbrk(int);
    int sleep(int);
    int uptime(void);
    	// 添加如下2行
    int sigalarm(int ticks, void (*handler)());
    int sigreturn(void);
    
  4. 修改kernel/syscall.h

    // System call numbers
    
    ...
    
    #define SYS_unlink 18
    #define SYS_link   19
    #define SYS_mkdir  20
    #define SYS_close  21
    	// 添加如下2行
    #define SYS_sigalarm 22
    #define SYS_sigreturn 23
    
  5. 修改kernel/syscall.c

    ...
    extern uint64 sys_wait(void);
    extern uint64 sys_write(void);
    extern uint64 sys_uptime(void);
    	// 声明在别处定义的两个系统调用函数
    extern uint64 sys_sigalarm(void);
    extern uint64 sys_sigreturn(void);
    
    ...
        // 将新增的系统调用按顺序添加到系统调用数组中
    static uint64 (*syscalls[])(void) = {
    [SYS_fork]    sys_fork,
    [SYS_exit]    sys_exit,
    [SYS_wait]    sys_wait,
    [SYS_pipe]    sys_pipe,
    [SYS_read]    sys_read,
    [SYS_kill]    sys_kill,
    [SYS_exec]    sys_exec,
    [SYS_fstat]   sys_fstat,
    [SYS_chdir]   sys_chdir,
    [SYS_dup]     sys_dup,
    [SYS_getpid]  sys_getpid,
    [SYS_sbrk]    sys_sbrk,
    [SYS_sleep]   sys_sleep,
    [SYS_uptime]  sys_uptime,
    [SYS_open]    sys_open,
    [SYS_write]   sys_write,
    [SYS_mknod]   sys_mknod,
    [SYS_unlink]  sys_unlink,
    [SYS_link]    sys_link,
    [SYS_mkdir]   sys_mkdir,
    [SYS_close]   sys_close,
        //新增
    [SYS_sigalarm] sys_sigalarm,
    [SYS_sigreturn] sys_sigreturn,
    
    };
    
  6. kernel/proc.h中在proc结构体中增加新的字段,用以保存进程进行Alarm操作的相关信息(提示中已给出)

    // Per-process state
    struct proc {
      struct spinlock lock;
    
      // p->lock must be held when using these:
      enum procstate state;        // Process state
      struct proc *parent;         // Parent process
      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
    
      // 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 alarm_interval;          // 报警间隔
      void (*alarm_handler)();     // 指向报警处理函数的指针
      int ticks_count;             // 两次报警间的滴答计数
    };
    
  7. 并对新增的两个字段进行相应的allocfree操作,在kernel/proc.callocproc() freeproc()

    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();
    
      // Allocate a trapframe page.
      if((p->trapframe = (struct trapframe *)kalloc()) == 0){
        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;
    
      
      p->alarm_handler = 0;
      p->alarm_interval = 0;
      p->ticks_count = 0;
      return p;
    }
    
    static void
    freeproc(struct proc *p)
    {
      if(p->trapframe)
        kfree((void*)p->trapframe);
      p->trapframe = 0;
      if(p->pagetable)
        proc_freepagetable(p->pagetable, p->sz);
      p->pagetable = 0;
      p->sz = 0;
      p->pid = 0;
      p->parent = 0;
      p->name[0] = 0;
      p->chan = 0;
      p->killed = 0;
      p->xstate = 0;
      p->state = UNUSED;
      p->alarm_handler = 0;
      p->alarm_interval = 0;
      p->ticks_count = 0;
    }
    
  8. 下面是对新增的两个系统调用的具体实现,在kernel/sysproc.c

    // 根据给出的提示
    uint64
    sys_sigalarm(void)
    {
        // p->alarm_interval = p->trapframe->a0 
        // (uint64*)p->alarm_handler = p->trapframe->a1
      if(argint(0,&myproc()->alarm_interval) < 0 || argaddr(1,(uint64*)&myproc()->alarm_handler) < 0)
        return -1;
    
      return 0;
    }
    
    uint64
    sys_sigreturn(void)
    {
      return 0;
    }
    
  9. 完善kernel/trap.c

    // give up the CPU if this is a timer interrupt.
      if(which_dev == 2){
        if(++p->ticks_count == p->alarm_interval){    //滴答计数达到设定的报警间隔
    
          //滴答计数归0,设置陷阱帧中保留的程序计数器
          p->ticks_count = 0;
          p->trapframe->epc = (uint64)p->alarm_handler;
    
        }
    
        yield();    
      }
    
      usertrapret();
    
  10. 测试效果

    test0 passed

test1/2

需要解决的主要问题是 寄存器保存恢复和防止重复执行的问题。

没有alarm时运行的大致过程

  1. 进入kernel space,保存用户寄存器到p->trapframe
  2. 陷阱处理过程
  3. 恢复用户寄存器,返回user space

在test0,添加alarm后,过程有些改变

  1. 进入kernel space, 保存用户寄存器到p->trapframe
  2. 陷阱处理过程
  3. 恢复用户寄存器,返回user space。但是,此时返回的不是trap时的程序地址,而是处理函数handler的地址,而handler有可能会改变用户寄存器。

因此,我们需要在usertrap中再次保存用户寄存器,当handler调用sigreturn时再次恢复,并且要防止handler执行过程中重复调用

实验过程如下

  1. 再在kernel/proc.hstruct proc中新增两个字段

    int is_alarming;             // 是否正在执行警告处理函数----handler函数
    struct trapframe* alarm_trapframe;  // 用来备份用户寄存器的警告陷阱帧
    
  2. kernel/proc.c中,修改allocproc()和freeproc(),设定好相关分配和回收的代码

    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();
    
      // Allocate a trapframe page.
      if((p->trapframe = (struct trapframe *)kalloc()) == 0){
        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;
    
    
      // 初始化警告字段
      if((p->alarm_trapframe = (struct trapframe*)kalloc()) == 0){
        freeproc(p);
        release(&p->lock);
        return 0;
      }
      p->alarm_handler = 0;
      p->alarm_interval = 0;
      p->ticks_count = 0;
      p->is_alarming = 0;
      return p;
    }
    
    static void
    freeproc(struct proc *p)
    {
      if(p->trapframe)
        kfree((void*)p->trapframe);
      p->trapframe = 0;
      if(p->pagetable)
        proc_freepagetable(p->pagetable, p->sz);
      if(p->alarm_trapframe)	//释放备份的警告陷阱帧
        kfree((void*)p->alarm_trapframe);
      p->pagetable = 0;
      p->sz = 0;
      p->pid = 0;
      p->parent = 0;
      p->name[0] = 0;
      p->chan = 0;
      p->killed = 0;
      p->xstate = 0;
      p->state = UNUSED;
        // 回收警告字段
      p->is_alarming = 0;
      p->alarm_trapframe = 0;
      p->alarm_handler = 0;
      p->alarm_interval = 0;
      p->ticks_count = 0;
    }
    
  3. 修改kernel/trap.c 中的 usertrap()

    ...
    // give up the CPU if this is a timer interrupt.
      if(which_dev == 2){
        if(++p->ticks_count == p->alarm_interval && p->alarm_interval != 0 && p->is_alarming == 0){    
          
          // 保存用户寄存器的内容
          memmove(p->alarm_trapframe, p->trapframe, sizeof(struct trapframe));
    
          //滴答计数归0,设置陷阱帧中保留的程序计数器
          p->ticks_count = 0;
          p->trapframe->epc = (uint64)p->alarm_handler;
          p->is_alarming = 1;   //标记正在执行handler函数
    
        }
    
        yield();    
      }
     ...
    
  4. 修改kernel/sysproc.c中的sys_sigreturn

    uint64
    sys_sigreturn(void)
    {
      // 恢复陷阱帧
      memmove(myproc()->trapframe, myproc()->alarm_trapframe, sizeof(struct trapframe));
      myproc()->is_alarming = 0;
      return 0;
    }
    

测试结果:passed。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值