【MTI 6.S081 Lab】Page tables
- Speed up system calls (easy)
- 实验任务
- Hints
- 哪些其它的系统调用能通过这个共享页面变得更快,请解释。
- 解决方案
- 分配和释放页面
- 初始化结构
- 实验心得
- Print a page table (easy)
- 实验任务
- Hints
- 根据图3-4从文本中解释vmprint的输出。第0页包含什么?第2页是什么?在用户模式下运行时,进程是否可以读取/写入第1页映射的内存?倒数第三页包含什么?
- 解决方案
- Detect which pages have been accessed (hard)
- 实验任务
- Hint
- 解决方案
- 访问位
- sys_pgaccess()
Speed up system calls (easy)
一些操作系统(如Linux)通过在用户空间和内核之间共享只读区域来加速某些系统调用。这就消除了在执行这些系统调用时对内核交叉的需要。为了帮助您了解如何将映射插入到页面表中,您的第一个任务是为xv6中的getpid()系统调用实现此优化。
实验任务
当一个进程被创建,映射一个只读页在USYSCALL(在memlayout.h中定义的一个虚拟地址)。在这个页的开始位置,存储一个结构struct usyscall
(也被定义在memlayout.h中),初始化它并且存储当前进程的PID。对于这个lab,ugetpid()已经在用户空间端提供,并且将自动使用USYSCALL的映射。当你运行pgtbltest,如果ugetpid测试例子全部通过,你将得到这个部分的全部的分数。
Hints
- 你将在
kernel/proc.c
执行映射proc_pagetable()
- 确定权限位,使得用户空间对这个页面只读
mappages
是一个有用的工具- 不要忘记在
allocproc()
分配和初始化这个页面 - 确保在
freeproc()
释放这个页面
哪些其它的系统调用能通过这个共享页面变得更快,请解释。
答:不需要修改内核维护的信息的那些系统调用都可以通过这个共享页面变得更快。
解决方案
分配和释放页面
// Create a user page table for a given process, with no user memory,
// but with trampoline and trapframe pages.
pagetable_t
proc_pagetable(struct proc *p)
{
pagetable_t pagetable;
// An empty page table.
pagetable = uvmcreate();
if(pagetable == 0)
return 0;
// map the trampoline code (for system call return)
// at the highest user virtual address.
// only the supervisor uses it, on the way
// to/from user space, so not PTE_U.
if(mappages(pagetable, TRAMPOLINE, PGSIZE,
(uint64)trampoline, PTE_R | PTE_X) < 0){
uvmfree(pagetable, 0);
return 0;
}
// map the trapframe page just below the trampoline page, for
// trampoline.S.
if(mappages(pagetable, TRAPFRAME, PGSIZE,
(uint64)(p->trapframe), PTE_R | PTE_W) < 0){
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmfree(pagetable, 0);
return 0;
}
// 映射一个页面,确定许可位, 这个页面用户只要可读即可
if (mappages(pagetable, USYSCALL, PGSIZE, (uint64)(p->usyscall), PTE_U | PTE_R) < 0) { // 这里分配不成功,那么要把前两个分配成功的给取消映射,避免内存泄漏
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmfree(pagetable, 0);
return 0;
}
return pagetable;
}
// Free a process's page table, and free the
// physical memory it refers to.
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);
}
初始化结构
static struct proc*
allocproc(void)
{
...
// 注意,要先在这里分配一个物理页面,才能把这个物理页面映射到虚拟地址上,再往页表添加条目PTE
// 在此处将系统调用优化映射的页面初始化
if((p->usyscall = (uint64)kalloc()) == 0) {
freeproc(p);
release(&p->lock);
return 0;
}
(*(struct usyscall *)p->usyscall).pid = p->pid;
// An empty user page table.
...
return p;
}
实验心得
- 先要分配物理页面,然后再将这个物理页面映射到进程虚拟地址的USYSCALL位置
- 在进程释放页表时,要先取消这个映射。
Print a page table (easy)
为了帮助你可视化RISC-V的页表,也或许可以辅助之后的debug,你的第二个任务是写一个函数用于打印页表的内容。
实验任务
定义一个函数名为vmprint()
。他以pagetable_t
为参数,按照下面的格式打印这个页表。在exec.c中的返回argc之前插入if(p->pid==1)vmprint(p->pagetable),以打印第一个进程的页表。如果你通过了合格等级的pte printout 测试,你将获得lab这一部分的全部分数。
完成后,当你启动xv6,它应该输出如下内容,描述第一个进程刚刚完成exec()初始化时的页表:
第一行显示vmprint
的参数。之后,每个PTE都有一行,包括引用树中更深层次的页面表页面的PTE。" …"代表PTE在树中的深度。每个PTE行在其页面表页面中显示PTE索引、PTE位和从PTE中提取的物理地址。不要打印无效的PTE。在上面的示例中,顶级页面表页面具有条目0和255的映射。条目0的下一级仅映射了索引0,该索引0的底层映射了条目0、1和2。
你的代码可能会发出与上面显示的物理地址不同的物理地址。条目数和虚拟地址应相同。
Hints
- 在
kernel/vm.c
中添加vmprint()
- 使用在
kernel/riscv.h
文件中最后定义的那些宏定义 freewalk
能给你灵感- 在
kernel/defs.h
中定义vmprint
的原型,以便于你能从exec.c
中调用它 - 在printf调用中使用%p可以打印出完整的64位十六进制PTE和地址,如示例所示。
根据图3-4从文本中解释vmprint的输出。第0页包含什么?第2页是什么?在用户模式下运行时,进程是否可以读取/写入第1页映射的内存?倒数第三页包含什么?
答:xv6为39为虚拟地址,高27位用于通过页表查找物理页,每级页表分配9位。最低级的页表查询的结果即为虚拟地址空间中某地址对应物理内存中那一页所在的位置,然后和虚拟地址的第12位组成实际的物理地址,即为进程所需要的数据。vmprint中,
- 第0页的高27为均为0,所以此处存放的是程序指令。
- 后面接着放程序数据,包括初始化全局和静态变量和未初始化全局和静态变量。
- 保护页面,避免栈溢出.。如果用户栈溢出并且进程试图使用栈下方的地址,那么由于映射无效(
PTE_V
为0)硬件将生成一个页面故障异常。当用户栈溢出时,实际的操作系统可能会自动为其分配更多内存。 - 用户栈
- 堆
- 蹦床页面
NOTE :可以看到xv6的进程虚拟地址空间的布局和x86不一样,它的栈紧接着程序数据,然后高地址才是堆。
解决方案
static void
vmprintwalk(pagetable_t pagetable, int level) {
char prefix[10] = " .. .. ..";
prefix[level * 3] = '\0';
for (int i = 0; i < 512; ++i) {
pte_t pte = pagetable[i];
if ((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0) {
uint64 child = PTE2PA(pte);
printf("%s%d: pte %p pa %p\n", prefix, i, pte, child);
vmprintwalk((pagetable_t)child, level+1);
} else if (pte & PTE_V) {
// pte指向的页面 可读 或 可写 或 可执行,说明已经指向了进程自己的虚拟地址了,此时应该会是第三级页表了
printf("%s%d: pte %p pa %p\n", prefix, i, pte, PTE2PA(pte));
}
}
}
void
vmprint(pagetable_t pagetable) {
printf("page table %p\n", pagetable);
vmprintwalk(pagetable, 1);
}
Detect which pages have been accessed (hard)
一些垃圾收集器(自动内存管理的一种形式)可以从有关哪些页面已被访问(读取或写入)的信息中受益。在Lab的这一部分中,您将向xv6添加一个新功能,该功能通过检查RISC-V页面表中的访问位来检测并向用户空间报告这些信息。RISC-V硬件页面walker在PTE中标记这些位,每当它解决TLB未命中时。
实验任务
您的工作是实现pgaccess()
,这是一个报告哪些页面已被访问的系统调用。系统调用需要三个参数。第一个的参数为第一个用户页面的起始虚拟地址。第二个参数为需要检查的页数。第三个参数,它将用户地址带到缓冲区,将结果存储到位掩码(一种每页使用一位的数据结构,其中第一页对应于最低有效位)。如果在运行pgtbltest时pgaccess测试用例通过,您将获得实验室这一部分的全部学分。
Hint
- 阅读
user/pgtlbtest.c
中的函数pgaccess_test()
去了解pgaccess
是如何被使用的 kernel/sysproc.c
中开始执行sys_pgaccess()
- 你需要使用
argaddr()
和argint()
解析参数 - 对于输出位掩码,可以更容易地在内核中存储一个临时缓冲区,并在填充正确的位后将其复制到用户(通过copiout())。
- 可以对可以扫描的页数设置上限。
- 在
kernel/vm.c
中的walk()
对于找到正确的PTEs是非常有用的 - 你需要在
kernel/riscv.h
中定义访问位PEE_A。 - 确保在检查PTE_A后清除它。否则,确定一个页面在上一次pgaccess()后是否被访问是不可能的,因为它一直被置位了。
vmprint()
在调试页面表时可能会派上用场。
解决方案
访问位
#define PTE_A (1L << 6) // access bit, 访问位
sys_pgaccess()
int
sys_pgaccess(void)
{
// lab pgtbl: your code here.
uint64 base;
int len;
uint64 mask = 0;
uint64 mask_addr;
argaddr(0, &base);
if(base == 0) {
return -1; // 解析参数失败
}
// base可能不是PGSIZE的整数倍,因为用户malloc时只需要8或者16字节对齐即可
base = PGROUNDDOWN(base); // 向下舍入,得到buf中第一个字节所在的页面
argint(1, &len);
if (len < 0 || len > 64) { // 暂时只能处理64个页面的检测
return -1;
}
argaddr(2, &mask_addr);
struct proc *p = myproc();
for (int i = 0; i < len; ++i) {
pte_t *pte = walk(p->pagetable, base + i * PGSIZE, 0); // alloc为0说明不要为页表分配,其实如果此时要分配,实际上就没有访问过
if (pte == 0 || (*pte & PTE_A) == 0) {
// 没有访问过
continue;
} else {
mask |= (1L << i); // 第i个页面访问过
*pte = (*pte & ~PTE_A); // 清除PTE_A位
}
}
// 将mask写入传入的地址中
copyout(p->pagetable, mask_addr, (char *)&mask, sizeof(mask));
return 0;
}