MIT6.S081最详解析与归纳——lab3:Page tables


源码:https://github.com/InQing/xv6-operating-system/tree/pgtbl

问题的引入:从上一节的系统调用我们知道,用户进程如果要获取内核的数据,必须通过copyout函数实现传递,同理,内核获取用户进程数据也需要通过copyin函数。然而,这样是通过软件方法模拟三级页表的遍历从而实现的,有没有更高效的方式呢?

有——那就是通过硬件。

我们都知道,页表是通过硬件遍历寻址的,MMU,TLB也都是硬件支持,所以速度更快。
在这里插入图片描述

(一)前置知识:页表详解

(1)页表映射

  • 虚拟地址:
    RISC-V中,地址是64位的,但仅用低位39位表示虚拟地址。39位的虚拟地址中,27位为页表项(PTE)的编号,12位为offest
  • PTE:
    内存中,每个PTE的自身大小占4B
    PTE由物理块号(PPN)和flags构成。
    每个物理块大小为2^12字节(页长)=4KB。物理块号对应物理内存中高44位的地址,物理块号的44位+offset(在物理块中对于物理块起始地址的偏移量)的12位构成了一个物理地址。
  • 虚拟地址到物理地址的映射关系:
    虚拟地址=PTE编号+offset
    PTE编号+页表基地址->物理块号
    物理块号+offset=物理地址
  • 寻址过程:
    (1)MMU从satp寄存器中获取页表的基地址
    (2)从虚拟地址中取出PTE编号与offest,根据页表的基地址,MMU在内存中找到该页表,然后遍历页表,由PTE编号找到相应的PTE
    (3)从PTE中取出PNN,如果是单级页表,则PNN+offest则为最终物理地址,如果是多级页表,则PNN作为下一级页表的基地址
    在这里插入图片描述

(2)多级页表

  • 多级页表的优势
    多级页表的好处是节约页表本身大小所占用的内存
    RISC-V中,每级页表占用8bit,即每级页表维护2^8=512个页表项。
    查找一个映射,从L2->L1->L0,只需要访问3*512的条目。而如果只有单级页表,一页中包含2^27个条目,需要全部放入内存中。
  • 三级页表
    SV39中,每个进程维护一张用户地址空间页表和一张内核地址空间页表,每个页表都是三级的。
    高地址为内核空间,低地址为用户空间
  • 三级页表的映射过程
  1. 逻辑地址划分:
三级 二级 一级 偏移量
L2 L1 L0 Offset
9bits 9bits 9bits 12bits
  1. 从satp寄存器中取出三级页表的基地址A3
    计算三级页表条目地址(PTE):A3+L2x4 (每个PTE占4字节)
    读取该PTE对应的物理块号PNN,读取结果作为二级页表的基地址A2
  2. 获取二级页表的基地址A2
    计算二级页表条目地址(PTE):A2+L1x4 (每个PTE占4字节)
    读取该PTE对应的物理块号PNN,读取结果作为一级页表的基地址A1
  3. 获取一级页表的基地址A1
    计算一级页表条目地址(PTE):A1+L0x4 (每个PTE占4字节)
    读取该PTE对应的物理块号PNN,读取结果作为最终物理页面的基地址A0
  4. 最终物理地址为:A0+offest
    在这里插入图片描述
    另外说明一下,kernel里u开头的函数代表用户进程的,k开头的函数代表内核态的,其中有vm的就是和虚拟内存相关的

(二)Print a page table

(1)实验要求

写一个函数vmprint(pagetable_t pagetable),用于打印页表,包括页表深度,PTE与PA(物理地址,其实可以理解为PNN)

(2)实验思路

按照提示,分析一下freewalk

void freewalk(pagetable_t pagetable)
{
   
   
  // there are 2^9 = 512 PTEs in a page table.
  for (int i = 0; i < 512; i++)
  {
   
   
    pte_t pte = pagetable[i];
    if ((pte & PTE_V) && (pte & (PTE_R | PTE_W | PTE_X)) == 0)
    {
   
   
      // this PTE points to a lower-level page table.
      uint64 child = PTE2PA(pte);
      freewalk((pagetable_t)child);
      pagetable[i] = 0;
    }
    else if (pte & PTE_V)
    {
   
   
      panic("freewalk: leaf");
    }
  }
  kfree((void *)pagetable);
}

