操作系统实验Mit6.S081笔记 Lab4: Traps

RISC-V assembly (easy)

要求:

理解一点RISC-V组装是很重要的,这在6.004中已经介绍过了。 在您的xv6 repo中有一个文件user/call.c。make fs.img编译它,并在user/call.asm中生成程序的可读汇编版本。

阅读call.asm中函数g、f和main的代码。RISC-V的使用说明书在参考页。 这里有一些你应该回答的问题(将答案存储在文件answers-traps.txt中):

Backtrace (moderate)

要求:

对于调试来说,使用回溯跟踪通常是很有用的:在错误发生点之上的堆栈上的函数调用列表。

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

backtrace:
0 x0000000080002cda
0 x0000000080002bb6
0 x0000000080002898

bttest后退出qemu。 在你的终端中:地址可能略有不同,但如果你运行addr2line -e kernel/kernel(或riscv64-unknown-elf-addr2line -e kernel/kernel),然后剪切和粘贴上面的地址如下:
$ addr2line -e kernel/kernel
0 x0000000080002de2
0 x0000000080002f4a
0 x0000000080002bfc
ctrl - d

你应该看到这样的东西:
kernel/ sysproc.c: 74
kernel/ syscall.c: 224
kernel/ trap.c: 85

编译器在每个栈帧中放入一个包含调用者帧指针地址的帧指针。 反向跟踪应该使用这些帧指针遍历栈帧,并在打印每个栈帧中保存的返回地址。

提示:

将backtrace的原型添加到kernel/defs.h中,这样就可以在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.c中的panic中调用它,这样你就可以看到当内核发生panic时的回溯。

代码:
/*
riscv.h

提示中直接给出了代码
*/
static inline uint64
r_fp()
{
    uint64 x;
    asm volatile("mv %0,s0" : "=r"(x));//使用内嵌程序集,将当前执行函数的帧指针存储在寄存器s0中
    return x;
}
/*
printf.c

不太明白这边为什么要对齐啊
*/
void
backtrace(void)
{
    printf("backtrace:\n");
    uint64 fp=r_fp();//读取当前帧指针
    uint64 pg=PGROUNDDOWN(fp);//定义在riscv.h中的PGROUNDDOWN(fp)(往低地址处对齐)和PGROUNDUP(fp)(往高地址处对齐)可用来计算堆栈页的顶部和底部地址 用于backtrace终止循环
    uint64 ra;
    while(PGROUNDDOWN(fp)==pg){
        ra=*(uint64*)(fp-8);//固定的返回地址与堆栈帧指针的偏移量(-8)
        fp=*(uint64*)(fp-16);//固定的保存的帧指针与帧指针的偏移量(-16)
        printf("%p\n",ra);
    }
}
/*
sysproc.c

在sys_sleep中,调用backtrace
*/
uint64
sys_sleep(void)
{
  backtrace();//调用backtrace
  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);
  return 0;
}
结果:

在这里插入图片描述
在这里插入图片描述

Alarm (hard)

要求:

在本练习中,您将向xv6添加一个特性,该特性在进程使用CPU时间时定期发出警报。 对于想要限制占用CPU时间的计算受限进程,或者想要进行计算但又想采取一些周期性操作的进程,这可能很有用。 更一般地,您将实现用户级中断/故障处理程序的基本形式; 例如,您可以使用类似的方法来处理应用程序中的页面错误。 如果您的解决方案通过alarmtest和usertests,那么它就是正确的。

您应该添加一个新的sigalarm(interval, handler)系统调用。 如果应用程序调用sigalarm(n, fn),那么在该程序消耗的每n个CPU时间之后,内核应该调用应用程序函数fn。 当fn返回时,应用程序应该恢复到它停止的地方。 在xv6中,tick是一个相当随意的时间单位,由硬件计时器生成中断的频率决定。 如果应用程序调用sigalarm(0,0),内核应该停止生成周期性的警报调用。

您将在您的xv6存储库中找到文件user/alarmtest.c。 将其添加到Makefile中。 只有添加了sigalarm和sigreturn系统调用(见下文),它才能正确编译。

