xv6 pagetables

发布时间 2023-11-21 00:07:38作者: trashwin

页表

地址空间简介

Xv6运行在Sv39 RISC-V上,这意味着只使用64位虚拟地址的底部39位;不使用前25位。在这个Sv39配置中,RISC-V页表逻辑上是一个包含227(134,217,728)个页表项(pte)的数组。每个PTE包含一个44位物理页码(PPN)和一些标志。分页硬件通过使用39位的前27位索引到页表中以查找PTE来转换虚拟地址,并生成一个56位的物理地址,其顶部44位来自PTE中的PPN,其底部12位来自原始虚拟地址。

页表
三层结构允许以内存高效的方式记录pte。在大范围的虚拟地址没有映射的常见情况下,三层结构可以省略整个页面目录。

PTE_V指示PTE是否存在:如果没有设置,对该页的引用将导致异常(即不允许)。PTE_R控制是否允许指令读到该页。PTE_W控制是否允许指令写入页。PTE_X控制CPU是否可以将页面的内容解释为指令并执行它们。PTE_U控制是否允许用户模式下的指令访问页面;如果不配置PTE_U,则该PTE只能在管理员模式下使用。

PTE项所指示的下一级页表的物理地址为PPN<<2,即(*PTE>>10)<<12,使用PTE2PA即可。

内核必须将可分页根页的物理地址写入satp寄存器。CPU将使用它自己的节点所指向的页表来翻译由后续指令生成的所有地址。每个CPU都有自己的页表,因此不同的CPU可以运行不同的进程,每个进程都有一个由自己的页表描述的私有地址空间。页目录位于物理内存中,内核可以通过使用标准存储指令写入PTE的虚拟地址来对页目录中PTE的内容进行编程。

Xv6为每个进程维护一个页表,描述每个进程的用户地址空间,外加一个描述内核地址空间的页表。内核配置其地址空间的布局,以便在可预测的虚拟地址上访问物理内存和各种硬件资源。

内核映射

trampoline和内核栈都不是直接映射。物理页在内核的虚拟地址空间中被映射两次:一次在虚拟地址空间的顶部,一次使用直接映射。虽然内核通过高内存映射使用它的堆栈,但内核也可以通过直接映射的地址访问它们。另一种设计可能只有直接映射,并使用直接映射地址上的堆栈。然而,在这种安排中,提供保护页将涉及取消映射虚拟地址,否则这些虚拟地址将引用物理内存,而物理内存将难以使用。

内核用权限PTE_R和PTE_X映射trampoline和kernel text的页面。内核从这些页面读取并执行指令。内核用PTE_R和PTE_W权限映射其他页,这样它就可以读写这些页中的内存。

trampoline中存放trampoline.S代码,用于从用户态进入内核态,并从内核态返回用户态,用户空间和内核空间的trampoline的虚拟地址相同,因此可以说所有进程的trampoline的虚拟地址和物理地址都相同(即内核trampoline)的物理地址。
trapframe中存放trampoline.S中的代码执行时的寄存器状态,不同进程的trapframe虚拟地址相同,内存地址不同。

内核空间为所有进程共享?

地址空间创建

使用pagetable_t来指向一个页表(用户或者内核页表)
函数接口主要在vm.c中,以kvm开头的函数操作内核页表,以uvm开头的函数操作用户页表。

  • walk 从页表中获取虚拟地址对应的PTE
  • mappages 为指定范围的虚拟地址和物理地址设置PTE
  • copyin 从用户空间拷贝数据到内核空间
  • copyout 从内核空间拷贝数据到用户空间

内核页表的初始化通过kvminit间接调用kvmmake进行,主要创建页表并对几个主要的直接映射(映射与物理地址相等的虚拟地址上的资源)页表项进行映射,然后为每个进程分配内核栈,大小为一页(并跟随一个保护页,PTE_V未设置),然后将根页表页的物理地址写入寄存器satp。
对于TLB,每次xv6更改页表,使用sfence_vma进行刷新。注意在更改satp之前之后都要刷新,确保对于旧页表的更新已完成

xv6使用内核之后和PHYSTOP之间的物理内存进行运行时分配,内核在运行时为页表、用户内存、内核堆栈和管道缓冲区分配和释放物理内存。
这部分主要在kalloc.c中,比较简单,使用单链表来组织空闲页。

进程地址空间

较简单,和各种课程学的类似
进程地址空间

uvmalloc用来申请内存,在原虚拟内存基础上扩充。

栈是一个单独的页面,并显示由exec创建的初始内容。包含命令行参数的字符串以及指向它们的指针数组位于堆栈的最顶端。下面是允许程序在main处启动的值,就像函数main(argc, argv)刚刚被调用一样。为了检测用户堆溢出了已分配的堆栈内存,xv6通过清除PTE_U标志在堆正下方放置了一个不可访问的保护页。如果用户栈溢出,并且进程试图使用栈下面的地址,则硬件将生成一个页面错误异常,因为在用户模式下运行的程序无法访问保护页。

每个进程都将其内存视为具有从0开始的连续虚拟地址,而进程的物理内存可以是非连续的。

exec使用uvmalloc申请内存空间,再使用loadseg函数将程序加载到对应页面中。
exec分配并初始化用户堆栈。它只分配一个堆栈页面。exec将参数字符串一次一个地复制到堆栈顶部,并在ustack中记录指向它们的指针。在传递给main的argv列表的末尾放置一个空指针。ustack中的前三个条目是伪返回程序计数器、argc和argv指针?然后把ustack中的所有栈指针传到用户栈中。