3.3 代码:创建一个地址空间

大多数用于操作地址空间和页表的xv6代码都写在vm.c (kernel/vm.c:1)中。其核心数据结构是pagetable_t,它实际上是指向RISC-V根页表页的指针;一个pagetable_t可以是内核页表,也可以是一个进程页表。最核心的函数是walkmappages,前者为虚拟地址找到PTE,后者为新映射装载PTE。名称以kvm开头的函数操作内核页表;以uvm开头的函数操作用户页表;其他函数用于二者。copyoutcopyin复制数据到用户虚拟地址或从用户虚拟地址复制数据,这些虚拟地址作为系统调用参数提供; 由于它们需要显式地翻译这些地址,以便找到相应的物理内存,故将它们写在vm.c中。

在按序启动的前期,main调用kvminit (kernel/vm.c:22) 来创建内核的页表。这个调用发生在xv6使能RISC-V分页之前,所以地址直接引用物理内存。kvminit首先分配一个物理内存页面来保存根页表页。然后它调用kvmmap来装载内核需要的转换。转换包括内核的指令和数据、物理内存的上限是PHYSTOP,并包括实际上是设备的内存。

kvmmap(kernel/vm.c:118)调用mappages(kernel/vm.c:149),mappages将范围虚拟地址到同等范围物理地址的映射装载到一个页表中。它以页面大小为间隔,为范围内的每个虚拟地址单独执行此操作。对于要映射的每个虚拟地址,mappages调用walk来查找该地址的PTE地址。然后,它初始化PTE以保存相关的物理页号、所需权限(PTE_WPTE_X和/或PTE_R)以及用于标记PTE有效的PTE_V(kernel/vm.c:161)。

在查找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)。