Overview
- 本 lab 关于页表(page table),涉及到 xv6 的虚拟内存、物理内存、三级页表、内存访问权限等内容
- Speed up system calls:要在内核和用户态之间创建一个共享的只读页,使用户态能够直接读取内核态写入的数据,从而加速系统调用
- Print a page table:要在 exec 中插入一个打印函数,使得 xv6 启动时会打印首个进程的页表信息。我们要实现这个打印函数,将三级页表的所有可用 PTE 均打印出来
- Detect which pages have been accessed:利用 PTE_A 位,监控有哪些 PTE 被访问(read/write)过,将结果返回给用户态
- 用xv6-book上的一张图来展示xv6的页表设计

- 虚拟地址只用了39位
- 页表有2272^{27}227个页表项(PTE)的数组,分三级来排布,每一级有 512 个 PTE
- 每一个 PTE 包含 44 位的物理页号(PPN)和 10 位的 Flags
- 第一、二级的PPN指向下一级页表的 PTE,而第三级的PPN指向物理内存
- 映射时,xv6 先通过 39 位虚拟地址的高 27 位来寻找第三级 PTE(通过三级页表映射),然后将其中的 44 位 PPN 和虚拟地址剩下的 12 位组合成 56 位的物理地址
Speed up system calls (easy)
- 为了保护数据安全,用户态是不能直接读取内核态的数据,而是要通过系统调用。如果创建一个可读 PTE 指向一块内存,该 PTE 是用户态和内核态共享的,那么用户态就可以直接读取这块内核数据,而无需经过复杂的系统调用
- 以 getpid 为例,proc结构是内核态数据,用户态无法直接读取,因此需要通过系统调用 getpid 来读取 pid。
- 所以需要我们创建一个共享 PTE,将虚拟地址 USYSCALL 映射到 pid 的物理地址,这样用户态直接读取 USYSCALL 就可以获取到 pid 了
int
ugetpid(void)
{
struct usyscall *u = (struct usyscall *)USYSCALL;
return u->pid;
}
- 首先,我们 proc 新增一个字段,存放 usyscall 的地址
struct proc
{
struct usyscall *usyscall;
}
- 然后,在 allocproc 时,为其分配一块物理内存,参考已给出的Allocate a trapframe page(进程的陷阱帧的分配)代码
static struct proc*
allocproc(void)
{
if((p->trapframe = (struct trapframe *)kalloc()) == 0)
{
freeproc(p);
release(&p->lock);
return 0;
}
#ifdef LAB_PGTBL
if ((p->usyscall = (struct usyscall*)kalloc()) == 0)
{
freeproc(p);
release(&p->lock);
return 0;
}
p->usyscall->pid = p->pid;
#endif
}
- proc 有一个字段名为 pagetable,用于存放页表的地址。页表的初始化位于函数 proc_pagetable 中,通过 mappages 在页表中注册新的 PTE,参考 TRAPFRAME 的方式,将 USYSCALL 映射到 p->usyscall 中
- 注意:这里的flag 位不仅仅要置位 PTE_R,还要置位 PTE_U,否则用户态无权访问
pagetable_t
proc_pagetable(struct proc *p)
{
pagetable_t pagetable;
if(mappages(pagetable, TRAPFRAME, PGSIZE,
(uint64)(p->trapframe), PTE_R | PTE_W) < 0){
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmfree(pagetable, 0);
return 0;
}
#ifdef LAB_PGTBL
if(mappages(pagetable, USYSCALL, PGSIZE,(uint64)p->usyscall, PTE_R | PTE_U) < 0)
{
uvmunmap(pagetable, USYSCALL, 1, 0);
uvmfree(pagetable, 0);
}
#endif
return pagetable;
}
- 新增完映射后,我们需要在进程 free 时对其解映射,同样参考 TRAPFRAME 的代码
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
#ifdef LAB_PGTBL
uvmunmap(pagetable, USYSCALL, 1, 0);
#endif
uvmfree(pagetable, sz);
}
- 前面分配了空间,用完以后肯定要释放,因此需要在 freeproc 处将其释放,同样参考trapframe的实现
static void
freeproc(struct proc *p)
{
if(p->trapframe)
kfree((void*)p->trapframe);
p->trapframe = 0;
#ifdef LAB_PGTBL
if (p->usyscall)
kfree((void*)p->usyscall);
p->usyscall = 0;
#endif
}
Print a page table (easy)
- 实现一个内核函数 vmprint,其接收一个 pagetable,能够将其中所有的可用 PTE 的信息全部打印出来
- 利用DFS实现(因为就是三级页表,所以直接用三层for循环嵌套也可以实现)
- 在多级页表中,中间层的页表条目通常只设置存在位,而最底层的页表条目会设置存在位和权限位,由此可以作为递归终止判断条件(这在下面的结果中也可以得到印证)
void
vmprint_dfs(pagetable_t pagetable, uint depth)
{
static char* prefix[] = {"","..",".. ..",".. .. .."};
for (int i = 0; i < 512; i++)
{
pte_t pte = pagetable[i];
if (pte & PTE_V)
{
pte_t child = PTE2PA(pte);
printf("%s%d: pte %p pa %p\n", prefix[depth], i, pte, child);
if (child && ((pte & (PTE_R | PTE_W | PTE_X))==0))
vmprint_dfs((pagetable_t)child, depth + 1);
}
}
}
void
vmprint(pagetable_t pagetable)
{
printf("page table %p\n", pagetable);
vmprint_dfs(pagetable, 1);
}
- 在 exec 中当 pid 为1时调用 vmprint
int
exec(char *path, char **argv)
{
if(p->pid == 1)
vmprint(p->pagetable);
}
- make qemu 时即可打印出首进程的页表信息。
xv6 kernel is booting
hart 1 starting
hart 2 starting
page table 0x0000000087f6b000
..0: pte 0x0000000021fd9c01 pa 0x0000000087f67000
.. ..0: pte 0x0000000021fd9801 pa 0x0000000087f66000
.. .. ..0: pte 0x0000000021fda01b pa 0x0000000087f68000
.. .. ..1: pte 0x0000000021fd9417 pa 0x0000000087f65000
.. .. ..2: pte 0x0000000021fd9007 pa 0x0000000087f64000
.. .. ..3: pte 0x0000000021fd8c17 pa 0x0000000087f63000
..255: pte 0x0000000021fda801 pa 0x0000000087f6a000
.. ..511: pte 0x0000000021fda401 pa 0x0000000087f69000
.. .. ..509: pte 0x0000000021fdcc13 pa 0x0000000087f73000
.. .. ..510: pte 0x0000000021fdd007 pa 0x0000000087f74000
.. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000
init: starting sh
$
Detect which pages have been accessed (hard)
- 实现一个系统调用 sys_pgaccess,从一个虚拟地址对应的 PTE 开始,往下搜索一定数量的被访问过的页表,并把结果通过 mask 的方式返回给用户。每当 sys_pgaccess 调用一次,页表被访问标志就要清 0
- 怎么知道哪些页表被访问了?
- 每个 PTE 有个 PTE_A 位,该位被置 1 则说明被访问过。置位操作有硬件完成,无需我们考虑。但是,硬件只能做到置位,无法做到复位。因此每次 sys_pgaccess 时要手动将 PTE_A 复位 0
- 怎么通过虚拟地址依次遍历后续 PTE?
- PTE 是连续的,一个 PTE 大小为 PGSIZE,因此只要将虚拟地址按 PGSIZE 累加即可得到后续的 PTE
walk(p->pagetable, va + i * PGSIZE, 0);
- PTE_A 是第 6 位
- sys_pgaccess,接收三个参数,分别为:起始虚拟地址、 遍历页数目、 用户存储返回结果的地址。因为其是系统调用,故参数的传递需要通过 argaddr、argint 来完成
- 通过不断的 walk 来获取连续的 PTE,然后检查其 PTE_A 位,如果为 1 则记录在 mask 中,随后将 PTE_A 手动清 0。最后,通过 copyout 将结果拷贝给用户即可
int
sys_pgaccess(void)
{
struct proc* p = myproc();
uint64 va;
int num_pages;
uint64 access_result;
uint64 result = 0;
argaddr(0, &va);
argint(1, &num_pages);
argaddr(2, &access_result);
if (num_pages <= 0 || num_pages > 512)
return -1;
for (int i = 0; i < num_pages; i++)
{
pte_t* pte = walk(p->pagetable, va + i * PGSIZE, 0);
if (pte && (*pte & PTE_V) && (*pte & PTE_A))
{
*pte &= ~PTE_A;
result |= (1 << i);
}
}
copyout(p->pagetable, access_result, (char*)&result, sizeof(result));
return 0;
}