Appearance
page tables
补充内容:Chapter 3: Page Tables - 知乎 (zhihu.com)
开启新实验
git fetch
git checkout pgtbl
make cleanSpeed up system calls
为了加速系统调用,很多操作系统都会在用户空间内开辟一些只读的虚拟内存,内核会把一些数据分享在这里。这样就可以减少来回在用户态和内核态中切换的操作。
任务描述:为系统调用getpid()实现这样的加速。
这是这个任务中需要使用到的重要函数mappages(),它在kernel/vm.c中被实现。

它实现将pagetable的从va开始大小为size的空间映射到物理地址pa,并设置标志位为perm。
- 在
kernel/proc.h中给struct proc添加成员usyscall
struct usyscall *usyscall;- 在
kernel/proc.c的proc_pagetable()函数中实现,当进程创建时在USYSCALL映射一个只读的页
proc_pagetable()会在创建新进程时被调用,符合我们的要求。
观察一下proc_pagetable()是如何使用mappages()来创建 trampoline 和 trapframe 页的:

如果映射失败,需要uvmunmap()取消之前映射成功的映射,并uvmfree()释放内存,然后返回。
在kernel/riscv.h中有标志位的定义:

对于本任务来说,标志位应该是PTE_R | PTE_U,代表允许读,和允许用户访问。
实现代码如下:
if(mappages(pagetable, USYSCALL, PGSIZE, (uint64)(p->usyscall), PTE_R | PTE_U) < 0){
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAM, 1, 0);
uvmfree(pagetable, 0);
return 0;
}我们已经成功创建了从虚拟内存到物理的映射,但是并没有在创建进程的时候申请物理内存(如果没申请物理内存,就会把一个虚拟内存映射到空指针上)。
- 接下来在
kernel/proc.c的allocproc()函数中为USYSCALL申请物理内存,并初始化p->usyscall。
可以照抄参考allocproc()中给 trapframe 分配物理内存的过程:

if((p->usyscall = (struct usyscall *)kalloc()) == 0){
freeproc(p->usyscall);
release(&p->lock);
return 0;
}
p->usyscall->pid = p->pid;- 还需要修改
kernel/proc.c中的freeproc(),同样参考对trapframe的处理即可。
if(p->usyscall)
kfree((void*)p->usyscall);
p->usyscall = 0;- 虽然Hint里没说,但是还需要修改
kernel/proc.c中的proc_freepagetable()函数,取消USYSCALL的映射。(不然会panic freewalk leaf)
uvmunmap(pagetable, USYSCALL, 1, 0);用户态的函数就不需要我们自己写了,根据实验提示,已经在 user\ulib.c 中实现了。
Print a page table
任务描述:如题。
- 在
kernel/vm.c添加vmprint()函数,接收一个pagetable_t参数。
因为 xv6 的页表是三级的,所以是一个树的结构,那么本质上就是需要写一个dfs打印树的函数。还需要层数的信息所以多写了一个函数比较方便。
可以参考同样在kernel/vm.c中的freewalk()函数的实现。

int vmprint_dfs(pagetable_t pagetable, int dep)
{
for(int i = 0; i < 512; i++){
pte_t pte = pagetable[i];
if(pte & PTE_V){ // 判断是否存在
uint64 child = PTE2PA(pte);
for(int j = 0; j < dep; j++)
printf(".. ");
printf("%d: pte %p pa %p\n", i, pte, child);
if(dep < 3) vmprint_dfs((pagetable_t)child, dep+1);
}
}
return 0;
}
int vmprint(pagetable_t pagetable)
{
printf("page table %p\n", pagetable);
vmprint_dfs(pagetable, 1);
return 0;
}- 在
kernel/exec.c中return argc;之前插入以下代码:
if(p->pid == 1) vmprint(p->pagetable, 0);因为 init 是系统创建的第一个进程,所以 init 的 pid 是 1,那么在创建 init 时,就会打印这个页表。
Detecting which pages have been accessed
任务描述:实现一个 pgaccess() 函数,这个函数的申明为:int pgaccess(void *base, int len, void *mask);。这个函数的主要作用就是检测从上次调用这个函数开始,页表是否被访问过。其中 base 参数是要检测的第一个页表,len 从这个页表开始,要检测多少个页表,而我们需要把每个页表的访问情况写到 mask 上,如果当前页表被访问,那么 mask 中对应的位应该是 1。
- 需要自行在
kernel/riscv.h中定义一下PTE_A

查阅资料后得知,记录是否访问的位置是第六位。
#define PTE_A (1L << 6)- 在
kernel/sysproc.c实现sys_pgaccess()
需要使用到kernel/vm.c中的walk()函数,对于一个给定的页表和虚拟地址,walk() 函数会返回对应这个虚拟地址的叶子 PTE。

int sys_pgaccess(void)
{
// 接收参数
int len;
uint base, mask_addr;
if(argaddr(0, &base) < 0) return -1;
if(argint(1, &len) < 0) return -1;
if(argaddr(2, &mask_addr) < 0) return -1;
// 设置上限,因为更长的话mask位数不够
if(len > 32) return -1;
int mask = 0;
pagetable_t pagetable = myproc()->pagetable;
pte_t *pte = walk(pagetable, base, 0);
for(int i = 0; i < len; i++){
// 如果页表存在且访问过
if((pte[i] & PTE_A) && (pte[i] & PTE_V)){
mask |= (1 << i); // mask置位
pte[i] ^= PTE_A; // PTE_A复位
}
}
// 将mask写入指定位置
if(copyout(pagetable, mask_addr, &mask, sizeof(mask)) < 0)
return -1;
return 0;
}受不了了
walk()为啥没在kernel/defs.h里声明!!!
The End

(这个最后一个测试是在干啥???是不是要等久一点我直接给终止了。。)
一个主要考察页表的Lab。