Alarmtest在test0中调用sigalarm(2, periodic),要求内核强制每隔2个节拍调用periodic(),然后旋转一段时间。 您可以在user/alarmtest中看到alarmtest的程序集代码, asm,这对调试可能很方便。 当alarmtest产生如下输出时,您的解决方案是正确的,用户测试也能正确运行:
$ 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:调用处理程序
首先修改内核,使其跳转到用户空间中的告警处理程序,这将导致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.h和kernel/syscall.c以允许alarmtest调用sigalarm和sigreturn系统调用。
现在,sys_sigreturn应该只是返回0。
您的sys_sigalarm()应该在proc结构(在kernel/proc.h中)的新字段中存储告警间隔和处理程序函数的指针。
您需要跟踪自上次调用(或直到下一次调用)以来传递给进程的警报处理程序的节拍数; 为此,在struct proc中也需要一个新的字段。 您可以在proc.c中的allocproc()中初始化proc字段。
每个ticks,硬件时钟强制一个中断,这是在kernel/trap.c中的usertrap()中处理的。
您只希望在出现计时器中断时操纵进程的警报滴答声; 你想要
If (which_dev == 2)…
只有在进程有一个未完成的计时器时才调用警报功能。 注意,用户报警功能的地址可能是0(例如,在user/alarmtest中。 Asm,周期在地址0)。
您需要修改usertrap(),以便当一个进程的告警间隔过期时,用户进程执行该处理函数。 当RISC-V上的一个陷阱返回到用户空间时,是什么决定了用户空间代码恢复执行的指令地址?
如果您告诉qemu只使用一个CPU,那么使用gdb查看陷阱将更容易,您可以通过运行
cpu = 1 qemu-gdb
如果alarmtest打印“alarm!”,则成功。

test1 /test2():恢复中断代码
可能是alarmtest在输出"alarm!“后在test0或test1中崩溃,或者alarmtest(最终)输出"test1 failed”,或者alarmtest退出而没有输出"test1 passed"。 要解决这个问题,您必须确保,当警报处理程序完成时,控制返回到用户程序最初被计时器中断中断的指令。 您必须确保寄存器内容恢复到中断时的值,以便用户程序在报警后可以继续不受干扰。 最后,您应该在每次警报计数器关闭后“重新武装”它,以便定期调用处理程序。

作为起点,我们已经为您做了一个设计决策:用户警报处理程序需要在完成时调用sigreturn系统调用。 以alarmtest.c中的periodic为例。 这意味着您可以向usertrap和sys_sigreturn中添加代码,以便在用户进程处理告警后能够正常恢复。

提示:

您的解决方案将要求您保存和恢复寄存器——为了正确地恢复中断的代码,您需要保存和恢复哪些寄存器? (提示:会有很多)。
让usertrap在struct proc中保存足够的状态,当计时器停止时,sigreturn可以正确地返回到被中断的用户代码。
防止对处理器的重入调用,如果处理器还没有返回,内核就不应该再次调用它。 test2测试它。
一旦您通过了test0、test1和test2,运行用户测试以确保您没有破坏内核的任何其他部分。

代码:
/*
user.h
新增系统调用
*/
int sigalarm(int ticks,void (*handler)());
int sigreturn(void);
/*
proc.h
*/
void (*handler)();
int ticks;
int remainticks;
struct trapframe trapframe_bak;
/*
sysproc

alarm函数 和 返回函数
*/
uint64
sys_sigalarm(void)
{
    int ticks;
    if(argint(0,&ticks)<0)
        return -1;
    uint64 addr;
    if(argaddr(1,&addr)<0)
        return -1; 
    struct proc *p=myproc();
    p->handler=(void(*)())addr;
    p->ticks=ticks;
    p->remainticks=ticks;
    return 0;
}
uint64
sys_sigreturn(void)
{
    struct proc* p=myproc();
    *p->trapframe=p->trapframe_bak;
    p->remainticks=p->ticks;
    return 0;
}
/*
trap.c
*/
  if(which_dev==2){//在出现计时器中断时操纵进程的警报滴答
    if(p->ticks>0&&--p->remainticks==0){
      p->trapframe_bak=*p->trapframe;
      p->trapframe->epc=(uint64)p->handler;//handler存储用户函数,需要返回用户态才能调用
    }
  }
  usertrapret();
}
结果:

在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值