xv6 lec6 陷入与系统调用

参考:
https://www.cnblogs.com/mlmz/p/16083803.html

Trap机制

Trap机制就是用户空间有内核空间的切换,目的是为了安全性隔离,并为了兼顾效率,由于系统调用与lazy allocation等导致的page
falut的频繁发生,Trap要设计的尽可能简单

有三种情况会发生trap:

  • 程序执行系统调用
  • 程序出现了类似page fault、运算时除以0的错误,就是异常
  • 一个设备触发了中断使得当前程序运行需要响应内核设备驱动,就是中断

初始时,shell程序(也就是shell脚本的解释器)运行在用户态,如果要执行系统调用,比如write,就会从拥有user权限并且位于用户空间切换到拥有supervisor权限的内核。切换到内核需要修改一个程序状态.
其中最重要的就是32个用户寄存器,使用寄存器的指令性能最好,
注意PC,MODE,SATP,STVEC,SEPC,SSCRATCH这些寄存器不属于32个寄存器之中

在这里插入图片描述
Trap过程中寄存器的变化:

  • 由于内核程序也需要使用这些寄存器,并且为了安全性考虑,内核代码不应该去使用用户态下的寄存器中的数据,因为其中可能保存着恶意数据,所以为了安全性与透明性,在Trap之前,以及回到用户态之后,这些寄存器的值不能够改变
  • pc寄存器也需要保存,这样返回内核的时候才知道去执行哪一条用户态指令
  • 我们需要将mode改成supervisor mode,因为我们想要使用内核中的各种各样的特权指令
  • SATP寄存器现在正指向user page table,而user page table只包含了用户程序所需要的内存映射和一两个其他的映射,它并没有包含整个内核数据的内存映射。所以在运行内核代码之前,我们需要将SATP指向kernel page table。
  • Trap过程也需要去将堆栈寄存器(堆栈寄存器属于32个用户寄存器)指向位于内核的一个地址,因为我们需要一个堆栈来调用内核的C函数。

supervisor mode的特权

  • 可以读写控制寄存器了。比如说,当你在supervisor mode时,你可以:读写SATP寄存器,也就是page table的指针;STVEC,也就是处理trap的内核指令地址;SEPC,保存当发生trap时的程序计数器;SSCRATCH,保存了trapframe page的用户页表虚拟地址,等等。在supervisor mode你可以读写这些寄存器,而用户代码不能做这样的操作
  • 它可以使用PTE_U标志位为0的PTE。当PTE_U标志位为1的时候,表明用户代码可以使用这个页表;如果这个标志位为0,则只有supervisor mode可以使用这个页表

supervisor mode也存在限制

  • supervisor mode中的代码并不能读写任意物理地址。在supervisor mode中,就像普通的用户代码一样,也需要通过page table来访问内存。如果一个虚拟地址并不在当前由SATP指向的page table中,又或者SATP指向的page table中PTE_U=1(也就是用户态才能够读的页表项),那么supervisor mode不能使用那个地址。所以,即使我们在supervisor mode,我们还是受限于当前page table设置的虚拟地址。

Trap的执行流程

以write系统调用举例:

  • 在用户态把系统调用号放到a7寄存器
  • ecall指令(ecall是一个硬件指令)
  • trampoline中的uservec()
  • trap.c中的usertrap()
  • syscall()
  • sys_write()
  • 回到trap.c中usertrap()
  • trap.c中的usertrapret()
  • trampoline中的userret()
  • 回到用户态
    在这里插入图片描述

Trapframe Page:

Trapframe page 是用于存储处理异常情况的上下文信息的页面。当处理器发生异常或中断时,操作系统需要保存当前正在执行进程的状态,包括寄存器的值、程序计数器、堆栈指针等。这些信息被保存在 trapframe page 中,以便在异常处理程序执行完毕后能够恢复进程的状态。Trapframe page 通常是每个进程都有一个,用于存储该进程的异常上下文。

Trampoline Page:

Trampoline page 是用于进程间切换的辅助页面。在一些操作系统的设计中,当发生进程切换时,会使用一个 trampoline page 来跳转到新的进程执行。Trampoline page 包含了切换进程所需的指令和数据,它负责将控制权传递给新的进程,并在进程切换完成后恢复原始进程的状态。Trampoline page 在进程切换过程中充当临时的桥梁,确保进程切换的顺利执行。

ECALL之前

wirte函数关联到了一个库函数,这个库函数在user/usys.S中
在这里插入图片描述

在ecall之前的用户页表的最后两项表示trampoline与trampframe页
在这里插入图片描述

由于标志位u未置位,那么只有在supervisor mode才能访问这两个PTE,在ecall之后,可以访问每一个进程虚拟地址空间中的trampoline与trapframe页
在这里插入图片描述

ECALL之后

ecall并不会切换page table,这是ecall指令的一个非常重要的特点。所以这意味着,trap处理代码必须存在于每一个user page table中。在从内核空间进入到用户空间之前,内核会设置好STVEC寄存器指向内核希望trap代码运行的位置。

内核已经事先设置好了STVEC寄存器的内容为0x3ffffff000,这就是trampoline page的起始位置。STVEC寄存器的内容,就是在ecall指令执行之后,我们会在这个特定地址执行指令的原因。

在这里插入图片描述
我们是通过ecall走到trampoline page的,而ecall实际上只会改变三件事情:

  • 第一,ecall将代码从user mode改到supervisor mode。
  • 第二,ecall将程序计数器的值保存在了SEPC寄存器。SEPC寄存器保存了程序计数器的值。
  • 第三,ecall会跳转到STVEC寄存器指向的指令。

