lab4 traps

news2024/9/25 13:19:57

image-20230821172327132

在开始做lab之前务必弄清楚

  1. 所谓的系统调用,本质上就是内核态和用户态之间的切换
  2. 内核态和用户态的区别本质上就是一些关键属性的区别,比如页表
    而他们的运行方式都一样,就是取指执行,没有魔法
    只不过内核态和用户态的执行不在一个体系上,所以切换会比较复杂,但这些切换也就只是修改一些关键属性
  3. 进程的运行本质上就是一些值不断的变化,trapframe中的值完全可以保存并复现一个进程执行到了哪里加粗样式

PreRead

  • xv6 book的第四章
  • kernel/trampoline.S:在用户态和内核态进行切换的汇编代码
  • kernel/trap.c:处理陷入的代码

RISC-V assembly

task

有一个文件user/call.c,通过make fs.img可以编译并生成一个可读性很高的汇编代码user/call.asm

阅读这个汇编代码中的函数g,f,main,并回答以下问题

1. a0-a7,a2
2. 被优化了?
3. 630
4. 38
5. He110 World,第一个是直接输出十六进制的表示,第二个是将十六进制的每个字节看做一个字符,并且是小端法
   将i改成0x726c6400,57616不用改
6. 随机值,因为相当于调用printf的时候没有给出寄存器a2的值,那么这时候就会根据a2的值随机出现答案

Backtrace

task

如果能够清楚在错误发生之前的一系列函数调用,那么对debug很有帮助

  1. kernel/printf.c中实现一个backtrace()函数

    kernel文件夹里的,不是user文件夹里,user文件夹里也有一个printf.c

  2. sys_sleep中插入对backtrace函数的调用

  3. 运行bttest,它会调用sys_sleep,你的输出应该是

    backtrace:
    0x0000000080002cda
    0x0000000080002bb6
    0x0000000080002898
    

    bttest结束之后,在你的终端中运行

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

    你会看到

        kernel/sysproc.c:74
        kernel/syscall.c:224
        kernel/trap.c:85
    
  4. 编译器会给每个栈帧一个frame pointer,你应该使用这个指针去遍历栈并且打印每个栈帧中保存的返回地址

hints

  1. 记得在kernel/defs.h中声明你的backtrace函数

  2. GCC编译器将栈指针存放在当前执行函数的s0寄存器中,将下面这个函数添加到kernel/riscv.h中,并且在backtrace函数中调用它

    static inline uint64
    r_fp()
    {
      uint64 x;
      asm volatile("mv %0, s0" : "=r" (x) );
      return x;
    }
    
  3. 返回地址位于fp-8的固定位置

    被保存的栈帧地址位于fp-16的固定位置

    image-20230821111932007

  4. 你可以使用这两个宏,有助于终止backtrace的循环

    PGROUNDDOWN(fp)计算栈顶的位置

    PGROUNDUP(fp)计算栈底的位置

  5. 如果你的backtrace工作了,可以在kernel/printf.cpanic调用它,这样一旦内核出错了,你就可以看到调用栈

思路

一行一行地打印函数调用的地址,思路比较简单

  1. 首先通过文档提供给我们的r_fp函数获得当前的fp
  2. 通过这个fp不断打印这个函数的返回地址,并将fp置为上一个函数的fp,具体位置如上面那个栈帧的图所示

具体实现上

  1. 需要通过hints里提示的宏,找到栈帧的终点,也是我们循环的终点。
    1. 注意了,这个终点也就是最高层的函数,它不需要继续打印了,而它自己也被它调用的函数打印了,因此就直接结束
    2. fp这个指针,并不能直接取值,将它减8,它才正好指向当前栈帧底部的第一个值,注意!
  2. 如果想打印出64位,并在前面补0,前面还加上0x,用%p就可以打印出来了,不用自己瞎搞,chatgpt还忽悠人
