6.8 userret函数

现在程序执行又到了trampoline代码。

第一步是切换page table。在执行csrw satp, a1之前,page table应该还是巨大的kernel page table。这条指令会将user page table(在usertrapret中作为第二个参数传递给了这里的userret函数,所以存在a1寄存器中)存储在SATP寄存器中。执行完这条指令之后,page table就变成了小得多的user page table。但是幸运的是,user page table也映射了trampoline page,所以程序还能继续执行而不是崩溃。(注,sfence.vma是清空页表缓存,详见4.4)。

在uservec函数中,第一件事情就是交换SSRATCH和a0寄存器。而这里,我们将SSCRATCH寄存器恢复成保存好的用户的a0寄存器。在这里a0是trapframe的地址,因为C代码usertrapret函数中将trapframe地址作为第一个参数传递过来了。112是a0寄存器在trapframe中的位置。(注,这里有点绕,本质就是通过当前的a0寄存器找出存在trapframe中的a0寄存器)我们先将这个地址里的数值保存在t0寄存器中,之后再将t0寄存器的数值保存在SSCRATCH寄存器中。

为止目前,所有的寄存器内容还是属于内核。

接下来的这些指令将a0寄存器指向的trapframe中,之前保存的寄存器的值加载到对应的各个寄存器中。之后,我们离能真正运行用户代码就很近了。

学生提问:现在trapframe中的a0寄存器是我们执行系统调用的返回值吗?

Robert教授:是的,系统调用的返回值覆盖了我们保存在trapframe中的a0寄存器的值(详见6.6)。我们希望用户程序Shell在a0寄存器中看到系统调用的返回值。所以,trapframe中的a0寄存器现在是系统调用的返回值2。相应的SSCRATCH寄存器中的数值也应该是2,可以通过打印寄存器的值来验证。

现在我们打印所有的寄存器,

我不确定你们是否还记得,但是这些寄存器的值就是我们在最最开始看到的用户寄存器的值。例如SP寄存器保存的是user stack地址,这是一个在较小的内存地址;a1寄存器是我们传递给write的buffer指针,a2是我们传递给write函数的写入字节数。

a0寄存器现在还是个例外,它现在仍然是指向trapframe的指针,而不是保存了的用户数据。

接下来,在我们即将返回到用户空间之前,我们交换SSCRATCH寄存器和a0寄存器的值。前面我们看过了SSCRATCH现在的值是系统调用的返回值2,a0寄存器是trapframe的地址。交换完成之后,a0持有的是系统调用的返回值,SSCRATCH持有的是trapframe的地址。之后trapframe的地址会一直保存在SSCRATCH中,直到用户程序执行了另一次trap。现在我们还在kernel中。

sret是我们在kernel中的最后一条指令,当我执行完这条指令:

  • 程序会切换回user mode

  • SEPC寄存器的数值会被拷贝到PC寄存器(程序计数器)

  • 重新打开中断

现在我们回到了用户空间。打印PC寄存器,

这是一个较小的指令地址,非常像是在用户内存中。如果我们查看sh.asm,可以看到这个地址是write函数的ret指令地址。

所以,现在我们回到了用户空间,执行完ret指令之后我们就可以从write系统调用返回到Shell中了。或者更严格的说,是从触发了系统调用的write库函数中返回到Shell中。

学生提问:你可以再重复一下在sret过程中,中断会发生什么吗?

Robert教授:sret打开了中断。所以在supervisor mode中的最后一个指令,我们会重新打开中断。用户程序可能会运行很长时间,最好是能在这段时间响应例如磁盘中断。

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

另一方面,XV6实现trap的方式比较特殊,XV6并不关心性能。但是通常来说,操作系统的设计人员和CPU设计人员非常关心如何提升trap的效率和速度。必然还有跟我们这里不一样的方式来实现trap,当你在实现的时候,可以从以下几个问题出发:

  • 硬件和软件需要协同工作,你可能需要重新设计XV6,重新设计RISC-V来使得这里的处理流程更加简单,更加快速。

  • 另一个需要时刻记住的问题是,恶意软件是否能滥用这里的机制来打破隔离性。

好的,这就是这节课的全部内容。

Last updated