xv6项目开源—03

xv6项目开源—03

理论

1、页表是操作系统为每个进程提供私有地址空间和内存的机制。页表决定了内存地址的含义,以及物理内存的哪些部分可以访问。它们允许xv6隔离不同进程的地址空间,并将它们复用到单个物理内存上。页表还提供了一层抽象(a level of indirection),这允许xv6执行一些特殊操作:映射相同的内存到不同的地址空间中(a trampoline page),并用一个未映射的页面保护内核和用户栈区。

2、页表以三级的树型结构存储在物理内存中。该树的根是一个4096字节的页表页,其中包含512个PTE(页表条目),每个PTE中包含该树下一级页表页的物理地址。这些页中的每一个PTE都包含该树最后一级的512个PTE(也就是说每个PTE占8个字节)。分页硬件使用27位中的前9位在根页表页面中选择PTE,中间9位在树的下一级页表页面中选择PTE,最后9位选择最终的PTE。

3、因为 CPU 在执行转换时会在硬件中遍历三级结构,所以缺点是 CPU 必须从内存中加载三个 PTE 以将虚拟地址转换为物理地址。为了减少从物理内存加载 PTE 的开销,RISC-V CPU 将页表条目缓存在 Translation Look-aside Buffer (TLB) 中。

4、每个PTE包含标志位,这些标志位告诉分页硬件允许如何使用关联的虚拟地址。PTE_V指示PTE是否存在:如果它没有被设置,对页面的引用会导致异常(即不允许)。PTE_R控制是否允许指令读取到页面。PTE_W控制是否允许指令写入到页面。PTE_X控制CPU是否可以将页面内容解释为指令并执行它们。PTE_U控制用户模式下的指令是否被允许访问页面;如果没有设置PTE_U,PTE只能在管理模式下使用。

5、QEMU模拟了一台计算机,它包括从物理地址0x80000000开始并至少到0x86400000结束的RAM(物理内存),xv6称结束地址为PHYSTOP。QEMU模拟还包括I/O设备,如磁盘接口。QEMU将设备接口作为内存映射控制寄存器暴露给软件,这些寄存器位于物理地址空间0x80000000以下。内核可以通过读取/写入这些特殊的物理地址与设备交互;这种读取和写入与设备硬件而不是RAM通信。

6、内核必须在运行时为页表、用户内存、内核栈和管道缓冲区分配和释放物理内存。xv6使用内核末尾到PHYSTOP之间的物理内存进行运行时分配。它一次分配和释放整个4096字节的页面。它使用链表的数据结构将空闲页面记录下来。分配时需要从链表中删除页面;释放时需要将释放的页面添加到链表中。

实践

参考链接:MIT 6.S081 Operating System - 知乎 (zhihu.com)

注意事项:

开始编码之前,请阅读xv6手册的第3章和相关文件:

  • *kernel/memlayout.h*,它捕获了内存的布局。
  • *kernel/vm.c*,其中包含大多数虚拟内存(VM)代码。
  • *kernel/kalloc.c*,它包含分配和释放物理内存的代码。

Lab3这个实验主要涉及了解xv6物理地址和虚拟地址的转换和寻址, 内核态下的内存地址和用户态下的内存地址的差别等. xv6采用3层间接映射的方式来进行虚拟地址到物理地址的寻址. 我们需要为每个进程单独分配一个内核页表而不是所有进程在内核态时共用一个内核页表, 这极大的简化了将数据在内核态和用户态的来回传输, ricsv硬件可以直接帮助地址转化, 让不用我们手动地在内核态里做虚拟地址到物理地址的寻址转换.

1、Print a page table

定义一个名为vmprint()的函数。它应当接收一个pagetable_t作为参数,并以下面描述的格式打印该页表。在exec.c中的return argc之前插入if(p->pid==1) vmprint(p->pagetable),以打印第一个进程的页表。

看一下kernel/vm.c里面的freewalk方法,主要的代码如下:

