mit6.s081 lab4实验记录

lab4 :trap

本章实验的内容是xv6系统的陷入机制,主要讲了用户态到内核态的陷入过程。主要分为三种情形:

  1. 系统调用
    当用户进程请求系统资源或服务时,会使用系统调用进行操作。如进行IO输入输出、磁盘读写、请求/释放内存等等。
  2. 中断
    一个设备触发了中断使得当前进程的运行需要响应内核设备驱动,如时钟中断,IO设备中断等。
  3. 异常
    程序出现了类似缺页、运算除0等错误时会导致异常,此时将陷入内核态,由内核对发生异常的进程进行处理,比如杀死该进程。

当trap发生时,需要将用户进程的“现场”保存到内核中,内核使用pcb中的trapframe模块来保存所有与进程有关的寄存器。并在返回进程时,从trapframe恢复所有的寄存器。以系统调用为例,下面简单分析一下trap的全部流程:

  1. 用户进程请求系统调用

  2. 由于对系统调用函数已经设置了入口entry,所以会将系统调用的编号存入CPU的a7寄存器,同时将相关参数保存在a0-a8寄存中。接着使用ecall进入汇编“跳板”程序trampoline,为后续转入内核做准备

  3. ecall指令主要做了以下三点工作:
    (1)将代码从user mode切换到supervisor mode,使得可以执行特权指令以及修改一些高级寄存器如satp(页表地址寄存器), stvec(保存trap指令的起始地址), sepc(保存进程程序计数器的值), ssratch(保存trapframe page的虚拟地址)。(2)ecall指令中将PC保存到SEPC,尽管其他的寄存器还是还是用户寄存器,但是这里的程序计数器明显已经不是用户代码的程序计数器。这里的程序计数器是从STVEC寄存器拷贝过来的值。
    (3)内核提前设置好了STVEC寄存器的值为trampoline page,ecall会会跳转到STVEC寄存器指向的指令

  4. 进入trampoline之后,并没有完全进入内核,在trampoline页我们需要:
    (1)首先,我们需要保存32个用户寄存器。因为很显然我们需要恢复用户应用程序的执行,尤其是当用户程序随机的被设备中断所打断时。我们希望内核能够响应中断,之后在用户程序完全无感知的情况下再恢复用户代码的执行。所以这意味着32个用户寄存器不能被内核弄乱。但是这些寄存器又要被内核代码所使用,所以在trap之前,你必须先在某处保存这32个用户寄存器。
    (2)程序计数器也需要在某个地方保存,它几乎跟一个用户寄存器的地位是一样的,我们需要能够在用户程序运行中断的位置继续执行用户程序。
    (3)我们需要将mode改成supervisor mode,因为我们想要使用内核中的各种各样的特权指令。
    (4)SATP寄存器现在正指向user page table,而user page table只包含了用户程序所需要的内存映射和一两个其他的映射,它并没有包含整个内核数据的内存映射。所以在运行内核代码之前,我们需要将SATP指向kernel page table。
    (5)我们需要将堆栈寄存器指向位于内核的一个地址,因为我们需要一个堆栈来调用内核的C函数。
    (6)一旦我们设置好了,并且所有的硬件状态都适合在内核中使用, 我们需要跳入内核的C代码。
    几个问题:
    用户态和内核态都能找trampoline page?
    TA:因为在内核页表和用户页表都使用同一个虚拟地址TRAMPOLINE映射到了该页的真实物理地址,所以这个虚拟地址在用户态和内核态都是可以直接解析的。
    为什么在uservec函数中可以找到trapframe的地址并将寄存器保存进去?
    TA:在保存之前有一个交换寄存器值的操作

# swap a0 and sscratch
# so that a0 is TRAPFRAME
csrrw a0, sscratch, a0

