Lab4 Trap(内核状态切换)

相关知识

每当

  • 程序执行系统调用

  • 程序出现了类似page fault、运算时除以0的错误

  • 一个设备触发了中断使得当前程序运行需要响应内核设备驱动

都会发生用户空间和内核空间的切换 。

在trap的最开始,CPU的所有状态都设置成运行用户代码而不是内核代码。在trap处理的过程中,我们实际上需要更改一些这里的状态,或者对状态做一些操作。这样我们才可以运行系统内核中普通的C程序。接下来我们先来预览一下需要做的操作:

  • 首先,我们需要保存32个用户寄存器。因为很显然我们需要恢复用户应用程序的执行,尤其是当用户程序随机的被设备中断所打断时。我们希望内核能够响应中断,之后在用户程序完全无感知的情况下再恢复用户代码的执行。所以这意味着32个用户寄存器不能被内核弄乱。但是这些寄存器又要被内核代码所使用,所以在trap之前,你必须先在某处保存这32个用户寄存器。

  • 程序计数器也需要在某个地方保存,它几乎跟一个用户寄存器的地位是一样的,我们需要能够在用户程序运行中断的位置继续执行用户程序。

  • 我们需要将mode改成supervisor mode,因为我们想要使用内核中的各种各样的特权指令。

  • SATP寄存器现在正指向user page table,而user page table只包含了用户程序所需要的内存映射和一两个其他的映射,它并没有包含整个内核数据的内存映射。所以在运行内核代码之前,我们需要将SATP指向kernel page table。

  • 我们需要将堆栈寄存器指向位于内核的一个地址,因为我们需要一个堆栈来调用内核的C函数。

一旦我们设置好了,并且所有的硬件状态都适合在内核中使用, 我们需要跳入内核的C代码。

注意:即使处于内核态,访问物理内存仍然需要通过page tables

write系统调用跟踪

ecall之前

作为用户代码的Shell调用write时,实际上调用的是关联到Shell的一个库函数。你可以查看这个库函数的源代码,在usys.s。它首先将SYS_write加载到a7寄存器,SYS_write是常量16。这里告诉内核,我想要运行第16个系统调用,而这个系统调用正好是write。之后这个函数中执行了ecall指令,从这里开始代码执行跳转到了内核。内核完成它的工作之后,代码执行会返回到用户空间,继续执行ecall之后的指令,也就是ret,最终返回到原c程序中。

在ecall处放置断点,为了能放置断点,我们需要知道ecall指令的地址,我们可以通过查看由XV6编译过程产生的sh.asm找出这个地址。sh.asm是带有指令地址的汇编代码。

打印程序计数器(Program Counter),检查是否到期望的位置。

还可以输入_info reg_打印全部32个用户寄存器,

从QEMU界面,输入_ctrl a + c_可以进入到QEMU的console,之后输入_info mem_,QEMU会打印完整的page table

ecall之后

ecall之后,程序并不会立刻执行内核中的C代码,

ecall实际上只会改变三件事情:

  • 第一,ecall将代码从user mode改到supervisor mode。

  • 第二,保存程序计数器的值。我们可以通过打印程序计数器看到这里的效果

  • 第三,ecall会跳转到应该运行的内核代码处,即trap.c

注意,此时进程仍使用的user page table,现在寄存器里面还都是用户程序的数据,并且这些数据也还只保存在这些寄存器中,所以我们需要非常小心,在将寄存器数据保存在某处之前,我们在这个时间点不能使用任何寄存器,否则的话我们是没法恢复寄存器数据的。如果内核在这个时间点使用了任何一个寄存器,内核会覆盖寄存器内的用户数据,之后如果我们尝试要恢复用户程序,我们就不能恢复寄存器中的正确数据,用户程序的执行也会相应的出错。

ecall帮我们做了一点点工作,但是实际上我们离执行内核中的C代码还差的很远。接下来:

  • 我们需要保存32个用户寄存器的内容,这样当我们想要恢复用户代码执行时,我们才能恢复这些寄存器的内容。

  • 因为现在我们还在user page table,我们需要切换到kernel page table。

  • 我们需要创建或者找到一个kernel stack,并将Stack Pointer寄存器的内容指向那个kernel stack。这样才能给C代码提供栈。

  • 我们还需要跳转到内核中C代码的某些合理的位置。

ecall并不会为我们做这里的任何一件事。

处理上面任务后,调用到

:::success

通过前面传入a7寄存器中的索引号,找到对应的sys_write系统调用函数。在函数中,可以通过查看a0寄存器,这是第一个参数,a1是第二个参数,a2是第三个参数等,获得在用户空间调用write时传入的参数。之后sys_write返回,将返回值赋给a0寄存器、

:::

之后,内核开始准备返回到用户空间。

最后总结一下,系统调用被刻意设计的看起来像是函数调用,但是背后的user/kernel转换比函数调用要复杂的多。之所以这么复杂,很大一部分原因是要保持user/kernel之间的隔离性,内核不能信任来自用户空间的任何内容。

linux系统的状态切换

64 位系统调用

  • 用户态

  • 将请求参数保存到_寄存器_

  • 将系统调用名称转为系统调用号保存到寄存器 rax 中

  • 通过 syscall 汇编指令跳转内核态

  • 内核态

  • 保存用户寄存器的内容,用于恢复用户代码执行

  • page tables的切换。

  • 创建或者找到一个kernel stack,并将Stack Pointer寄存器的内容指向那个kernel stack。给内核C代码提供栈。

  • 在系统调用函数表 sys_call_table 中根据调用号找到对应的系统调用函数

  • 将_寄存器_中保存的参数取出来作为函数参数执行函数, 将返回值写入寄存器

  • 通过sysretq指令返回用户态

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值