实验内容网址:https://xv6.dgs.zone/labs/requirements/lab3.html
本实验的代码分支:https://gitee.com/dragonlalala/xv6-labs-2020/tree/pgtbl2/
Print a page table
关键点:递归、三级页表
思路:
用上图来解释三级页表的原理最为清晰明了。satp
的作用是存放根页表页在物理内存中的地址。页表以三级的树型结构存储在物理内存中。该树的根是一个4096字节(512*8byte)的页表页,其中包含512个PTE,每个PTE中包含该树下一级页表页的物理地址。这些页中的每一个PTE都包含该树最后一级的512个PTE(也就是说每个PTE占8个字节,正如图3.2最下面所描绘的)。分页硬件使用27位中的前9位在根页表页面中选择PTE,中间9位在树的下一级页表页面中选择PTE,最后9位选择最终的PTE。一级页表通过stap和L2确定二级页表的基地址,二级页表的基地址加上L1确定三级页表的基地址,三级页表的基地址和L0确定物理地址的前44位,与原来offset的12位组成了物理地址。总体上说,这个过程类似3级512叉树。这样做的目的是为了节省内存,在大范围的虚拟地址没有被映射的常见情况下,三级结构可以忽略整个页面目录。
在每一级页表中,后十位是标志位,在一二级页表中,这些标志位中的RWX是不使用的,一二级页表是起到索引功能,所以只使用了V标志位。
步骤&代码:
kernel/vm.c
中定义vmprint()
函数,题目要求参数为pagetable_t
,但在本题中,需要进行递归,并且递归过程中需要知道当前是递归的第几层,所以需要另外定义一个递归函数,_vmprint(pagetable, level);
传递页表指针和递归层数。需要注意的是vmprint()
函数需要到def.h文件中声明,_vmprint()
函数需要在vmprint()
函数前进行定义。
void
vmprint(pagetable_t pagetable){
// 打印根页表
printf("page table %p\n", pagetable);
// 重新写个函数是为了传递level级和递归
_vmprint(pagetable, 1);
}
- 编写
_vmprint()
函数,仿照freewalk
函数的遍历方式。通过pte & PTE_V
可以判断pte的有效性,在有效的前提下通过(pte & (PTE_R|PTE_W|PTE_X)) == 0)
可以判断是哪一级页表,在第三级页表中,第三级页表存放的是物理地址,页表中页表项中W位,R位,X位起码有一位会被设置为1。根据以上思路编写如下代码:
void _vmprint(pagetable_t pagetable, int level){
for(int i = 0; i < 512; i++){
pte_t pte = pagetable[i];
// 检查pte的有效性
if(pte & PTE_V ){
// this PTE points to a lower-level page table.
uint64 child = PTE2PA(pte);
// 打印树的深度
for(int j = 0; j < level; j++){
if(j==0){
printf("..");//第一个..前面不打印空格
}else{
printf(" ..");
}
}
printf("%d: pte %p pa %p\n",i,pte,child);
// 第三级页表存放的是物理地址,页表中页表项中W位,R位,X位起码有一位会被设置为1。如果是索引页表则这些值是0
if((pte & (PTE_R|PTE_W|PTE_X)) == 0){
_vmprint((pagetable_t)child,level+1);// 还没到第三级,继续递归。
}
}
}
}
A kernel page table per process
前置知识:
原本的xv6系统只有一个内核页表。内核页表直接映射(恒等映射)到物理地址,也就是说内核虚拟地址x映射到物理地址仍然是x。每个进程有单独的用户页表,但只包含该进程用户内存的映射,从虚拟地址0开始。内核页表中不含有这些映射,因此用户地址(虚拟地址)在内核中无效,只能通过copyin(),copyoput()等函数将用户地址转化为物理地址再使用。
关于内核栈:
内核栈页面。每个进程都有自己的内核栈,它将映射到偏高一些的地址,这样xv6在它之下就可以留下一个未映射的保护页(guard page)。保护页的PTE是无效的(也就是说PTE_V没有设置),所以如果内核溢出内核栈就会引发一个异常,内核触发panic。如果没有保护页,栈溢出将会覆盖其他内核内存,引发错误操作。恐慌崩溃(panic crash)是更可取的方案。(注:Guard page不会浪费物理内存,它只是占据了虚拟地址空间的一段靠后的地址,但并不映射到物理地址空间。)
如图中的kstack0,1是每个进程的内核栈。/kernel/proc.c
文件中的procinit
函数中初始化了每个进程的内核栈。在执行系统调用陷入内核之后,这些内核代码所使用的栈并不是原先进程用户空间中的栈,而是一个单独内核空间的栈,这个称作进程内核栈 ,除了系统调用,像进程切换时的上下文也是保存到内核栈中的。
// initialize the proc table at boot time.
void
procinit(void)
{
struct proc *p;
initlock(&pid_lock, "nextpid");
for(p = proc; p < &proc[NPROC]; p++) {
initlock(&p->lock, "proc");
// Allocate a page for the process's kernel stack.
// Map it high in memory, followed by an invalid
// guard page.
char *pa = kalloc();
if(pa == 0<