xv6 陷入与系统调用
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下面的一条指令由于有些系统调用需要大量时间处理,所以当保存好了当前进程的寄存器等相关状态后,可以打开中断,之后去调用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的地址,真实的返回值被放到了sscratch寄存器中
在trapframe中的寄存器的值放回到进程对应寄存器后,交换sscratch与a0的值,那么sscratch
中就有了trapframe
的地址,a0
中存的就是系统调用的返回值,最后再执行sret
返回, sret
会做三件事:- 程序会切换回user mode
- SEPC寄存器的数值会被拷贝到PC寄存器(程序计数器)
- 重新打开中断
其他问题
- 为什么没有把函数参数放到寄存器的指令,
函数调用的调用规范保证了放到寄存器了
- 移位了是什么意思?
- 这里为什么要+PGSIZE?
p->trapframe
中的这些变量在内核态的时候被修改过吗?