xv6 lec6 陷入与系统调用

https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/lec06-isolation-and-system-call-entry-exit-robert

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()
  • 回到用户态

ECALL之前

  • wirte函数关联到了一个库函数,这个库函数在user/usys.S中
  • 在ecall之前的用户页表的最后两项表示trampoline与trampframe页
    由于标志位u未置位,那么只有在supervisor mode才能访问这两个PTE,在ecall之后,可以访问每一个进程虚拟地址空间中的trampoline与trapframe页

ECALL之后

  • ECALL(ecall是一个硬件指令指令)会做三件事:
    • 将user mode改为supervisor mode
    • ecall将程序计数器的值保存在了SEPC寄存器。
    • STVEC是一个内核寄存器,其中存有trampoline page的最开始的地址,但是内核寄存器只有在supervisor mode下才能读写,由于ecall将代码从user mode改为了supervisr mode,ecall便可以使pc指向trampoline page的最开始,
      trampoline page中的
  • 由于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灵活性可以带来如下好处:

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作为参数,也就是放在a0a1中,转入userret`函数

userret

  • 内核页表转为用户页表
  • trapframe中的a0寄存器保存的是系统调用的返回值,由于先需要使用trapframe,所以现在的a0放的是trapframe的地址,真实的返回值被放到了sscratch寄存器中
    在trapframe中的寄存器的值放回到进程对应寄存器后,交换sscratch与a0的值,那么sscratch中就有了trapframe的地址,a0中存的就是系统调用的返回值,最后再执行sret返回,
  • sret会做三件事:
    • 程序会切换回user mode
    • SEPC寄存器的数值会被拷贝到PC寄存器(程序计数器)
    • 重新打开中断

其他问题

  • 为什么没有把函数参数放到寄存器的指令,
    函数调用的调用规范保证了放到寄存器了
  • 移位了是什么意思?
  • 这里为什么要+PGSIZE,内核栈是向上增长的吗?
  • p->trapframe中的这些变量在内核态的时候被修改过吗?
posted @ 2022-03-31 20:03  抿了抿嘴丶  阅读(167)  评论(0编辑  收藏  举报