MIT6.S081 Lab 3 Page tables
Lab 3 Page tables
Speed up system calls
一些操作系统(如 Linux)通过在用户空间和内核之间共享只读区域中的数据来加快某些系统调用的速度。这样,在执行这些系统调用时就不需要进出内核了。为了帮助你学习如何在页表中插入映射,你的第一个任务就是在 xv6 中的 getpid() 系统调用中实现这一优化。
每当创建一个进程时,在 USYSCALL(memlayout.h 中定义的一个 VA)处映射一个只读页面。在该页面的开头,存储一个 struct usyscall(也在 memlayout.h 中定义),并将其初始化以存储当前进程的 PID。在本实验中,ugetpid() 将在用户空间侧提供,并自动使用 USYSCALL 映射。在运行 pgtbltest 时,如果 ugetpid 测试用例通过,本部分实验将获得满分。
提示
- 您可以在 kernel/proc.c 中的 proc_pagetable() 中实现映射
- 选择只允许用户空间读取页面的权限位
- 您可能会发现 mappages() 是一个有用的工具。
- 别忘了在 allocproc() 中分配和初始化页面
- 确保在 freeproc() 中释放页面。
使用该共享页面,还有哪些 xv6 系统调用可以变得更快?请解释如何实现。
源码阅读和思路
proc_pagetable为新进程建立TRAMPOLINE和TRAPFRAME映射,可以将USYSCALL的映射建立在这之后
#define TRAPFRAME (TRAMPOLINE - PGSIZE)
#ifdef LAB_PGTBL
#define USYSCALL (TRAPFRAME - PGSIZE)
struct usyscall {
int pid; // Process ID
};
#endif
/*
MAXVA
| PGSIZE
TRAMPOLINE
| PGSIZE
TRAPFRAME
| PGSIZE
USYSCALL
*/
建立映射需使用mappages函数
walk函数通过三级树页表结构查找pte
PA2PTE通过((虚拟地址<<12) >> 10)获得PPN,PTE2PA反之
allocproc 分配一页,在建立用户页表函数中proc_paget 将struct usyscall的pid存入物理地址,建立只读映射,这样usrspace中就可以通过这个映射读取物理地址中的数据。确保进程创建时分配一个页并在进程结束时销毁
完成
proc.h
struct usyscall *sharedData; // user/kernel shared page
proc.c
allocproc中为USYSCALL分配一页,填入pid
// static struct proc* allocproc(void)
if((p->sharedData = (struct usyscall *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}
p->sharedData->pid = p->pid; //store pid
freeproc中释放USYSCALL的一页
// static void freeproc(struct proc *p)
if(p->sharedData) // free user/kernel shared page
kfree((void*)p->sharedData);
p->sharedData = 0;
pagetable_t proc_pagetable建立USYSCALL的映射
// pagetable_t proc_pagetable(struct proc *p)
if(mappages(pagetable, USYSCALL, PGSIZE,
(uint64)(p->sharedData), PTE_R | PTE_U) < 0){
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmfree(pagetable, 0);
return 0;
}
proc_freepagetable()中取消USYSCALL的映射
// proc_freepagetable()
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);
}
使用该共享页面,还有哪些 xv6 系统调用可以变得更快?请解释如何实现。
共享页面不需要内核向用户空间传输调用结果,因此查看内核才能查看的固定信息的系统调用可以变得更快,内核可以提前将信息写入只读页面
Print a page table (easy)
第二个任务是写一个打印页表内容的函数
定义一个名为 vmprint() 的函数。它应该接受一个 pagetable_t 参数,并按照下面描述的格式打印该分页表。在 exec.c 中返回 argc 之前插入 if(p->pid==1) vmprint(p->pagetable),以打印第一个进程的页表。如果通过了 make grade的 pte printout测试,这部分实验将获得满分。
第一行显示 vmprint 的参数。之后,每一个 PTE 都有一行,包括指向树中更深页表页的 PTE。每行 PTE 都以"... "缩进,表示其在树中的深度。每行显示页表页中的 PTE 索引、PTE 位以及从 PTE 中提取的物理地址。不要打印无效的 PTE。在上例中,顶层页表页有条目 0 和 255 的映射。下一级的条目 0 只映射了索引 0,而索引 0 的下一级映射了条目 0、1 和 2。
您的代码可能会产生与上述不同的物理地址。条目数和虚拟地址应该相同。
提示
- 可以在 kernel/vm.c 中加入 vmprint()
- 使用 kernel/riscv.h 文件末尾的宏
- freewalk可能会给人启发。
- 在 kernel/defs.h 中定义 vmprint 的原型,这样就可以在 exec.c 中调用 vmprint()
- 在 printf 调用中使用 %p 来打印完整的 64 位十六进制 PTE 和地址,如示例所示
源码阅读和思路
// Recursively free page-table pages.
// All leaf mappings must already have been removed.
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){
panic("freewalk: leaf");
}
}
kfree((void*)pagetable);
}
如果 PTE 包含 PTE_V 标志,并且同时不包含 PTE_R | PTE_W | PTE_X 这些标志,那么说明这个 PTE 指向的是一个下一级的页表,而不是叶子页。这样只能判断是否抵达叶子页,无法判断处于第几级,如果知道虚拟地址的话还能通过判断pte是第几位来确定级数。所以还是需要一个变量level
思路:深度遍历三级页表,打印有效页目录
完成
static char* depth[3] = {".. .. ..", ".. ..", ".. "};
void deepSearch(pagetable_t pagetable, int level){
for(int i = 0; i < 512; i++){
pte_t *pte = &pagetable[i];
if(*pte & PTE_V) {
printf("%s%d: pte %p pa %p\n", depth[level], i, *pte, (pte_t)PTE2PA(*pte));
if(level > 0)
deepSearch((pagetable_t)PTE2PA(*pte), level-1); // search deeper level page table
}
}
return;
}
void vmprint(pagetable_t pagetable){
printf("page table %p\n", *pagetable);
deepSearch(pagetable, 2);
return;
}
结果
思考题
根据文中图 3-4 解释 vmprint 的输出。第 0 页包含什么内容?第 2 页包含什么?以用户模式运行时,进程能否读/写第 1 页映射的内存?倒数第三页包含什么内容?
Detecting which pages have been accessed
一些垃圾回收器(一种自动内存管理形式)可以从哪些页面已被访问(读取或写入)的信息中获益。在这部分实验中,您将为 xv6 添加一项新功能,通过检查 RISC-V 页表中的访问位来检测并向用户空间报告这些信息。每当 RISC-V 硬件走页器解决 TLB 未命中问题时,都会在 PTE 中标记这些位。
你的任务是实现 pgaccess(),这是一个系统调用,用于报告哪些页面已被访问。系统调用需要三个参数。首先,它需要第一个要检查的用户页面的起始虚拟地址。其次,它接受要检查的页面数。最后,它需要一个缓冲区的用户地址,以便将结果存储到位掩码(一种数据结构,每页使用一位,其中第一页对应的是最小有效位)中。如果在运行 pgtbltest 时通过了 pgaccess 测试用例,这部分实验将获得满分。
提示
- 在 kernel/sysproc.c 中实现 sys_pgaccess()
- 你需要使用 argaddr() 和 argint() 来解析参数
- 对于输出位掩码,在内核中存储一个临时缓冲区,在填入正确的位后通过 copyout()将其复制给用户
- 设置可扫描页数的上限也是可以的
- kernel/vm.c 中的 walk() 对于找到正确的 PTE 非常有用。
- 您需要在 kernel/riscv.h 中定义访问位 PTE_A。请查阅 RISC-V 手册确定其值。
- 在检查 PTE_A 是否被设置后,请务必将其清除。否则,将无法确定上次调用 pgaccess() 后是否访问过页面(即该位将永远被设置)。
- vmprint() 可能会在调试页表时派上用场。
手册阅读
4.3.1 Addressing and Memory Protection
Each leaf PTE contains an accessed (A) and dirty (D) bit. The A bit indicates the virtual page has been read, written, or fetched from since the last time the A bit was cleared. The D bit indicates the virtual page has been written since the last time the D bit was cleared.
完成
使用walk访问每页对应的pte,查看PTE_A标志,发现被修改则设置掩码并再次清除PTE_A,最后使用copy将结果拷贝到用户空间
uint64
sys_pgaccess(void)
{
uint64 va;
int size;
int dest;
if(argaddr(0, &va) < 0) return -1;
if(argint(1, &size) < 0) return -1;
if(argint(2, &dest) < 0) return -1;
if(size > 64 || size < 1) return -1;
uint64 bitmask = 0;
pde_t *pte;
pagetable_t pagetable = myproc()->pagetable;
for(int i = 0; i < size; i++){
pte = walk(pagetable, (uint64)va + (uint64)(PGSIZE*i), 0);
if(*pte != 0 && (*pte & PTE_A)){
bitmask |= (1L << i); // store in bitmask
*pte &= ~PTE_A; // clear PTE_A
}
}
copyout(pagetable, dest, (char *)&bitmask, sizeof(int));
return 0;
}
一点小问题:
由于walk函数是在vm.c的内部使用的,所以walk并没有声明在def.h中
全部测试同通过

浙公网安备 33010602011771号