3.3 代码:创建一个地址空间
大多数用于操作地址空间和页表的xv6代码都写在 vm.c (kernel/vm.c:1) 中。其核心数据结构是pagetable_t
,它实际上是指向RISC-V根页表页的指针;一个pagetable_t
可以是内核页表,也可以是一个进程页表。最核心的函数是walk
和mappages
,前者为虚拟地址找到PTE,后者为新映射装载PTE。名称以kvm
开头的函数操作内核页表;以uvm
开头的函数操作用户页表;其他函数用于二者。copyout
和copyin
复制数据到用户虚拟地址或从用户虚拟地址复制数据,这些虚拟地址作为系统调用参数提供; 由于它们需要显式地翻译这些地址,以便找到相应的物理内存,故将它们写在vm.c中。
在启动序列的前期,main
调用 kvminit
(kernel/vm.c:54) 以使用 kvmmake
(kernel/vm.c:20) 创建内核的页表。此调用发生在 xv6 启用 RISC-V 上的分页之前,因此地址直接引用物理内存。 kvmmake
首先分配一个物理内存页来保存根页表页。然后它调用kvmmap
来装载内核需要的转换。转换包括内核的指令和数据、物理内存的上限到 PHYSTOP
,并包括实际上是设备的内存。 Proc_mapstacks
(kernel/proc.c:33) 为每个进程分配一个内核堆栈。它调用 kvmmap 将每个堆栈映射到由 KSTACK 生成的虚拟地址,从而为无效的堆栈保护页面留出空间。
kvmmap
(kernel/vm.c:127)调用mappages
(kernel/vm.c:138),mappages
将范围虚拟地址到同等范围物理地址的映射装载到一个页表中。它以页面大小为间隔,为范围内的每个虚拟地址单独执行此操作。对于要映射的每个虚拟地址,mappages
调用walk
来查找该地址的PTE地址。然后,它初始化PTE以保存相关的物理页号、所需权限(PTE_W
、PTE_X
和/或PTE_R
)以及用于标记PTE有效的PTE_V
(kernel/vm.c:153)。
在查找PTE中的虚拟地址(参见图3.2)时,walk
(kernel/vm.c:72)模仿RISC-V分页硬件。walk
一次从3级页表中获取9个比特位。它使用上一级的9位虚拟地址来查找下一级页表或最终页面的PTE (kernel/vm.c:78)。如果PTE无效,则所需的页面还没有分配;如果设置了alloc
参数,walk
就会分配一个新的页表页面,并将其物理地址放在PTE中。它返回树中最低一级的PTE地址(kernel/vm.c:88)。
上面的代码依赖于直接映射到内核虚拟地址空间中的物理内存。例如,当walk
降低页表的级别时,它从PTE (kernel/vm.c:80)中提取下一级页表的(物理)地址,然后使用该地址作为虚拟地址来获取下一级的PTE (kernel/vm.c:78)。
main
调用kvminithart
(kernel/vm.c:53)来安装内核页表。它将根页表页的物理地址写入寄存器satp
。之后,CPU将使用内核页表转换地址。由于内核使用标识映射,下一条指令的当前虚拟地址将映射到正确的物理内存地址。
main
中调用的procinit
(kernel/proc.c:26)为每个进程分配一个内核栈。它将每个栈映射到KSTACK
生成的虚拟地址,这为无效的栈保护页面留下了空间。kvmmap
将映射的PTE添加到内核页表中,对kvminithart
的调用将内核页表重新加载到satp
中,以便硬件知道新的PTE。
每个RISC-V CPU都将页表条目缓存在转译后备缓冲器(快表/TLB)中,当xv6更改页表时,它必须告诉CPU使相应的缓存TLB条目无效。如果没有这么做,那么在某个时候TLB可能会使用旧的缓存映射,指向一个在此期间已分配给另一个进程的物理页面,这样会导致一个进程可能能够在其他进程的内存上涂鸦。RISC-V有一个指令sfence.vma
,用于刷新当前CPU的TLB。xv6在重新加载satp
寄存器后,在kvminithart
中执行sfence.vma
,并在返回用户空间之前在用于切换至一个用户页表的trampoline
代码中执行sfence.vma
(kernel/trampoline.S:79)。