void backtrace() {
    printf("backtrace:\n");
    uint64 fp = r_fp();
    uint64 up_edge = PGROUNDUP(fp);
    while (fp < up_edge) {
        printf("%p\n", *(uint64 *)(fp - 8));
        fp = *(uint64 *)(fp - 16);
    }
}

Alarm

task

在这个练习中,你将给xv6增加一个特性,即会根据CPU时间周期性地alert一个进程

如果你的解答可以通过alarmtestusertests

  1. 你应该添加一个新的系统调用sigalarm(interval,handler)

  2. 如果一个应用调用了sigalarm(n,fn)

    那么在这个应用消耗了n ticks个CPU时间后,内核会调用函数fn

    fn返回后,应用会回到被打断的地方继续执行

  3. 如果一个应用调用了sigalarm(0,0),内核应该暂停生成周期性的alarm

  4. 在你的xv6文件中有一个文件叫user/alarmtest.c,你需要将其加入到Makefile

    只有你正确添加了sigalarmsigreturn系统调用之后,才可以正确编译

hints1

  • 首先修改内核,跳转到用户空间的alarm handler

    这将让test0打印alarm!

  • 现在还不用管打印之后发生了什么,你的程序在打印之后崩了就行

  1. 你需要去修改Makefile,让它去编译alarmtest.c成为用户程序

  2. user/user.h中正确的声明应该如下

        int sigalarm(int ticks, void (*handler)());
        int sigreturn(void);
    
  3. 更新user/usys.plkernel/syscall.hkernel/syscall.c

    使得alarmtest能够调用sigalarmsigreturn系统调用

  4. 至此你的sys_sigreturn应该只返回0

    你的sys_sigalarm应该存下alarm的间隔和处理函数的指针到proc结构体的新的区域

  5. 你需要去跟踪自从上次调用alarm的处理函数到现在已经过去了多少ticks

    这也需要在struct proc中增加一个新的字段,你可以在proc.c 的 allocproc()中初始化这个字段

  6. 每次来一个tick,都会在kernel/trap.cusertrap中被处理

    你只需要在有时钟中断的时候操作

  7. 只需要在一个进程有 t i m e r   o u t s t a n d i n g timer\ outstanding timer outstanding的时候调用alarm函数

    小心函数地址为0的情况,因为函数地址可以为0,我是傻逼!

  8. 你将需要去修改usertrap函数,使得当一个进程的alarm时间间隔到期时,用户进程执行处理函数

    当一个陷入返回到用户空间时,是什么决定着用户空间代码继续执行的指令地址?

  9. 如果你运行make CPUS=1 qemu-gdb,会使用用gdb查看trap的时候更容易

  10. 如果alarmtest打印了alarm!,你就成功了

思路1

  1. 首先根据它的提示去各个文件中把系统调用的声明给弄好

  2. 然后在struct proc中增加如下字段,其中关键在于uint64 handler,它是函数指针,不过终究也就是个指针,因此可以用uint64来表示

        int cur_ticks;
        uint64 handler;
        int ticks;
    
  3. sysprorc.c中完成sys_sigalarmsys_sigreturn

    uint64
    sys_sigalarm(void) {
        struct proc *p;
        p = myproc();
        argint(0, &p->ticks);
        argaddr(1, &p->handler);
        p->cur_ticks = 0;
        return 0;
    }
    
    uint64
    sys_sigreturn(void) {
        return 0;
    }
    
  4. 最后在trap.c中完成调用

    注意了,函数指针可能是0,所以用ticks是否为0判断是否需要计数

        if (which_dev == 2) {
            if (p->ticks != 0) {
                p->cur_ticks++;
                if (p->cur_ticks == p->ticks) {
                    p->cur_ticks = 0;
                    p->trapframe->epc = p->handler;
                }
            }
            yield();
        }
    

    这里的实现是如果当前已经到了第n个时钟中断,那么会先去中断,等下一次获得cpu使用权时,再去执行handler操作

    我试了一下在放弃cpu之前直接p->handler(),结果不允许

    估计是因为地址的原因,现在可是在内核态,怎么可能能够通过这个用户态的虚拟地址来执行

    所以,只能等到这个进程再次获得CPU并且回归用户态用,就会用epc这个参数来初始化pc,就会从这里开始执行了

