🗒️Xv6-Page Table

2023-4-23|2023-5-22
Anthony
Anthony
type
status
date
slug
summary
tags
category
icon
password
本Blog基于任职美国于波特兰洲立大学的教授@hhp3
在Youtube上对xv6内核的讲解视频
本期讲解xv6中的页表,视频链接在下面
 

预备知识

  • 在xv6中,页表由指针satp(supervisor register for address translation pointer)指向。这是一个CSR(control and status register)类型的指针。
 
  • 虚拟地址转换通常会在用户模式监督者模式(Supervisor Mode)打开。satp指针在系统初始化时为0,在进入用户态和监督者态之后将会指向需要使用的页表
 
  • 页表又分为Kernel Page TableUser Page Table。每一个用户都有一张User Page Table
  • Kernel Page Table 提供
    • 一对一的映射(绝大多数情况)
      映射所有的实际内存(实存),因此kernel代码不需要做任何的计算就可以访问实存中的任何地址。
      对于IO设备储存的映射
  • User Page Table
    • 每一个用户都拥有一张User Page Table
💡
RISCV提供了页表的五种组织形式,包括 Sv32(二级页表)“32位系统使用” Sv39(三级页表)“Xv6使用Sv39形式” Sv48(四级页表)“64位系统使用Sv39/Sv48”
 
  • 为了提升性能,真正的处理器都拥有TLBs(Translation Lookaside Buffers)。这些寄存器对于用户是透明(无需感知)的。作用是作为最近访问的PTE(Recent Page Table Entry)的缓存。对于用户而言,他们只需要知道每当sapt寄存器被更新(即更换页表),TLBs就应该被刷新(不然呢?)该操作由sfence_vma()函数执行sfence.vma完成。
 

虚地址映射流程

数据结构

SV39的结构
Sv39由三层页表结构组成(三层🌳结构)树的根部是一个4096字节的页表页,每一项占据8个字节,因此一个页表页可以有512个PTE,这些PTE包含树的下一级页表页的物理地址。在L0级页表中,PTE指向的是数据的物理地址块。
 
虚拟地址
其中EXT(25bits)忽略不使用,L2,L1,L0(9bits)分别用于三层页表的索引,Offset(12bits)被作为页内偏移。也就是说每一个页表都可以储存512个页表项,每个页表的大小为4kb
notion image
 
 
PTE(Page Table Entry)结构
10位保留,44位物理页号
V是否合法
R可读
W可写
X可执行
U用户态是否可以访问
notion image
 
 

流程

整体流程为,根据三个9bits的L2,L1,L0索引找到对应的PTE,获取44bits物理页号,然后拼接上12bits页内偏移,得到56位真实地址
notion image
 
 
 

深入代码

任何对于主存的LOAD/STORE/FETCH操作在xv6中都反映为walk the page table
kernel/vm.c walk函数中访问若干次内存
功能:该函数根据给定的虚拟地址(Va)计算一个物理PTE地址
返回值pte_t* 即一个指向PTE地址的指针
接收参数
  • pagetable_t pagetable 一张页表
  • unit64 va 一个无符号64位地址
  • alloc 0则walk through table, !0则创建一张新表
细节
  • if (va ≥ MAXVA) panic(”walk”) 首先判断虚拟地址是否合法
  • for(int level = 2; level > 0; level--) {} 一个for循环遍历从高层到低层遍历页表(from level=2 → level=1)🌳
    • pte_t *pte = &pagetable[PX(level, va)]; 这里是要获下一层PTE的地址(&pagetable),我们查看一下PX做了什么,PX定义在kernel/riscv.h
      • 显然,根据虚拟地址的结构,我们看到PXva在不断地右移,并且与9bits掩码PXMAS做与操作,那很可能PX就是想要取va中的L2,L1,L0数据段。通过查看PXSHIFT 可以进一步验证我们的想法,当level=1,PXSHIFT=21;level=2,PXSHIFT=30。
        也就是说PX是在第三级和第二级页表中根据Va寻找下一级页表的物理地址的。再直白点就是取了L2,L1,L0的那9bits.
         
    • if(*pte & PTE_V) {} 这里是判断有没有获取到合法的PTE地址,其中PTE_Vkernel/riscv.h中定义为#define PTE_V (1L << 0) // valid 根据PTE结构我们就可以知道这条语句是判断PTE的最后一位valid位是否合法。如果合法,我们就进入这个if
      • 来看看这个if里面做了什么吧
        根据函数名字PTE2PA盲猜是页表toPA(Physical Address)的转换。同样地我们在kernel/riscv.h中找到PTE2PA
        接收的是一个PTE地址(64bits)首先右移10位,回想PTE的结构就知道这里我们去掉了最后的10位,留下[reserved(10bits)][physical page number(44bits)]。然后我们再左移12位,回想一下真实的物理地址的结构就知道,真实的物理地址是[PPN(44bits)][offset(12bits)],因此我们左移12位,后面的offset就是0,所以我们得到的就是下一级页表的地址(也就说是下一级页表的物理位置的头)。
         
    • else
      • 可以看到这里判断了!alloc(也就是非分配新页表)或者pagetable = (pde_t*)kalloc()) == 0 (感觉这里就是一个找不到的处理)如果既不是要分配,又找不到,那就返回0(应该是错误的意思)
        💡
        有关kalloc()可以参考我的另一篇博客
        🗒️
        Xv6-Klloc
         
        如果alloc是非0的(就是说要分配新的页表)那么就调用memset(pagetable, 0, PGSIZE); 分配内存,然后将pte指针指向该页表的地址。其中valid位被置为1。我们可以在kernel/riscv.h中查看函数PA2PTE
        即给一个刚分配好的物理地址pa,然后右移12位,左移10位。回顾一下PTE的结构我们就可以知道,这样子刚刚好最后十位就是一堆校验,然后有44位的physical page number,还有10位保留。(其实这就是刚刚PTE2PA的逆过程)
         
  • 最后,return &pagetable[PX(0, va)]; 当遍历到最后一级的时候,调用PX,传入参数为level=0。根据上面对PX的分析,当level=0,PXSHIFT=12 。这就意味着我们取出了PTE中L0的九位,然后根据这9位置去L0级的页表,找到属于这个va的44bits PNN。
 

walk函数总结

这个函数接收一个L2级别页表,接收一个虚拟地址,目的是找到该虚拟地址对应的真实地址的44bitsPNN。我们首先判断了va是否合法,然后从第二级向下搜索到完第一级。搜索的过程可以描述为:
首选由PX函数取出属于该级别的9bits索引,然后根据这个索引找到下一级表的PNN。据此找到下一级表的PA。然后根据这个PA找到下一级表之后,又根据一开始的VA,匹配到属于这一级别的9bits索引,找到下一级表的PNN,找到PA。然后在第0级表中,也是找到9bits的0级别索引,返回一个44bitsPNN。
后续应该只需要将这个PNN和12bits offset拼接就可以得到56bits真实地址了。
Words in Real ScenesXv6-Klloc