6.7 usertrapret函数

usertrap函数的最后调用了usertrapret函数,来设置好我之前说过的,在返回到用户空间之前内核要做的工作。我们可以查看这个函数的内容。

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

在下一行我们设置了STVEC寄存器指向trampoline代码,在那里最终会执行sret指令返回到用户空间。位于trampoline代码最后的sret指令会重新打开中断。这样,即使我们刚刚关闭了中断,当我们在执行用户代码时中断是打开的。

接下来的几行填入了trapframe的内容,这些内容对于执行trampoline代码非常有用。这里的代码就是:

  • 存储了kernel page table的指针

  • 存储了当前用户进程的kernel stack

  • 存储了usertrap函数的指针,这样trampoline代码才能跳转到这个函数(注,详见6.5中 ld t0 (16)a0 指令)

  • 从tp寄存器中读取当前的CPU核编号,并存储在trapframe中,这样trampoline代码才能恢复这个数字,因为用户代码可能会修改这个数字

现在我们在usertrapret函数中,我们正在设置trapframe中的数据,这样下一次从用户空间转换到内核空间时可以用到这些数据。

学生提问:为什么trampoline代码中不保存SEPC寄存器?

Robert教授:可以存储。trampoline代码没有像其他寄存器一样保存这个寄存器,但是我们非常欢迎大家修改XV6来保存它。如果你还记得的话(详见6.6),这个寄存器实际上是在C代码usertrap中保存的,而不是在汇编代码trampoline中保存的。我想不出理由这里哪种方式更好。用户寄存器(User Registers)必须在汇编代码中保存,因为任何需要经过编译器的语言,例如C语言,都不能修改任何用户寄存器。所以对于用户寄存器,必须要在进入C代码之前在汇编代码中保存好。但是对于SEPC寄存器(注,控制寄存器),我们可以早点保存或者晚点保存。

接下来我们要设置SSTATUS寄存器,这是一个控制寄存器。这个寄存器的SPP bit位控制了sret指令的行为,该bit为0表示下次执行sret的时候,我们想要返回user mode而不是supervisor mode。这个寄存器的SPIE bit位控制了,在执行完sret之后,是否打开中断。因为我们在返回到用户空间之后,我们的确希望打开中断,所以这里将SPIE bit位设置为1。修改完这些bit位之后,我们会把新的值写回到SSTATUS寄存器。

我们在trampoline代码的最后执行了sret指令。这条指令会将程序计数器设置成SEPC寄存器的值,所以现在我们将SEPC寄存器的值设置成之前保存的用户程序计数器的值。在不久之前,我们在usertrap函数中将用户程序计数器保存在trapframe中的epc字段。

接下来,我们根据user page table地址生成相应的SATP值,这样我们在返回到用户空间的时候才能完成page table的切换。实际上,我们会在汇编代码trampoline中完成page table的切换,并且也只能在trampoline中完成切换,因为只有trampoline中代码是同时在用户和内核空间中映射。但是我们现在还没有在trampoline代码中,我们现在还在一个普通的C函数中,所以这里我们将page table指针准备好,并将这个指针作为第二个参数传递给汇编代码,这个参数会出现在a1寄存器。

倒数第二行的作用是计算出我们将要跳转到汇编代码的地址。我们期望跳转的地址是tampoline中的userret函数,这个函数包含了所有能将我们带回到用户空间的指令。所以这里我们计算出了userret函数的地址。

倒数第一行,将fn指针作为一个函数指针,执行相应的函数(也就是userret函数)并传入两个参数,两个参数存储在a0,a1寄存器中。

Last updated