假设我们在支持 M、S 和 U 模式的 HART 上,并且当前处于启用虚拟地址的 S 模式,并且
satp
指向当前 PTE 表(内核 PTE 表)。
现在假设我们要在 U 模式下启动一个进程,那么我首先分配所有所需的页面并填充新的进程 PTE 表(通过手动将主管虚拟地址转换为物理地址)。然而,在使用
sret
切换到 U 模式之前,我们首先需要加载进程 PTE 表的地址,因为在 U 模式下 satp
将无法访问。
但是当我们使用用户 PTE 表更新
satp
时,我们将无法访问我们的内核代码,因为 satp
立即开始转换虚拟地址,并且从现在起我们将只访问进程内存而不访问内核内存 satp
不再指向内核 PTE 表。
我们也应该无法访问下一条指令,因为
pc
持有由旧PTE表管理的虚拟地址,因此不再有效,因为现在satp
指向新的PTE表。
我找到的唯一解决方案是暂时关闭内核虚拟内存,手动翻译内核虚拟地址来存储内核内容,跳转到新PTE表指向的页面,在
satp
中加载新进程PTE表从而进入在用户进程虚拟内存中,然后发出 sret
进入 U 模式。
为用户进程启用虚拟内存而不干扰内核虚拟内存的正确方法是什么?
当我们想要管理在 S 模式下在 U 模式下发出的中断时,也会出现同样的问题。如果管理程序中断向量表存储在内核虚拟内存中而不是物理内存中,那么当用户进程引起中断时,MMU 将使用虚拟地址通过
satp
查找中断向量表,但 satp
现在指向不映射 IVT 的用户进程 PTE 表。
即使在这种情况下,我找到的唯一解决方案是在管理模式下暂时禁用虚拟内存,以便找到应该存储在众所周知的物理地址的内核 PTE 表的位置,以便在没有 MMU 的情况下找到该表。
有不依赖于禁用内核虚拟内存的解决方案吗?为什么 RiscV 规范没有添加新的 CSR 来仅在用户模式下管理地址转换?
但是当我们使用用户 PTE 表更新 satp 时,我们将无法访问内核代码,因为 satp 立即开始翻译虚拟地址,并且我们将仅访问进程内存而不是内核内存,因为现在 satp 不指向不再查看内核 PTE 表。
...
为用户进程启用虚拟内存而不干扰内核虚拟内存的正确方法是什么?
...
我可以想到两种方法:
将内核页面映射到每个用户进程的页表中,并清除内核页面 PTE 中的
U
位。这允许在 S 模式下运行的内核访问这些页面(在处理异常或中断时),并且如果在 U 模式下运行的进程尝试执行相同操作,则会触发页面错误。设置 SUM
位还可以让内核访问进程页面。
可以为内核 PTE 设置
G
(全局)位,以便在上下文切换之间在 TLB 中维护内核页面映射,从而提高性能。
中完成的
内核有单独的页表。但保留一个映射到内核和进程页表的 trampoline 页,并保存修改
satp
和特权模式的代码。当切换到用户进程时,内核会跳转到该蹦床,并且异常和中断从蹦床中的某些代码开始,这些代码在跳转到相应的处理程序之前指向内核页表。这节省了空间(更少的页表映射),但使上下文切换的成本更高一些。
xv6 操作系统的人们选择了这种方法。