【MIT6.S081】Lab4: traps(详细解答版)

2024-04-21 17:28
文章标签 详细 解答 s081 mit6 traps lab4

本文主要是介绍【MIT6.S081】Lab4: traps(详细解答版),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

实验内容网址:https://xv6.dgs.zone/labs/requirements/lab4.html
本实验的代码分支:https://gitee.com/dragonlalala/xv6-labs-2020/tree/traps2/

Backtrace

关键点:trapframe、栈

思路:

这道题的关键是栈结构,先阅读xv6中关于栈的知识(https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/lec05-calling-conventions-and-stack-frames-risc-v/5.5-stack)。
[图片]

阅读链接的资料,我们可以知道,

  1. 每调用一个函数,函数都会为自己创建一个Stack Frame。
  2. 栈是从高地址开始向低地址使用。所以栈总是向下增长。
  3. Return address总是会出现在Stack Frame的第一位,指向前一个Stack Frame的指针也会出现在栈中的固定位置
  4. 第一个寄存器是SP(Stack Pointer),它指向Stack的底部并代表了当前Stack Frame的位置。第二个寄存器是FP(Frame Pointer),它指向当前Stack Frame的顶部。FP-8位是返回地址,FP-16位是上一个stack frame的fp地址。
  5. 保存前一个Stack Frame的指针的原因是为了让我们能跳转回去。
    XV6在内核中以页面对齐的地址为每个栈分配一个页面。你可以通过PGROUNDDOWN(fp)和PGROUNDUP(fp)(参见kernel/riscv.h)来计算栈页面的顶部和底部地址。

步骤&代码:

  1. 将下面的函数添加到kernel/riscv.h
static inline uint64
r_fp()
{uint64 x;asm volatile("mv %0, s0" : "=r" (x) );return x;
}
  1. 在kernel/defs.h中添加backtrace的原型,在printf.c中定义backtrace函数。
void       
backtrace(void){// 读取当前Frame Pointeruint64 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);}
}
  1. 在sys_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;
}

未解:

这里说的stack是存放在用户页表中的吗

Alarm

关键点:从用户空间陷入陷阱、epc寄存器、trapframe

思路:(参考自https://xv6.dgs.zone/labs/answers/lab4.html)

这项练习要实现定期的警报。首先是要通过test0,如何调用处理程序是主要的问题。程序计数器的过程是这样的:

  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中主要就是完成它的设置工作。
    接下来要通过test1和test2,要解决的主要问题是寄存器保存恢复和防止重复执行的问题。考虑一下没有alarm时运行的大致过程
  7. 进入内核空间,保存用户寄存器到进程陷阱帧
  8. 陷阱处理过程
  9. 恢复用户寄存器,返回用户空间
    而当添加了alarm后,变成了以下过程
  10. 进入内核空间,保存用户寄存器到进程陷阱帧
  11. 陷阱处理过程
  12. 恢复用户寄存器,返回用户空间,但此时返回的并不是进入陷阱时的程序地址,而是处理函数handler的地址,而handler可能会改变用户寄存器
    因此我们要在usertrap中再次保存用户寄存器,当handler调用sigreturn时将其恢复,并且要防止在handler执行过程中重复调用。
    步骤&代码:
    根据题目的提示进行编程
  13. 需要修改Makefile以使alarmtest.c被编译为xv6用户程序。
UPROGS=\
...$U/_zombie\$U/_alarmtest\
  1. 在user/user.h中放入函数声明:
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);
  1. 更新user/usys.pl(此文件生成user/usys.S)、kernel/syscall.h和kernel/syscall.c以允许alarmtest调用sigalarm和sigreturn系统调用。
# user/usys.pl文件中
...
entry("sleep");
entry("uptime");
entry("sigalarm");
entry("sigreturn");// syscall.h文件添加
#define SYS_sigalarm  22
#define SYS_sigreturn  23// syscall.c文件中
... 
extern uint64 sys_uptime(void);
extern uint64 sys_sigalarm(void);// 新添加
extern uint64 sys_sigreturn(void);// 新添加... 
[SYS_mkdir]   sys_mkdir,
[SYS_close]   sys_close,
[SYS_sigalarm]   sys_sigalarm,// 新添加
[SYS_sigreturn]   sys_sigreturn,// 新添加
...
  1. 在sysproc.c文件中添加函数