sscratch寄存器在内核启动之后进入用户态之前,就trapframe固定的虚拟地址TRAPFRAME写入了该寄存器。上述汇编指令将sscratch寄存器的值与a0寄存器的值交换。因为此时的页表为用户页表,所以可以通过寄存器a0来对trapframe进行读写。trapframe虽然是PCB中的一个属性,但是在进程页表初始化中也将该物理块映射到用户页表了,因此可以通过用户页表访问。

pagetable_t
proc_pagetable(struct proc *p)
{
  //...............
  if (mappages(pagetable, TRAMPOLINE, PGSIZE,
               (uint64)trampoline, PTE_R | PTE_X) < 0)
  {
    uvmfree(pagetable, 0);
    return 0;
  }

  // map the trapframe just below TRAMPOLINE, for trampoline.S.
  if (mappages(pagetable, TRAPFRAME, PGSIZE,
               (uint64)(p->trapframe), PTE_R | PTE_W) < 0)
  {
    uvmunmap(pagetable, TRAMPOLINE, 1, 0);
    uvmfree(pagetable, 0);
    return 0;
  }

  return pagetable;
}

  1. 接下来读取内核栈地址到sp寄存器、读取kernel_trap地址作为后续跳转位置、读取内核页表地址并载入页表寄存器刷新快表。
  2. 最后,跳转进入kernel_trap,在用户态到内核态的kernel_trap是trap.c/usertrap函数。
  3. 进入usertrap函数后首先会将stvec寄存器指向kernelvec变量,这是内核空间trap处理代码的位置。因为此时已经进入内核,如果再发生trap如异常、中断等就需要内核的trap处理函数进行操作。
  4. 接着会判断trap发生原因如果是系统调用就会转入系统调用的代码、如果是其他的也会执行对应操作。
  5. 完成后转入usertrapret函数,首先关闭中断,在usertrap过程中打开了中断,这里关闭中断是因为我们将要更新stvec寄存器来指向用户空间trap处理代码,而之前在内核中,我们指向的是内核空间的trap处理代码。当我们将STVEC更新到指向用户空间的trap处理代码时,我们仍然在内核中执行代码。如果这时发生了一个中断,那么程序执行会走向用户空间的trap处理代码,即便我们现在仍然在内核中,出于各种各样具体细节的原因,这会导致内核出错。所以我们这里关闭中断。
  6. 设置了STVEC寄存器指向trampoline代码的userret函数,在那里最终会执行sret指令返回到用户空间。位于trampoline代码最后的sret指令会重新打开中断。这样,即使我们刚刚关闭了中断,当我们在执行用户代码时中断是打开的。
  7. 接下来的几行填入了trapframe的内容,这些内容对于执行trampoline代码非常有用。
    存储了kernel page table的指针
    存储了当前用户进程的kernel stack
    存储了usertrap函数的指针,这样trampoline代码才能跳转到这个函数
    从tp寄存器中读取当前的CPU核编号,并存储在trapframe中,这样trampoline代码才能恢复这个数字,因为用户代码可能会修改这个数字

    现在我们在usertrapret函数中,我们正在设置trapframe中的数据,这样下一次从用户空间转换到内核空间时可以用到这些数据。
    在这里插入图片描述
  8. 接下来我们要设置SSTATUS寄存器,这是一个控制寄存器。这个寄存器的SPP bit位控制了sret指令的行为,该bit为0表示下次执行sret的时候,我们想要返回user mode而不是supervisor mode。这个寄存器的SPIE bit位控制了,在执行完sret之后,是否打开中断。因为我们在返回到用户空间之后,我们的确希望打开中断,所以这里将SPIE bit位设置为1。修改完这些bit位之后,我们会把新的值写回到SSTATUS寄存器。
  9. 最后请添加图片描述
    在trampoline代码的最后执行了sret指令。这条指令会将程序计数器设置成SEPC寄存器的值,所以现在我们将SEPC寄存器的值设置成之前保存的用户程序计数器的值。在不久之前,我们在usertrap函数中将用户程序计数器保存在trapframe中的epc字段。
  10. 接下来,我们根据user page table地址生成相应的SATP值,这样我们在返回到用户空间的时候才能完成page table的切换。实际上,我们会在汇编代码trampoline中完成page table的切换,并且也只能在trampoline中完成切换,因为只有trampoline中代码是同时在用户和内核空间中映射。但是我们现在还没有在trampoline代码中,我们现在还在一个普通的C函数中,所以这里我们将page table指针准备好,并将这个指针作为第二个参数传递给汇编代码,这个参数会出现在a1寄存器。
    倒数第二行的作用是计算出我们将要跳转到汇编代码的地址。我们期望跳转的地址是tampoline中的userret函数,这个函数包含了所有能将我们带回到用户空间的指令。所以这里我们计算出了userret函数的地址。
    倒数第一行,将fn指针作为一个函数指针,执行相应的函数(也就是userret函数)并传入两个参数,两个参数存储在a0,a1寄存器中。
    请添加图片描述
    进入userret函数中首先将用户页表写入satp并刷新快表,接着从trapframe中读取保存的寄存器,交换a0的sscratch的值,最后使用sret切换会用户空间。sret会根据设置切换到用户空间,并将spec寄存器的值写入pc,重新打开中断

