Speed up system calls (easy)
要求
一些操作系统(比如Linux)通过在用户与内核之间只读的一块区域共享数据来加速系统调用。这能消减执行这些系统调用时用户态与内核态切换的开销。为了帮助你们理解如何在页表中插入映射,你的第一个任务是为getpid实现这种优化。
每当一个进程被创建时,在USYSCALL(在memlayout.h中定义)位置建立一个只读内存页的映射。在这页开始,存储一个struct usyscall,并初始化该结构体以保存当前进程ID。实验中的ugetpid()已在用户空间提供且会自动使用USYSCALL映射。
提示:
- 你可以在
kernel/proc.c的proc_pagetable()中实现映射 - 选择合适的权限置位以满足用户只读此页
mappages()是有用的工具- 切记在
allocproc()中开辟内存页的空间并初始化 - 确保在
freeproc()中释放该内存页
问题:
哪个(哪些)系统调用能通过这个共享内存页改善性能?如何实现?
实现
步骤:
在
struct proc中添加struct usyscall指针字段struct proc { ... // 其他字段不变 struct usyscall* usyscall; // 添加`struct usyscall`指针字段 };在
proc_pagetable()中添加usyscall物理地址到虚拟地址的映射:pagetable_t proc_pagetable(struct proc *p) { ... // 这一段是源码目的是添加p->trapframe物理地址到 // 虚拟地址TRAPFRAME的映射,为我们提供了很好的参考 if(mappages(pagetable, TRAPFRAME, PGSIZE, (uint64)(p->trapframe), PTE_R | PTE_W) < 0){ uvmunmap(pagetable, TRAMPOLINE, 1, 0); uvmfree(pagetable, 0); return 0; } // 参考上边,添加usyscall内存页的映射 // 权限要求用户只读,因此置位PTE_R | PTE_U if (mappages(pagetable, USYSCALL, PGSIZE, (uint64)(p->usyscall), PTE_R | PTE_U) < 0) { uvmunmap(pagetable, TRAPFRAME, 1, 0); uvmunmap(pagetable, TRAMPOLINE, 1, 0); uvmfree(pagetable, 0); } return pagetable; }同样,建立映射后还需要有对应的位置解除映射(在
free_pagetable()中)void proc_freepagetable(pagetable_t pagetable, uint64 sz) { uvmunmap(pagetable, USYSCALL, 1, 0); // 参考下边添加USYSCALL解除映射命令 uvmunmap(pagetable, TRAMPOLINE, 1, 0); uvmunmap(pagetable, TRAPFRAME, 1, 0); uvmfree(pagetable, sz); }在
allocproc()中开辟空间并初始化static struct proc* allocproc(void) { ... // Allocate a trapframe page. if((p->trapframe = (struct trapframe *)kalloc()) == 0) { freeproc(p); release(&p->lock); return 0; } // 参考上边例子开辟usyscall内存页并初始化 if ((p->usyscall = (struct usyscall*) kalloc()) == 0) { freeproc(p); release(&p->lock); return 0; } p->usyscall->pid = p->pid; ... }在
freeproc()中释放内存页static void freeproc(struct proc *p) { if(p->trapframe) kfree((void*)p->trapframe); p->trapframe = 0; // 依旧参考上边 if (p->usyscall) kfree((void*)p->usyscall); p->usyscall = 0; ... }
结果
运行结果:
pgacess是要在下边实验中实现的功能,所以还不能通过测试结果:
Print a page table (easy)
要求
为使RISC-V页表可视化且可能在后续实验中提供帮助,你的第二个任务是实现打印页表内容的功能
定义函数vmprint(),读取一个pagetable_t参数,并按照下列格式打印页表信息。在exec.c的return argc命令前插入if(p->pid==1) vmprint(p->pagetable)命令来打印第一个进程的页表。
格式:
page table 0x0000000087f6b000
..0: pte 0x0000000021fd9c01 pa 0x0000000087f67000
.. ..0: pte 0x0000000021fd9801 pa 0x0000000087f66000
.. .. ..0: pte 0x0000000021fda01b pa 0x0000000087f68000
.. .. ..1: pte 0x0000000021fd9417 pa 0x0000000087f65000
.. .. ..2: pte 0x0000000021fd9007 pa 0x0000000087f64000
.. .. ..3: pte 0x0000000021fd8c17 pa 0x0000000087f63000
..255: pte 0x0000000021fda801 pa 0x0000000087f6a000
.. ..511: pte 0x0000000021fda401 pa 0x0000000087f69000
.. .. ..509: pte 0x0000000021fdcc13 pa 0x0000000087f73000
.. .. ..510: pte 0x0000000021fdd007 pa 0x0000000087f74000
.. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000
init: starting sh提示:
- 你可在
kernel/vm.c中实现vmprint() - 使用在
kernel/riscv.h最后定义的宏 - 函数
freewalk具有参考价值 - 在
defs.h中声明vmprint - 在printf中使用
%p打印64位PTE和地址
实现
阅读freewalk源码
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);
}可得到信息:
- 一个页表有512个PTE(页表项)
- 通过
pte & PTE_V判断一个PTE是否有效 - 通过
(pte & (PTE_R|PTE_W|PTE_X)) == 0判断一个PTE是否指向下一个页表 PTE2PA这个宏可帮助我们计算PTE的物理地址
vmprint实现思路:
- 除第一行外,可通过递归打印PTE及其物理地址
- 只打印有效PTE,通过
pte & PTE_V判断 - 递归终止条件:只对指向下个页表的PTE做递归,通过
(pte & (PTE_R|PTE_W|PTE_X)) == 0判断
由此,可借助printPTE实现vmprint:
void printPTE(pagetable_t pt, int depth) {
int i, j;
static char* dots = " ..";
for (i = 0; i < 512; ++i) {
if (pt[i] & PTE_V) { // 若PTE可用则打印
uint64 pa = PTE2PA(pt[i]);
for (j = 0; j < depth; ++j) printf(dots);
printf("%d: pte %p pa %p\n", i, pt[i], pa);
if ((pt[i] & (PTE_R|PTE_W|PTE_X)) == 0) {
// 若PTE不可读不可写不可知执行说明指向下一个页表
// 因此需要递归打印,深度+1
printPTE((pagetable_t)pa, depth + 1);
}
}
}
}
void vmprint(pagetable_t pt) {
printf("page table %p\n", pt);
printPTE(pt, 1);
}结果
运行结果:
测试结果:
Detect which pages have been accessed (hard)
要求
为xv6实现一个新功能:通过检查 RISC-V 页表中的访问位,检测并报告用户空间访问了哪些页。
实现系统调用pgacess,报告访问过哪些页面。该系统调用需要三个参数:(1)第一个用户需要检测的页面的起始虚拟地址;(2)需要检测的页面数量;(3)一个指向缓存的用户地址,用以储存位掩码结果。提示:
- 在
user/pgtlbtest.c中了解如何使用pgaccess。 - 在
kernel/sysproc.c中实现sys_pgacess,可用argaddr()或argint()传递参数 - 对于位掩码,可保存在内核临时缓存区,在获取正确的位掩码之后通过
copyout()拷贝给用户 - 可设置扫描页面数的上限
kernel/vm.c中的walk()有助于找到正确的PTEs(页表项)- 在
kernel/riscv.h定义访问位PTE_A,参考手册确定其值 - 在检查一个页面的
PTE_A位是否被设置后,切记要恢复它。否则无法检测自上次pgacess之后用户是否再次访问它 vmprint()可能对debug有帮助
实现
根据
user/pgtlbtest.c中pgaccess的调用和user/user.h中的声明可知该函数的返回值和参数类型。int pgaccess(void *base, int len, void *mask);由此可知在
sys_pgacess()中需要传递三个参数,依次分别为64位地址、32位整型、64位地址:(此处我将内核pagacess函数参数的定义与用户保持一致)int sys_pgaccess(void) { // lab pgtbl: your code here. int npage; uint64 start_addr, res_addr; argaddr(0, &start_addr); argint(1, &npage); argaddr(2, &res_addr); return pgaccess(start_addr, npage, res_addr); }- 通过阅读
walk源码得知该函数的作用:通过给定的虚拟地址和页表指针,可返回该虚拟地址在该页表中对应的PTE的指针(物理地址) 由此可借助
walk函数实现pgaccess:int pgaccess(uint64 start_addr, int npage, uint64 res_addr) { int i; uint res = 0; pte_t* pte; if (start_addr > MAXVA) { panic("pgaccess: too large virtual address"); } if (npage > 32) { panic("too many pages"); } struct proc* p = myproc(); for (i = 0; i < npage; ++i) { pte = walk(p->pagetable, start_addr + i * PGSIZE, 0); // 通过walk获得遍历的虚拟地址在页表中的PTE // 若该PTE的置位可行且有PTE_A则代表被访问过 if ((*pte & PTE_V) && (*pte & PTE_A)) { res |= (1 << i); *pte ^= PTE_A; // 切记将PTE_A位恢复 } } // 通过copyout将结果拷贝给用户地址 return copyout(p->pagetable, res_addr, (char*)&res, sizeof(res)); }- 关于
PTE_A的定义:由下图可知PTE_A应当对应索引为6的比特位,因此在riscv.h中添加#define PTE_A (1L << 6)
结果
运行结果:
可以看到这次pgaccess_test就ok了
测试结果:
总结
make grade结果
个人收获
- 内核为每个进程维护的结构体
struct proc中储存了一个页表指针(该指针是物理地址),这个页表中存储的东西被称为PTE(页表项) - PTE存储的信息包括物理页表号(PPN)和一些权限置位。PPN表示一个物理地址,权限职位表示该地址权限和种类(比如不可读不可写不可执行时,这个地址指向下一级页表)
- 在xv6中一个虚拟地址包含的信息:三级页表的索引和一个偏置:
由一个虚拟地址到物理地址的过程:
- 通过satp寄存器获取L2级页表地址,并通过虚拟地址中L2级页表索引获取PTE
- 该PTE对应的物理物理地址即下一级页表指针,同理一直到获取L0级页表PTE
- L0级页表PTE对应物理地址+虚拟地址中的偏置级该虚拟地址对应的物理地址
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。