由于ECALL指令将将user mode改为supervisor mode,(这个时候页表还是用户页表)
在这里插入图片描述

那么这个时候就可以可以访问页表项的最后一项

  • trampoline page的第一条指令是csrrw a0, sscratch, a0,这条指令将a0的数据保存在了sscratch中,同时又将sscratch内的数据保存在a0中。之后内核就可以任意的使用a0寄存器了。
  • trampoline page包含了trap处理代码,因为ecall并不会切换page table,我们需要在user page
    table中的某个地方来执行最初的内核代码。

为了保持ecall指令的灵活性,ecall指令不会不会保存用户寄存器,或者切换page table指针来指向kernel page table,或者自动的设置Stack Pointer指向kernel stack,或者直接跳转到kernel的C代码。

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

  • 我们需要保存32个用户寄存器的内容,这样当我们想要恢复用户代码执行时,我们才能恢复这些寄存器的内容。
  • 因为现在我们还在user page table,我们需要切换到kernel page table。
  • 我们需要创建或者找到一个kernel stack,并将Stack Pointer寄存器的内容指向那个kernel
    stack。这样才能给C代码提供栈。
  • 我们还需要跳转到内核中C代码的某些合理的位置。

uservec

uservec是trampoline页的最开始的函数

每个进程被创建的时候会被分配一个trapframe,并做好在user page table中做好映射,其在进程的虚拟地址空间中trampoline的正下方

在这里插入图片描述

  • 使用csrrw指令,交换a0和sscratch两个寄存器的内容,sscratch中存有的是trapframe page的虚拟地址
  • 之后是保存用户的32个寄存器到trapframe
    在这里插入图片描述

每个进程的trapframe还保留了5个内核数据
在这里插入图片描述
其中kernel_sp就是进程的内核栈,寄存器sp的值会被设置为它在这里插入图片描述
保存CPU核的编号到tp寄存器,在内核中好几个地方都会使用了这个值,例如,内核可以通过这个值确定某个CPU核上运行了哪些进程。

在这里插入图片描述
将之后要执行的usertrap函数的指针放入t0寄存器
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
将用户页表转换为内核页表

在这里插入图片描述

进入usertrap函数

在这里插入图片描述

usertrap函数

首先将kernelvec()的地址放入stvec寄存器中,用户态的时候放的是trampoline的地址,在内核态,处理trap的函数是kernelvec()

在这里插入图片描述
之后是保存sepc寄存器,其中保存的是当发生trap时的程序计数器,因为可能发生这种情况:当程序还在内核中执行时,我们可能切换到另一个进程,并进入到那个程序的用户空间,然后那个进程可能再调用一个系统调用进而导致SEPC寄存器的内容被覆盖。所以,我们需要保存当前进程的SEPC寄存器到一个与该进程关联的内存中,这样这个数据才不会被覆盖。这里我们使用trapframe来保存这个程序计数器。

根据SCAUSE寄存器中的数字判断trap的类型做出相应的处理,

  • 对于系统调用,我们需要p->trapframe->epc += 4;,因为我们希望返回到用户态时,去执行ecall下面的一条指令,对于其他的trap类型,比如page fault就不会+4
    在这里插入图片描述
    由于有些系统调用需要大量时间处理,所以当保存好了当前进程的寄存器等相关状态后,可以打开中断,之后去调用syscall(),系统调用号用来匹配系统调用函数表,如果系统调用号是合法的,那么执行对应的系统调用,如果不合法,那就将代表错误的返回值-1放到a0寄存器中,如果系统调用号合法,那么就会去执行对应实现系统调用的内核函数,并将实现系统调用的内核函数的返回值放入a0中
    在这里插入图片描述
    可以看到内核中的实现系统调用的函数就是通过进程的trapframe获取参数

在这里插入图片描述
如果是设备中断,就交给devintr
如果是异常,那么就终止该进程的运行。

在这里插入图片描述

  • 处理完成系统调用等问题后,回到**usertrap函数,**执行usertrapret(void)函数
    在这里插入图片描述

usertrapret

首先是关中断,
在这里插入图片描述
我们之前在系统调用的过程中是打开了中断的,这里关闭中断是因为我们将要更新STVEC寄存器来指向用户空间的trap处理代码,而之前在内核中的时候,我们指向的是内核空间的trap处理代码(6.6)。我们关闭中断因为当我们将STVEC更新到指向用户空间的trap处理代码时,我们仍然在内核中执行代码。如果这时发生了一个中断,那么程序执行会走向用户空间的trap处理代码,即便我们现在仍然在内核中,出于各种各样具体细节的原因,这会导致内核出错。所以我们这里关闭中断。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
变量satp中存的是用户页表的物理地址,之后将调用trampoline中的userret函数,将TRAPFRAME与satp作为参数,也就是放在a0与a1中,转入userret 函数

在这里插入图片描述

userret

将内核页表转为用户页表
在这里插入图片描述
trapframe中的a0寄存器保存的是系统调用的返回值,由于先需要使用trapframe,所以现在的a0放的是trapframe的地址

在这里插入图片描述

在这里插入图片描述
在trapframe中的寄存器的值放回到进程对应寄存器后,交换sscratch与a0的值,那么sscratch中就有了trapframe的地址,a0中存的就是系统调用的返回值,最后再执行sret返回

sret会做三件事:

  • 程序会切换回user mode
  • SEPC寄存器的数值会被拷贝到PC寄存器(程序计数器)
  • 重新打开中断
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值