hints2

你需要在执行完alarm处理函数之后,正确返回程序被中断的地方,并且各种寄存器的状态也要不变

xv6已经为实现提供了一种思路,即每个alarm处理函数的最后都有一个alarmreturn函数,你可以通过usertrapsys_sigreturn合作来完成用户进程的恢复

  1. 你将需要保存和恢复寄存器,很多很多
  2. struct proc中保存足够多的状态,使得你可以在sigreturn中恢复
  3. 如果一个处理函数还没有结束,内核不应该再次调用它

思路2

到了这一步,必须要先搞清楚系统调用的过程中对于状态的保存和恢复了

  1. uservec保存了各种常用的寄存器
  2. usertrap将返回的pc地址存到了p->trapframe->epc
  3. usertrapret通过p->trapframe->epc恢复pc
  4. userret恢复各种寄存器

首先,我们希望在时钟中断之后,这个进程被调度回来的时候,去执行alarm处理函数,因此我们需要在时钟中断的处理中,将epc置为处理函数的地址,这样就完成了task0

但是如果只是这样的话,这个进程在执行完alarm处理函数之后并不能正确的返回需要执行的地方。那如何正确的返回呢?

可以发现,alarm处理函数的最后一句通常是alarmreturn,这是一个系统调用!如果我们能够在这个系统调用返回之前将trapframe(因为trapframe包括了所有返回用户态需要的信息,所以我们只需要这个就行了)变成在时钟中断处理之前的样子,那么就可以借用alarmreturn这个系统调用的返回操作回到我们想去的地方

而需要注意的是,如果已经执行了alarm处理函数,那此时的trapframe肯定是不行的,因为包括pc和各种通用寄存器都被破坏的,那哪个时间点的trapframe可以呢?

答案是刚进入if (which_dev == 2) 的时候,想一想,如果我们不需要搞这个什么alarm,那么等之后这个进程再次被调度到cpu之后,那不就是继续正常执行吗?说明这个时间点的trapframe可以通过任何一个系统调用的返回过程使得进程执行到继续执行的地方

不过我们也没必要每次进入这里都保存了,只需要在确定了会去执行alarm处理函数的时候保存,在alarmreturn中恢复即可

除此之外,题目还要求如果已经有一个alarm处理函数在执行,那么其他的必须等待,因此额外增加一个变量代表是否有在执行

具体实现如下

首先给proc结构体增加如下变量

    struct trapframe *alarm_tf;
    int is_runing;

并且在进程初始化和终止的时候对这两个变量进行处理

    // 进程初始化,这里主要是防止申请不成功,那就学着已有的代码对进程进行销毁
	p->alarm_tf = (struct trapframe *)kalloc();
    if (p->alarm_tf == 0) {
        freeproc(p);
        release(&p->lock);
        return 0;
    }
    p->is_runing = 0;
	// 进程结束
	kfree(p->alarm_tf);

trap.c中的代码进行如下更新

  • 注意p->cur_ticks >= p->ticks,这里变成大于等于,是为了保证现在可以等,但之后如果没有正在运行的了,那就可以进入alarm处理函数的流程。如果和之前一样是等于号的好,那可能就错过了
    if (which_dev == 2) {
        if (p->ticks != 0) {
            p->cur_ticks++;
            if (p->cur_ticks >= p->ticks && p->is_runing == 0) {
                memmove(p->alarm_tf, p->trapframe, sizeof(struct trapframe));
                p->is_runing = 1;
                p->cur_ticks = 0;
                p->trapframe->epc = (uint64)p->handler;
            }
        }
        yield();
    }

最后修改sys_sigreturn函数

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