至此就完成了一次trap的全部处理流程

2. Backtrace

这个函数就是实现曾经调用函数地址的回溯,这个功能在日常的编程中也经常见到,编译器报错时就是类似的逻辑,只不过题目的要求较为简单,只用打印程序地址,而实际的报错中往往打印程序文件名,函数名以及行号等信息(最后的可选练习就是实现这样的功能)。

// 打印返回地址
void backtrace()
{
  printf("%s\n", "backtrace");

  uint64 fp = r_fp(); // 获取寄存器fp的值
  uint64 loop_end = PGROUNDUP(fp);
  uint64 pre_fp;
  uint64 r_a;
  while (fp != loop_end)
  {
    // fp是当前栈帧的地址 将其向下偏移8字节得到栈帧中保存返回地址的地址
    // 为了获取改地址内保存的值,需要采用解指针的方式获取。但fp-8虽然是一个地址值
    // 但他并不是一个指针 所以需要现将其转化为一个指针类型 再去解指针即可
    // 这些虚拟地址是如何对应的最终物理地址 是由硬件通过内核页表获取映射实现的
    r_a = *((uint64 *)(fp - 8)); // 获取返回地址
    printf("%p\n", r_a);
    pre_fp = *((uint64 *)(fp - 16)); // 获取上一个栈帧的地址
    fp = pre_fp;
  }
}

​ 根据提示:返回地址位于栈帧帧指针的固定偏移(-8)位置,并且保存的帧指针位于帧指针的固定偏移(-16)位置。先使用r_fp()读取当前的帧指针,然后读出返回地址并打印,再将fp定位到前一个帧指针的位置继续读取即可。
V6在内核中以页面对齐的地址为每个栈分配一个页面,所以通过判断fp是否到达页面的最顶部来判断是否回溯结束。

3.Alarm

这个练习主要是想实现一个定期的警报,当进程占用的CPU时钟达到tick后,就会执行对应的函数,函数执行结束后会返回到进程原来的位置继续执行。
程序计数器的过程是这样的:

  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修改为要跳转的指令地址即可,handler可能会改变用户寄存器,为了后续能够恢复到原来的位置,需要将所有的寄存器值保存在另一个field内,并在sigreturn系统调用中将trapframe恢复原来的值。
    另外为了让报警函数不会重复执行,要确保当前函数执行完成后在执行下一次报警,所以需要新增一个字段waitRet确保只有一个报警函数在执行。因此我们要在usertrap中再次保存用户寄存器,当handler调用sigreturn时将其恢复,并且要防止在handler执行过程中重复调用。
    注意:新增的trapframe_Tmp字段要在allocproc函数中分配物理内存,并在进程销毁时释放对应的内存。结构的复制直接用赋值运算符即可。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值