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) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。