Lab: page tables
Speed up system calls
一些操作系统(例如Linux)通过在用户空间和内核之间共享只读区域的数据来加速某些系统调用。这样,在执行这些系统调用时,就不需要内核交叉了。为了帮助你学习如何在页表中插入映射,你的第一个任务是为xv6中的getpid()系统调用实现这种优化。
当每个进程被创建时,在USYSCALL(在memlayout.h中定义的VA)映射一个只读页。在这个页面的开始,存储一个结构usyscall(也定义在memlayout.h中),并初始化它以存储当前进程的PID。对于这个实验室,在用户空间方面已经提供了ugid(),并将自动使用USYSCALL的映射。如果在运行pgtbltest时,ugetpid测试用例通过,你将获得该部分实验的全部学分。
思路:创建进程时,直接把进程的pid存入共享内存中。在内核和用户程序之间创建一个共享的只读页,这样内核往这个页里写入数据的时候,用户程序就可以不经复杂的系统调用直接读取它了。
根据提示可以在 kernel/proc.c 这个文件中的 proc_pagetable() 中调用 mappages() 创建新的一页映射。可以看到程序中有两个mappages,先后映射的是TRAMPOLINE、TRAPFRAME,显然对USYSMAP的映射就在这里进行,问题是映射到哪。
所以还要阅读
mappages、uvmunmap、uvmfree的实现。
可以发现,如果当前这一页没有映射成功,我们需要把之前成功映射的 uvmunmap() 了。并且把映射失败的这一页 uvmfree()。
这是因为,如果想要使用 uvmunmap(),必须要确保我们 unmap 的页是存在的,如果不存在就会崩溃,所以,因为我们没有成功映射当前页,就只能 uvmfree() 去释放内存,而不是取消映射。
void uvmfree(pagetable_t pagetable, uint64 sz)
{
if(sz > 0)
uvmunmap(pagetable, 0, PGROUNDUP(sz)/PGSIZE, 1);
freewalk(pagetable);
}
void freewalk(pagetable_t pagetable)
{
// there are 2^9 = 512 PTEs in a page table.
for(int i = 0; i < 512; i++){
pte_t pte = pagetable[i];
if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
// this PTE points to a lower-level page table.
uint64 child = PTE2PA(pte);
freewalk((pagetable_t)child);
pagetable[i] = 0;
} else if(pte & PTE_V){ //PTE_V=1,映射没取消会panic
panic("freewalk: leaf");
}
}
kfree((void*)pagetable);
}
可以发现如果sz=0只调用freewalk,去释放一整个页表的内存。包括之前所有映射过的页。调用 freewalk() 时,我们必须确保映射是已经取消了的,所以我们会先调用 uvmunmap()。因此这个 USYSCALL(共享页) 的位置在 trampoline 和 trapframe 的下面:
if(mappages(pagetable, USYSCALL, PGSIZE, (uint64)(p->usyscall), PTE_R | PTE_U) < 0){
// 映射完成后,我们访问 USYSCALL 开始的页,就会访问到 p->usyscall
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmfree(pagetable, 0);
return 0;
}
//记得在proc.h中声明
struct usyscall *usyscall;
在看一下allocproc状态为UNUSED时分配内存,然后给进程表p赋各种值。
注意到
// Allocate a trapframe page.
if((p->trapframe = (struct trapframe *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}
显然p->trapframe是在这初始化,所以我们可以仿照trapframe的操作,在struct proc中添加一个参数struct usyscall *usyspage,然后用kalloc()分配一页内存,地址指向usyspage,并把该进程的pid存到页表中。
if((p->usyscall = (struct usyscall *)kalloc()) == 0)
{
freeproc(p);
release(&p->lock);
return 0;
}
p->usyscall->pid = p->pid;
仿照freeproc里对trapframe里的操作来释放usyspage:
if(p->usyscall)
kfree((void*)p->usyscall);
p->usyscall = 0;
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmunmap(pagetable, USYSCALL, 1, 0); //新加
uvmfree(pagetable, sz);
}
Print a page table
定义一个叫做vmprint()的函数。它应该接受一个pagetable_t参数,打印该pagetable。在exec.c中插入if(p->pid==1) vmprint(p->pagetable),在返回argc之前,以打印第一个进程的页表。在创建
init进程时,调用这个函数打印页表。
1.xv6使用的是三级页表。xv6访问页的时候,首先从指向第一级页表的寄存器satp开始(类似的东西在x86架构里叫CR3寄存器),使用虚拟地址的30~38位在页表里定位一项;如果该项的PTE_V这一位是1,则此页表项是有效的,可以根据此页表项内的地址访问下一级页表。依次类推,直到搜索到第三级,获取物理地址。由此可见,三级页表其实是一棵深度为3的树,我们可以使用BFS搜索来遍历这棵树。
2.使用BFS,遍历每个节点上的所有叶子(也就是页表项)。如果叶子的PTE_V为0,直接跳过;如果为1,先用宏PTE2PA把表项转换成物理地址,再递归调用这个地址。
3.使用一个计数变量dep来记录递归深度,初始必须置为0,由于树的深度最大只有3,则count==等于3时直接返回。不过看这门课的录像,是实现了一个只传入页表物理地址的vmprint。我猜应该是声明了一个全局变量,进入函数时变量+1,退出时将这个变量-1。
根据提示这个函数应该写在vm.c中,仿照freewalk写。
//根据提示在exec.c中插入if(p->pid==1) vmprint(p->pagetable)
proc_freepagetable(oldpagetable, oldsz);
if (p->pid == 1)
vmprint(p->pagetable, 0);
return argc; // this ends up in a0, the first argument to main(argc, argv)
bad:
//在defs.h中声明vmprint,在vm.c中定义
void vmprint(pagetable_t pagetable, uint dep)
{
if(dep == 0)
printf("page table %p\n", pagetable);
for(int i=0; i<512; ++i)
{
pte_t pte = pagetable[i];
// 如果叶子的PTE_V为0,直接跳过;
//如果为1,先用宏PTE2PA把表项转换成物理地址,再递归调用这个地址。
if(pte & PTE_V)
{
for(int j=0; j<dep; ++j)
printf(".. ");
uint64 child = PTE2PA(pte);
printf("..%d: pte %p pa %p\n", i, pte, child);
if (dep < 2) // 如果层数等于 2 就不需要继续递归了,因为这是叶子节点
vmprint((pagetable_t)child, dep+1);
}
}
}
Detecting which pages have been accessed
实现pgaccess(),报告哪些页面被系统调用过,从一个用户页表地址开始,搜索所有被访问过的页并返回一个bitmap来显示这些页是否被访问过。比如说,如果第二个页被访问过了,bitmap里从右往左数第二个bit就是1。需要3个参数:
- 接收要检查用户页面的起始虚拟指针。
- 检查页面的数量。
- 一个缓存区地址,用于将结果存储到bitmask
先定义在kernel/riscv.h中PTE_A
#define PTE_A (1L << 6) // 左移六位是看上图决定的
在defs.h中声明walk
int
sys_pgaccess(void)
{
// lab pgtbl: your code here.
pagetable_t u_pt = myproc()->pagetable;
uint64 fir_addr, mask_addr;
int ck_size;
int mask=0;
// fir_addr,ck_siz 和 mask_addr 分别对应函数申明中的三个参数。
if(argaddr(0, &fir_addr)<0)
return -1;
if(argint(1, &ck_size)<0)
return -1;
if(argaddr(2, &mask_addr)<0)
return -1;
if(ck_size > 32)
return -1;
pte_t* fir_pte = walk(u_pt, fir_addr, 0);
for(int i=0; i<ck_size; ++i)
{
if((fir_pte[i]&PTE_A) && (fir_pte[i] & PTE_V))
{
mask |= (1<<i); // 设置PTE
fir_pte[i] ^= PTE_A; // 清空PTE_A
}
}
copyout(u_pt, (uint64)mask_addr, (char* )&mask, sizeof(uint));
return 0;
}
创建answers-pgtbl.txt填入1,2的测试结果。