通过分析可知:

  • pagetable:页表基地址。i:PTE编号。pte(pagetable[i]):PTE,由PNN与flag组成。child:下一级页表的基地址,如果已经是一级页表了,那就是最终物理地址。PTE2PA:将PNN转为物理地址,原型为#define PTE2PA(pte) (((pte) >> 10) << 12),可以看到,实际上就是清除了PTE的标志位,然后扩展成一个56位的物理地址(PNN本身44位)
  • pte & PTE_V:判断该PTE是否valid
  • pte & (PTE_R | PTE_W | PTE_X):判断PTE是否可读、写、或执行。通过这个条件可以判断是否是最后一级页表,因为只有最后一级的pte才对应最终物理地址,一定满足读写可执行之一的条件,而第二、第三级的pte对应的只是下一级页表的索引地址,不需要读写或可执行
  • 页表的三级遍历由递归实现

(3)实验代码

根据上述分析,容易给出代码。
因为要打印起始页表的地址,不适合放入递归里写,所以拆成了两个函数。
注意,打印的… … …表示的是页表的深度而非级数,事实上是从三级页表开始索引,但深度是一。

void vmprint(pagetable_t pagetable)
{
   
   
  printf("page table %p\n", pagetable);

  printwalk(pagetable, 1); // 递归打印页表
}

void printwalk(pagetable_t pagetable, int depth)
{
   
   
  for (int i = 0; i < 512; i++)
  {
   
   
    pte_t pte = pagetable[i];
    if (pte & PTE_V)
    {
   
    // PTE合法
      uint64 child = PTE2PA(pte);
      // 按format打印level,PTE与PA
      switch (depth)
      {
   
   
      case 1:
        printf("..");
        break;
      case 2:
        printf(".. ..");
        break;
      case 3:
        printf(".. .. ..");
        break;
      }
      printf("%d: pte %p pa %p\n", i, pte, child);

      // 只有在页表的最后一级,才可读写或可执行
      // 若不在最后一级,则继续递归
      if ((pte & (PTE_R | PTE_W | PTE_X)) == 0)
        printwalk((pagetable_t)child, depth + 1);
    }
  }
}

最后,在exec.c里添加调用

	...
  if(p->pid == 1)
    vmprint(p->pagetable);

  return argc; // this ends up in a0, the first argument to main(argc, argv)

(三)A kernel page table per process

(1)实验要求

xv6原本的设计是,每个用户进程维护各自的用户页表,但当进入内核态时,所有进程都切换到同一张内核页表,这张内核页表是全局共享的
该实验的目的是让每个进程都有一张属于自己的内核页表,这一步的作用会在下一个实验揭晓。

(2)实验步骤

**1.**在PCB中添加内核页表

struct proc {
   
   
  struct spinlock lock;

  // p->lock must be held when using these:
  enum procstate state;        // Process state
  struct proc *parent;         // Parent process
  void *chan;                  // If non-zero, sleeping on chan
  int killed;                  // If non-zero, have been killed
  int xstate;                  // Exit status to be returned to parent's wait
  int pid;                     // Process ID

  // these are private to the process, so p->lock need not be held.
  uint64 kstack;               // Virtual address of kernel stack
  uint64 sz;                   // Size of process memory (bytes)
  pagetable_t pagetable;       // User page table
->pagetable_t kernelpagetable; // kernel page table for per process
  struct trapframe *trapframe; // data page for trampoline.S
  struct context context;      // swtch() here to run process
  struct file *ofile[NOFILE];  // Open files
  struct inode *cwd;           // Current directory
  char name[16];               // Process name (debugging)
};

2. 初始化内核页表函数 proc_kvminit()
先来看看 kvminit() 函数,它是初始化 全局内核页表

void kvminit()
{
   
   
  kernel_pagetable = (pagetable_t)kalloc()
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值