xv6 Lab (pgtbl)
Nowherechan 学徒

读完教材的页表章节后再开始写代码,要比直接上手开始写代码然后边写边学习轻松很多。最重要的就是理解:内核态下独有一份页表而用户态下每个用户进程都各自有一个页表,以及这两种页表的结构。

总的来说,一直听到传闻说 Page Tables 这章的实验非常难,但是一遍做下来感觉也没什么很难的地方,理解了虚拟页面管理以及内核与进程的虚拟内存映射后,基本上照着 hint 的提示一步一步做就能够完成,代码量也不是很多。相比于接下来的 traps alarm 实验,当真是小巫见大巫。

Speed up system calls

这个实验要求实现一个伪系统调用,即 ugetpid。大致思想就是进程创建时,多分配一个页,绑到页表的特定项上(这样它的虚拟地址就是确定的),在页里面写进该进程的 pid;当调用 ugetpid 时,会直接读这个页里面的数据,而不会进入到内核态,这样就避免了 kernel crossings 从而减小了开销。

首先就是修改 kernel/proc.h 中的 struct proc,加一个 usyscall 项,因为这里已经设计出了 usyscall 这个结构体。
之后在进程创建的时候,多分配一个页;观察 ugetpid 函数,发现是读 USYSCALL 所标识的地址,因此我们需要将多分配的页绑定到这个位置(通过 memlayout.h 文件可以知道这个位置其实就在 unused 顶部)。
这里要注意,当某个页面分配失败的时候,会认为进程创建失败,那么就得 free 掉之前 alloc 的页(trapoline 和 trapframe)。
代码

就是打印出页表的内容,包括页表项的表项以及页表项所对应的物理地址。

大致思路就是模拟 freewalk 的操作,只是在往下递归的时候把页表项打印出来。

每个页表项都用 64 位来存,转化为下一级页表或者最终物理地址,可以用宏 PTE2PA 来完成,查看细节,大致是先右移 10 位再左移 12 位。那难道每个物理地址低 12 位都是 0 吗?最终的物理地址当然不用说,因为要拼接低 12 位的页内偏移,至于二级页表与三级页表,可作如下解释:
Sv39 Risc-V 的页表设计为 9+9+9+12,即三级页表,以及 12 位的页内偏移。每一级页表都有 512 个页表项,每个页表项为 64 位,也就是 8 个 byte,因此每个页表占据 512*8 即 4kB,刚刚好 1 个页,因此对齐,同时也方便页面分配。
9 以及 Sv39 这两个数字大约是为了迎合这个目的而设计的。

在搜索全部页表项的过程中,需要不断判断 PTE_V 的标记来判断是否 valid,如果标记为 0,当然不必输出,也不必在该页表项上继续往下递归。
代码

Detect which pages have been accessed

写一个系统调用,pgaccess(virtual_addr, n, &bitmask),以虚拟地址为起始,将往后的 n 个页的 accessed 信息以 bitmask 的形式存到某个地方。并且清除访问信息,即 clear 掉 PTE 上的 PTE_A 标志位。

我一开始以为是这个系统调用在收集 accessed 信息的时候会“不知道为什么”使得页表项上的 PTE_A 标志位变成 1,因此我需要访问后将它们清除掉。
我怎么也想不明白为什么在内核态、用着内核页表的情况下为什么会影响到用户页表的页表项,因为这里的 vmprint 以及 pgaccess 所做的仅仅是模拟虚实地址转换,仅仅将页表项当作数据来访问,真正被 access 的只有内核页表的页表项。
然而后来发现是我理解错题目意思了,清除掉 PTE_A 位仅仅是 pgaccess 所需要完成的任务之一,但做这件事情并非因为 pgaccess 对其进行了什么影响或者干扰。
一切的源头都是理解问题,原谅我糟糕的英语水平。

能在内核中做这样的一些事情,例如 walk,vmprint,pgaccess 等,都是因为内核页表中对物理内存的直接映射。想来这是理解其虚拟内存管理的关键。
代码

 Comments