总结

  1. 系统调用的过程很复杂,设计的也很巧妙。并且由于内核态和用户态的虚拟地址空间不一样,导致了一些麻烦的操作。不过在各种状态的切换中,进程的trapframe包含了这个进程所有的信息,拥有一个进程某个时刻的trapframe,就可以在任意时候将这个进程恢复到这个状态,这也是这个lab考察的内容
  2. 在这个lab中我们不需要自己去做各种东西的切换,只需要提供一个正确的trapframe即可
  3. 系统调用会经历那四个阶段,时钟中断导致的进程切换和恢复最起码也会经历最后的两个阶段

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/907718.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

自动化客户服务必备神器——salesmartly

自动化客户服务必备神器——salesmartly 您是否正在寻找节省时间和简化客户服务流程的方法&#xff1f;客户服务自动化就是答案&#xff01;自动化使企业能够提供更快、更高效的客户服务&#xff0c;尤其是在电子商务领域。在本文中&#xff0c;我们将讨论自动化客户服务流程的…

没有高学历就没有高薪工作?

大家好&#xff0c;我是郑州软件测试4期的小路同学&#xff0c;是一个只有普通高中学历的河南人。由于自己上学的时候对学习不感兴趣&#xff0c;于是很早就辍学了。干过服务员&#xff0c;也卖过房子&#xff0c;但是干啥工作都没有挺过半年的&#xff0c;因为薪资很低&#x…

go_并发编程

go并发编程 一、 并发介绍1&#xff0c;进程和线程2&#xff0c;并发和并行3&#xff0c;协程和线程4&#xff0c;goroutine 二、 Goroutine1&#xff0c;使用goroutine1&#xff09;启动单个goroutine2&#xff09;启动多个goroutine 2&#xff0c;goroutine与线程3&#xff0…

docker compose的用法

目录 一、Docker-Compose介绍 1.1 Docker-Compose的概述 1.2 Docker-Compose 用来实现Docker容器快速编排 1.3 Docker-compose模板文件简介 二、YAML简介 2.1 YAML的概述 2.2 YAML的基本语法规则 2.3 YAML支持的数据架构 三、配置内部常用字段 四、Docker-compose 常…

常见的软件测试用例设计方法有哪些?

常见的软件测试用例设计方法&#xff0c;个人认为主要是下面这6种&#xff1a; 1)流程图法&#xff08;也叫场景法&#xff09; 2)等价类划分法 3)边界值分析 4)判定表 5)正交法 6)错误推测法 这6种常见方法中&#xff0c;我分别按照定义、应用场景、使用步骤、案例讲解这4个部…

Vue-13.创建完整的Vue项目(vue+vue-cli+js)-1

前言 之前写了命令创建Vue项目&#xff0c;但是事实上我们可以直接用编译器直接创建项目&#xff0c;这里我使用webstorm&#xff08;因为我是前后端兼修的所以我习惯使用Idea家族的编译器&#xff09; 只写前端的推荐用VsCode前后端都写的推荐用webstorm 新建项目 项目初始…

记录node 版本对应的 node-sass 版本号

当我们在项目中使用 node-sass 时&#xff0c;电脑的 node版本号一定要与 node-sass 版本号对应&#xff0c;不对应时下载就会报错 一、官方文档地址 https://www.npmjs.com/package/node-sass 二、对应版本表格

一张图看懂 USDT三种类型地址 Omni、ERC20、TRC20的区别

USDT是当前实用最广泛&#xff0c;市值最高的稳定币&#xff0c;它是中心化的公司Tether发行的。在今年的4月17日之前&#xff0c;市场上存在着2种不同类型的USDT。4月17日又多了一种波场TRC20协议发行的USDT&#xff0c;它们各自有什么区别呢?哪个转账最快到账&#xff1f;哪…

Android kotlin 跳转手机热点开关页面和判断热点是否打开