// 新添加
uint64 sys_sigalarm(void){return 0;
}
uint64 sys_sigreturn(void){return 0;
}
  1. 根据提示在struct proc 的定义中添加新字段,分别代表报警间隔,指向处理程序函数的指针,用于跟踪自上次调用到进程的报警处理程序间经历了多少滴答。
 int alarm_interval;           // 报警间隔void (*alarm_handle);           // 报警处理函数int last_tick_time;           // 上次报警的滴答时刻
  1. 初始化进程时初始化上面的三个新参数。
// 在allocproc函数中添加...memset(&p->context, 0, sizeof(p->context));p->context.ra = (uint64)forkret;p->context.sp = p->kstack + PGSIZE;p->alarm_handle = 0;p->alarm_interval = 0;p->last_tick_time = 0;...
  1. 根据提示,每一个滴答声,硬件时钟就会强制一个中断,这个中断在kernel/trap.c中的usertrap()中处理。因此在usertrap()中添加如下代码
// give up the CPU if this is a timer interrupt.if(which_dev == 2){// 获取当前的时钟数int xticks;acquire(&tickslock);xticks = ticks;release(&tickslock);if(xticks - p->last_tick_time >= p->alarm_interval && p->alarm_interval != 0 && p->alarm_handle != 0 ){// 调用处理函数p->trapframe->epc = (uint64)p->alarm_handle;// 更新上次的时钟p->last_tick_time = xticks;}yield();}

上面这个代码中获取时间的方式可能比较的弯弯绕绕。其实可以添加一个tick_count的字段,因为每次滴答中断都会进入这段代码,直接对这个变量进行累积即可。
8. 对sys_sigalarm函数进行编写,获得报警间隔和报警处理函数,并将值赋给新添加的字段

uint64 sys_sigalarm(void){int interval;uint64 handle;if(argint(0, &interval) < 0)return -1;if(argaddr(0, &handle) < 0)return -1;myproc()->alarm_interval = interval;myproc()->alarm_handle = (void *)handle;return 0;
}

上面8个步骤可以通过test0。但通不过test1 2,会出现panic。原因是执行完报警处理函数后返回用户空间,但此时返回的并不是进入陷阱时的程序地址,而是处理函数handler的地址,而handler可能会改变用户寄存器。必须确保完成报警处理程序后返回到用户程序最初被计时器中断的指令执行。必须确保寄存器内容恢复到中断时的值,以便用户程序在报警后可以不受干扰地继续运行。
8. 为了实现备份寄存器与防止对处理程序的重复调用,在struct proc定义中添加2个新字段。

 struct trapframe *trapframe_copy; // data page for trampoline.S copyint is_alarming;      
  1. 初始化进程时对这两个字段进行初始化。在allocproc函数中添加如下代码:
 ...// Allocate a trapframe page.if((p->trapframe = (struct trapframe *)kalloc()) == 0){release(&p->lock);return 0;}// Allocate a trapframe page(copy).if((p->trapframe_copy = (struct trapframe *)kalloc()) == 0){release(&p->lock);return 0;}......p->alarm_interval = 0;p->last_tick_time = 0;p->is_alarming = 0;...
  1. 进程释放时也要对新增的trapframe进行释放,因为开辟了一页内存空间,需要释放,在freeproc函数中添加:
 if(p->trapframe)kfree((void*)p->trapframe);p->trapframe = 0;if(p->trapframe_copy)kfree((void*)p->trapframe_copy);p->trapframe_copy = 0;...
  1. 在usertrap函数中,进入报警处理函数前,需要备份一下trapframe。修改usertrap函数代码
// give up the CPU if this is a timer interrupt.if(which_dev == 2){// 获取当前的时钟数int xticks;acquire(&tickslock);xticks = ticks;release(&tickslock);if(xticks - p->last_tick_time >= p->alarm_interval && p->alarm_interval != 0 && p->alarm_handle != 0 && p->is_alarming == 0){// 设置为正处于报警程序p->is_alarming = 1;// 备份trapframememmove(p->trapframe_copy, p->trapframe, sizeof(struct trapframe)); // 调用处理函数p->trapframe->epc = (uint64)p->alarm_handle;// 更新上次的时钟p->last_tick_time = xticks;}yield();}
  1. 补充sys_sigreturn函数,这个函数是由测试程序调用的。在执行完报警处理函数后执行。使得trapframe中寄存器的值恢复成报警前的状态。
