本文细致的剖析了 2021 FALL MIT 6.S081 课程的一项实验, Lab 链接 Lab: page tables (mit.edu) 。
新人博主,大家的每一次阅读都会激励我继续创作,大家的点赞将会是我继续更新的巨大动力,对文中内容或实验过程中有任何疑问欢迎留言!
Lab 3 主要探索页表并对其进行修改,以简化将数据从用户空间复制到内核空间的函数。本实验涉及到一些虚拟地址和物理地址结构,以及两者之间的转换,需要对虚拟内存的知识有基本认识。
Print a page table (easy)
为了帮助您了解 RISC-V 页表,也为将来的调试提供帮助,您的任务是编写一个打印页表内容的函数。
定义一个名为
vmprint()
的函数。它应当接收一个pagetable_t
作为参数,并以下面描述的格式打印该页表。在 exec.c 中的return argc
之前插入if(p->pid==1) vmprint(p->pagetable)
,以打印第一个进程的页表。如果你通过了pte printout
测试的make grade
,你将获得此作业的满分。
提示
- 你可以将
vmprint()
放在 kernel/vm.c 中 - 使用定义在 kernel/riscv.h 末尾处的宏
- 函数
freewalk
可能会对你有所启发 - 将
vmprint
的原型定义在 kernel/defs.h 中,这样你就可以在exec.c
中调用它了 - 在
printf
中使用%p
来打印像上面示例中的完成的 64 比特的十六进制 PTE 和地址
实验过程
上一实验中我们已经对内存管理中涉及到的主要函数与设计思想进行了剖析,因此完成本实验较为容易。
首先,按照题目要求,在 kernel/exec.c 中插入 if(p->pid==1) vmprint(p->pagetable)
:
rust
// kernel/exec.c
// ...
int
exec(char *path, char **argv)
{
// ...
// Commit to the user image.
oldpagetable = p->pagetable;
p->pagetable = pagetable;
p->sz = sz;
p->trapframe->epc = elf.entry; // initial program counter = main
p->trapframe->sp = sp; // initial stack pointer
proc_freepagetable(oldpagetable, oldsz);
if(p->pid==1) vmprint(p->pagetable);
return argc; // this ends up in a0, the first argument to main(argc, argv)
// ...
}
vmprint
接收一个 pagetable_t
类型的参数,并打印如下列格式,第一行为 vmprint
的输入参数值,接着以深度优先的方式,打印出页表的所有 PTE," .." 表示 page directory 的层级:
css
page table 0x0000000087f6e000
..0: pte 0x0000000021fda801 pa 0x0000000087f6a000
.. ..0: pte 0x0000000021fda401 pa 0x0000000087f69000
.. .. ..0: pte 0x0000000021fdac1f pa 0x0000000087f6b000
.. .. ..1: pte 0x0000000021fda00f pa 0x0000000087f68000
.. .. ..2: pte 0x0000000021fd9c1f pa 0x0000000087f67000
..255: pte 0x0000000021fdb401 pa 0x0000000087f6d000
.. ..511: pte 0x0000000021fdb001 pa 0x0000000087f6c000
.. .. ..510: pte 0x0000000021fdd807 pa 0x0000000087f76000
.. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000
xv6 采用了三级页表机制,与上一个题目中分析的 freewalk()
函数类似,(请参考深入学习操作系统!详细剖析 MIT 6.S081 课程 Lab 3 : page tables - 1 Speed up system calls),我们要依据不同的页表层级来做不同的操作,首先若 PTE 存在且 Valid bit 被置位,则为有效页表,应该打印页表信息。
只要 W
、 R
、 X
其中任何一位被置位,就是最后一级的页表,不需要递归。
若 PTE 不可读 & 不可写 & 不可执行,代表为第一/二级的页表,应该继续递归处理。
按照这一思路,参考 freewalk()
函数的代码,我们在 kernel/vm.c 中添加 vmprint()
函数的代码,由于vmprint()
仅接收一个 pagetable_t
类型的参数,而我们在递归过程中还需要一个参数来标记递归深度,因此我们设置一个辅助函数vmprint_recursive()
,参数类型为 pagetable_t
和 int
:
scss
// kernel/vm.c
// ...
void
vmprint_recursive(pagetable_t pagetable, int level)
{
for (int i = 0; i < 512; i++)
{
pte_t pte = pagetable[i];
if (pte & PTE_V) {
uint64 pa = PTE2PA(pte);
printf("..");
for (int j = 0; j < level; j++) {
printf(" ..");
}
printf("%d: pte %p pa %p\n", i, pte, pa);
// PTE without any WRX bit set points to low-level page table
if ((pte & (PTE_W|PTE_R|PTE_X)) == 0)
vmprint_recursive((pagetable_t)pa, level + 1);
}
}
}
void
vmprint(pagetable_t pagetable)
{
printf("page table %p\n", pagetable);
vmprint_recursive(pagetable, 0);
}
注意在 kernel/exec.c 中并未引用 kernel/vm.c ,那么 kernel/exec.c 中执行 vmprint()
函数应该会报错,我们应在 kernel/def.h 文件中增加一个函数定义(你问怎们知道在这个文件里增加?问就是对着 kernel/exec.c 包含的头文件一个一个找的):
arduino
// kernel/def.h
// ...
// vm.c
void kvminit(void);
void kvminithart(void);
void kvmmap(pagetable_t, uint64, uint64, uint64, int);
int mappages(pagetable_t, uint64, uint64, uint64, int);
pagetable_t uvmcreate(void);
void uvminit(pagetable_t, uchar *, uint);
uint64 uvmalloc(pagetable_t, uint64, uint64);
uint64 uvmdealloc(pagetable_t, uint64, uint64);
int uvmcopy(pagetable_t, pagetable_t, uint64);
void uvmfree(pagetable_t, uint64);
void uvmunmap(pagetable_t, uint64, uint64, int);
void uvmclear(pagetable_t, uint64);
uint64 walkaddr(pagetable_t, uint64);
int copyout(pagetable_t, uint64, char *, uint64);
int copyin(pagetable_t, char *, uint64, uint64);
int copyinstr(pagetable_t, char *, uint64, uint64);
void vmprint(pagetable_t pagetable);
// plic.c
// ...
此时,启动 xv6 ,即可看到打印出来的页表。