Android kotlin 跳转手机热点开关页面和判断热点是否打开 判断热点是否打开跳转手机热点开关页面顺带介绍一些其他常用的设置页面跳转 其他热点的一些相关知识Local-only hotspot 参考 判断热点是否打开 网上方法比较多&#xff0c;我这边使用了通过WifiManager 拿反射的getWi…

【C#学习笔记】委托和事件

文章目录 委托委托的定义委托实例化委托的调用多播委托 为什么使用委托&#xff1f;官方委托泛型方法和泛型委托 事件为什么要有事件&#xff1f;事件和委托的区别&#xff1a; 题外话——委托与观察者模式 委托 在 .NET 中委托提供后期绑定机制。 后期绑定意味着调用方在你所…

问题解决:Failed to start sshd.service: Unit is masked.

centos7.6 ssh突然不能用了 也启动不了 错误如下&#xff1a; 解决方式&#xff1a; systemctl unmask sshd systemctl start sshd

day 34 | ● 62.不同路径 ● 63. 不同路径 II

62.不同路径 递归公式为上 左 func uniquePaths(m int, n int) int {dp : make([][]int, m)for i : 0; i < m; i{tmp : make([]int, n)dp[i] tmpdp[i][0] 1}for i : 0; i < n; i{dp[0][i] 1}for i : 1; i < m; i{for j : 1; j < n; j{dp[i][j] dp[i - 1][j] …

倒计时动效

1. 效果 2. html <div class"count"><span>3</span><span>2</span><span>1</span> </div>3. css body {width: 100vw;height: 100vh;overflow: hidden;display: flex;justify-content: center;align-items: cente…

数据存储效率对决:Redis String vs. Hash性能大比拼,哪个更适合你?

一、Redis的数据类型 1、常规类型 1&#xff09;String&#xff08;字符串&#xff09;&#xff1a;最基本的数据结构&#xff0c;可以存储任何类型的字符串、数字或二进制数据。 2&#xff09;Hash&#xff08;哈希表&#xff09;&#xff1a;类似于关联数组或字典&#xff…

售后服务管理软件怎么选择?售后服务管理系统有什么用?

随着企业信息化发展&#xff0c;越来越多的企业纷纷选择售后服务管理软件来服务客户和进行内部人员管理。借助这款软件&#xff0c;企业能够高效地满足客户提出的需求&#xff0c;并提高客户对售后服务的满意度。售后服务通常涉及客户、客服、维修师傅和服务管理人员等各种角色…

[JavaWeb]【八】web后端开发-Mybatis

目录 一 介绍 二 Mybatis的入门 2.1 快速入门 2.1.1 准备SpringBoot工程 2.1.2 创建数据库mybatis以及对应库表user 2.1.3 创建User实体类 2.1.4 配置application.properties数据库连接信息 2.1.5 编写sql语句&#xff08;注解方式&#xff09; 2.1.6 测试运行 2.1.7 配…

Unity封装Debug.Log导致代码定位失准的解决办法

笔者通过翻资料&#xff0c;实现了这样的一个编辑器&#xff0c;虽然无法彻底消除指定的日志信息 但是可以实现”双击日志不跳转到这里的任意一个文件“ using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEngine;namespace AirEditor {publ…

浅谈java自定义中类两个对象的比较

目录 实现比较两个对象是否相同 1.前置代码 1.学生类 2.示例 3.输出 4.原因 2.那么我们要怎么做呢? 1.对Student类中重新实现quals方法(即对equals方法重写) 2.完整代码如下: 3.具体操作 4.演示 1.示例 2.输出 3.原因 实现比较两个对象的大小 第一种: 用…

智慧小区建设方案【47页PPT】

导读&#xff1a;原文《智慧小区建设方案【47页PPT】》&#xff08;获取来源见文尾&#xff09;&#xff0c;本文精选其中精华及架构部分&#xff0c;逻辑清晰、内容完整&#xff0c;为快速形成售前方案提供参考。 部分页面&#xff1a; 喜欢文章&#xff0c;您可以关注评论转…