uint64 sys_sigreturn(void){// 还原寄存器memmove(myproc()->trapframe, myproc()->trapframe_copy, sizeof(struct trapframe));myproc()->is_alarming = 0;return 0;
}

这篇关于【MIT6.S081】Lab4: traps(详细解答版)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/923639

相关文章

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

Spring AI集成DeepSeek的详细步骤

《SpringAI集成DeepSeek的详细步骤》DeepSeek作为一款卓越的国产AI模型,越来越多的公司考虑在自己的应用中集成,对于Java应用来说,我们可以借助SpringAI集成DeepSe... 目录DeepSeek 介绍Spring AI 是什么?1、环境准备2、构建项目2.1、pom依赖2.2

Goland debug失效详细解决步骤(合集)

《Golanddebug失效详细解决步骤(合集)》今天用Goland开发时,打断点,以debug方式运行,发现程序并没有断住,程序跳过了断点,直接运行结束,网上搜寻了大量文章,最后得以解决,特此在这... 目录Bug:Goland debug失效详细解决步骤【合集】情况一:Go或Goland架构不对情况二:

Python itertools中accumulate函数用法及使用运用详细讲解

《Pythonitertools中accumulate函数用法及使用运用详细讲解》:本文主要介绍Python的itertools库中的accumulate函数,该函数可以计算累积和或通过指定函数... 目录1.1前言:1.2定义:1.3衍生用法:1.3Leetcode的实际运用:总结 1.1前言:本文将详

Deepseek R1模型本地化部署+API接口调用详细教程(释放AI生产力)

《DeepseekR1模型本地化部署+API接口调用详细教程(释放AI生产力)》本文介绍了本地部署DeepSeekR1模型和通过API调用将其集成到VSCode中的过程,作者详细步骤展示了如何下载和... 目录前言一、deepseek R1模型与chatGPT o1系列模型对比二、本地部署步骤1.安装oll

Spring Boot整合log4j2日志配置的详细教程

《SpringBoot整合log4j2日志配置的详细教程》:本文主要介绍SpringBoot项目中整合Log4j2日志框架的步骤和配置,包括常用日志框架的比较、配置参数介绍、Log4j2配置详解... 目录前言一、常用日志框架二、配置参数介绍1. 日志级别2. 输出形式3. 日志格式3.1 PatternL

Springboot 中使用Sentinel的详细步骤

《Springboot中使用Sentinel的详细步骤》文章介绍了如何在SpringBoot中使用Sentinel进行限流和熔断降级,首先添加依赖,配置Sentinel控制台地址,定义受保护的资源,... 目录步骤 1: 添加 Sentinel 依赖步骤 2: 配置 Sentinel步骤 3: 定义受保护的

本地私有化部署DeepSeek模型的详细教程

《本地私有化部署DeepSeek模型的详细教程》DeepSeek模型是一种强大的语言模型,本地私有化部署可以让用户在自己的环境中安全、高效地使用该模型,避免数据传输到外部带来的安全风险,同时也能根据自... 目录一、引言二、环境准备(一)硬件要求(二)软件要求(三)创建虚拟环境三、安装依赖库四、获取 Dee

MySql9.1.0安装详细教程(最新推荐)

《MySql9.1.0安装详细教程(最新推荐)》MySQL是一个流行的关系型数据库管理系统,支持多线程和多种数据库连接途径,能够处理上千万条记录的大型数据库,本文介绍MySql9.1.0安装详细教程,... 目录mysql介绍:一、下载 Mysql 安装文件二、Mysql 安装教程三、环境配置1.右击此电脑

Git提交代码详细流程及问题总结

《Git提交代码详细流程及问题总结》:本文主要介绍Git的三大分区,分别是工作区、暂存区和版本库,并详细描述了提交、推送、拉取代码和合并分支的流程,文中通过代码介绍的非常详解,需要的朋友可以参考下... 目录1.git 三大分区2.Git提交、推送、拉取代码、合并分支详细流程3.问题总结4.git push