MIT 6.S081 pgtbl 实验篇(lab3):Print a page table (easy)
2026/8/5 23:55:22 网站建设 项目流程

Print a page table (easy)


实验目标

本实验要求我们在 xv6 内核中实现vmprint()函数:给定一个页表,按层级结构把它打印出来。当内核启动、加载第一个用户进程init(pid=1)时,打印它的页表,作为对 Sv39 三级页表结构的直观验证。

实验本身代码量很小,真正的价值在于借打印过程吃透页表遍历(walk)的递归本质——这也是A kernel page table per process (hard)Simplify copyin/copyinstr (hard)的基础。

前置知识:Sv39 三级页表

在 Sv39 分页模式下,虚拟地址到物理地址的翻译依赖一棵三级页表树("三级"指树的深度为 3):

整棵树的遍历过程,就是从satp指向的根页表出发,依据虚拟地址的 3 段 9 位索引逐级下行,直到抵达叶子节点取出物理页地址。

实现思路:复用 freewalk 的遍历骨架

xv6 已经在kernel/vm.c中提供了freewalk(),它递归遍历整棵页表树并释放页表页。我们的vmprint()与它的遍历逻辑完全一致,区别只在于"访问节点时做什么":

换句话说,把"释放"换成"打印",骨架原封不动。先看一下freewalk()的实现:

// kernel/vm.c/* * 递归释放页表页本身(不是用户数据页)。 * 调用前,所有叶子映射(用户数据页)必须已被移除。 * * 工作原理: * 遍历页表中的 512 个 PTE: * - 如果 PTE 有效且没有 R/W/X 标志 → 它指向下一级页表,递归释放 * - 如果 PTE 有效且有 R/W/X 标志 → 它是叶子 PTE,说明用户数据页还没清除,panic * 最后释放当前页表页本身。 */voidfreewalk(pagetable_tpagetable){// 每个页表页有 2^9 = 512 个 PTEfor(inti=0;i<512;i++){pte_tpte=pagetable[i];// PTE 有效 + 没有 R/W/X → 这是指向下级页表的中间节点if((pte&PTE_V)&&(pte&(PTE_R|PTE_W|PTE_X))==0){uint64 child=PTE2PA(pte);freewalk((pagetable_t)child);// 递归释放子页表pagetable[i]=0;// 清除当前 PTE}elseif(pte&PTE_V){// PTE 有效且有 R/W/X → 叶子节点,说明用户数据页还没被释放panic("freewalk: leaf");}}// 释放当前页表页本身kfree((void*)pagetable);}

对照上面的逻辑,实现vmprint()的关键点只有两个:

  1. 递归出口:PTE 无效(!PTE_V)时直接continue,不打印、也不再下行(xing)。
  2. 区分中间节点与叶子:用(pte & (PTE_R|PTE_W|PTE_X)) == 0判断——没有 R/W/X 权限位的 PTE 一定指向"下一级页表"(中间节点),需要继续递归;否则就是叶子,打印即可。

代码实现

kernel/vm.c:实现vmprintvmprint_helper

// kernel/vm.cstaticintvmprint_helper(pagetable_tpagetable,intdepth){// There are 2^9 = 512 PTEs in a page tablefor(inti=0;i<512;++i){pte_tpte=pagetable[i];if((pte&PTE_V)==0)/* 优先级:"==" > "&" "*/continue;// 如果该 PTE 无效,无需再打印对应的页表// 当前第 depth+1 级页表,打印 depth+1 个 ".."printf("..");for(inti=0;i<depth;++i)printf(" ..");// 按格式打印页表内容uint64 child=PTE2PA(pte);printf("%d: pte %p pa %p\n",i,pte,child);// 如果该 PTE 对应的不是物理地址,递归打印其对应的中间页表if((pte&(PTE_R|PTE_W|PTE_X))==0)vmprint_helper((pagetable_t)child,depth+1);}return0;}intvmprint(pagetable_tpagetable){// 按格式打印顶级页表 pagetableprintf("page table %p\n",pagetable);// 递归打印中间页表returnvmprint_helper(pagetable,0);}

格式说明:用depth参数记录当前递归深度(根页表为 0)。每深入一级,depth+1就多打印一组..,从而用缩进直观体现页表的层级关系——根页表项前缀..,二级.. ..,三级.. .. ..

小细节:if ((pte & PTE_V) == 0)==的优先级高于&,所以必须加括号,否则会被解析成pte & (PTE_V == 0),逻辑就错了。

kernel/defs.h:添加函数声明

defs.hvm.c函数声明区追加一行,让其他文件能调用vmprint

// kernel/defs.h...// vm.c...intcopyin(pagetable_t,char*,uint64,uint64);intcopyinstr(pagetable_t,char*,uint64,uint64);intvmprint(pagetable_t);// Print a page table

kernel/exec.c:在exec中触发打印

按照实验要求,在exec()函数的return argc之前插入调用。只打印 pid=1 的进程(即init),避免每个进程启动都刷屏:

...p->trapframe->epc=elf.entry;// initial program counter = mainp->trapframe->sp=sp;// initial stack pointerproc_freepagetable(oldpagetable,oldsz);if(p->pid==1)vmprint(p->pagetable);// Print a page table if pid = 1returnargc;// this ends up in a0, the first argument to main(argc, argv)bad:...

验证

回到 xv6 目录执行:

makeqemu

启动后,内核加载init时会在串口输出类似下面的内容:

xv6 kernel is booting hart2starting hart1starting page table 0x0000000087f64000..0: pte 0x0000000021fd8001 pa 0x0000000087f60000....0: pte 0x0000000021fd7c01 pa 0x0000000087f5f000......0: pte 0x0000000021fd841f pa 0x0000000087f61000......1: pte 0x0000000021fd780f pa 0x0000000087f5e000......2: pte 0x0000000021fd741f pa 0x0000000087f5d000..255: pte 0x0000000021fd8c01 pa 0x0000000087f63000....511: pte 0x0000000021fd8801 pa 0x0000000087f62000......510: pte 0x0000000021fed807 pa 0x0000000087fb6000......511: pte 0x0000000020001c0b pa 0x0000000080007000 init: startingsh

每一行的缩进层级对应页表的级,pa即为该 PTE 指向的物理地址。能够正常打印即说明实现正确。

复盘

这些在做实验时经常用到,当成常识就好

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询