// Recursively free page-table pages.
// All leaf mappings must already have been removed.
void freewalk(pagetable_t pagetable)
{
   
   
  // there are 2^9 = 512 PTEs in a page table.
  for(int i = 0; i < 512; i++){
   
    // 遍历页表中的每一个页表项(PTE)
    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); // 递归调用freewalk来释放这个子页表
      pagetable[i] = 0; // 清除当前页表项,表示该项不再指向任何页表或物理页面
    } else if(pte & PTE_V){
   
   
      // 如果页表项有效且代表一个叶节点(即直接指向物理内存的页表项),则报告错误
      // 因为在开始释放页表内存之前,所有的叶节点应该已经被移除
      panic("freewalk: leaf");
    }
  }
  // 在递归释放完所有子页表后,释放当前页表所占用的内存
  kfree((void*)pagetable);
}

这个函数的主要步骤如下:

  1. 遍历当前页表中的所有页表项(PTE),每个页表通常包含512个项。
  2. 对于每个页表项,检查它是否有效(PTE_V标志位)且不直接指向物理内存页(即没有设置读(PTE_R)、写(PTE_W)或执行(PTE_X)权限),这意味着该页表项指向另一个页表。
  3. 如果页表项指向另一个页表,使用提取的物理地址转换为虚拟地址,递归调用freewalk函数来释放该子页表所占用的内存。
  4. 如果发现任何直接指向物理内存的页表项(即叶节点),则调用panic函数报错,因为所有的叶节点映射应在此函数调用前已经被清除。
  5. 最后,释放当前页表占用的内存。

这个过程确保了整个页表结构被正确且彻底地清理,不会留下悬挂的指针或未释放的内存。

那么,根据freewalk,我们可以写下递归函数。对于每一个有效的页表项都打印其和其子项的内容。如果不是最后一层的页表就继续递归。通过level来控制前缀..的数量。

/**
 * @param pagetable 所要打印的页表
 * @param level 页表的层级
 */
void
_vmprint(pagetable_t pagetable, int level){
   
   
  // there are 2^9 = 512 PTEs in a page table.
  for(int i = 0; i < 512; i++){
   
    // 遍历页表中的512个页表项
    pte_t pte = pagetable[i]; // 获取当前页表项
    // PTE_V is a flag for whether the page table is valid
    if(pte & PTE_V){
   
    // 检查页表项是否有效
      for (int j = 0; j < level; j++){
   
    // 根据页表的层级打印缩进,增强可读性
        if (j) printf(" ");
        printf("..");
      }
      uint64 child = PTE2PA(pte); // 将页表项转换为物理地址
      printf("%d: pte %p pa %p\n", i, pte, child); // 打印页表项的索引、页表项本身和物理地址
      if((pte & (PTE_R|PTE_W|PTE_X)) == 0){
   
    // 如果页表项没有设置读写执行权限,表明它指向另一级页表
        // this PTE points to a lower-level page table.
        _vmprint((pagetable_t)child, level + 1); // 递归打印下一级页表
      }
    }
  }
  kfree((void*)pagetable); // 释放当前页表占用的内存(此行应谨慎处理,根据实际情况决定是否需要)
}

/**
 * @brief vmprint 打印页表
 * @param pagetable 所要打印的页表
 */
void
vmprint(pagetable_t pagetable){
   
   
  printf("page table %p\n", pagetable); // 打印页表的指针信息
  _vmprint(pagetable, 1); // 从第一层开始递归打印页表
}

/*
### `_vmprint`函数

- `pagetable`: 要打印的页表的指针。
- `level`: 页表的层级,用于辅助打印,使输出具有层次感。

函数遍历页表中的所有页表项(PTE),页表通常有512个页表项。

- 首先检查每个页表项是否有效,即`PTE_V`标志位是否被设置。只有有效的页表项才表示了一个实际的映射或指向另一级页表的指针。
- 对于每个有效的页表项,函数打印出层级前缀(通过打印`.`和空格来表示),使得输出具有层次结构。
- 接着,打印出页表项的索引`i`、页表项本身的值`pte`和它指向的物理地址`child`。物理地址是通过`PTE2PA`宏从页表项中提取出来的。
- 如果页表项没有设置任何读(`PTE_R`)、写(`PTE_W`)或执行(`PTE_X`)权限,这通常意味着该页表项指向另一级的页表,而不是直接映射到物理内存上。在这种情况下,函数会递归地调用自身`_vmprint`,以打印下一级的页表结构,层级`level`增加1。

### `vmprint`函数

这是一个对外的接口函数,用于开始打印页表的过程。

- `pagetabl
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星空有大海

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值