目录
1. 实验1:提速系统调用 (难度:简单)
实验参考步骤
2. 实验2:输出页表 (难度:简单)
实验参考步骤
3. 实验3:检测哪些页表被访问过(难度:困难)
实验参考步骤
在本实验中,我们将探索页表并对其进行修改,以加快某些系统调用,并检测那些页面已被访问。
在开始编码之前,请阅读xv6 book[1]和以下相关源程序:
kern/memlayout.h,定义内存的布局。
kern/vm.c,包含大多数虚拟内存(vm)代码。
kernel/kalloc.c,包含分配和释放物理内存的代码。
还可以参考RISC-V特权体系结构手册[2]。
在启动实验时,请切换到pgtbl分支:
$ git fetch
$ git checkout pgtbl
$ make clean
一些操作系统(例如Linux)通过在用户空间和内核之间的只读区域共享数据来加速某些系统调用。这消除了在执行这些系统调用时需要跨内核的需求。为了帮助你了解如何将映射插入到页表中,你的第一个任务是为xv6中的getpid()系统调用实现这种优化。
当创建一个进程时,将一个只读页面映射到虚拟地址USYSCALL所在页面。在该页面的开头,存储了一个struct usyscall结构变量,初始化该变量,使其存储当前进程的PID。对于本实验,请在用户空间提供了ugetpid()函数,并将自动使用USYSCALL映射。如果运行pgtbltest时能ugetpid测试用例通过,你将得到本实验的全部学分。(注:USYSCALL和struct usyscall都在memlayout.h中定义)
提示:
● 可以在kernel/proc.c中的proc_pagetable()中完成映射。
● 选择权限位允许在用户空间仅能读取页面。
● 你可能会发现mappages()是一个有用的实用工具。
● 不要忘记在allocproc()中分配和初始化页面。
● 确保在freeproc()中释放页面。
思考题:使用该共享页面还可以加快哪些xv6系统调用?请解释怎么做。
步骤1:在kernel/proc.h中的struct proc在增加一个域,存储共享内存块的物理地址。
步骤2:在kernel/proc.c的allocproc函数中增加申请共享内存页。
步骤3:在kernel/proc.c的proc_pagetable函数中增加在内核中共享内存页的初始化,以及对共享内存块的页表初始化。
/*
* the kernel's page table.
*/
extern pagetable_t kernel_pagetable;
// Create a user page table for a given process,
// with no user memory, but with trampoline 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 just below TRAMPOLINE, 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;
}
// map the USYSCALL just below TRAPFRAME.
if(mappages(pagetable, USYSCALL, PGSIZE,
(uint64)(p->usyscall), PTE_R | PTE_U) < 0){
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmfree(pagetable, 0);
return 0;
}
return pagetable;
}
步骤4:在kernel/proc.c的freeproc函数中增加释放共享内存块。
步骤5:在kernel/proc.c的proc_freepagetable函数中增加一行释放页表中共享内存页项。
为了帮助你可视化RISC-V页表,也许为了帮助将来的调试,你的第二个任务是写一个函数来输出页表的内容。
任务:定义一个名为vmprint( )的函数。它有一个pagetable_t类型的参数,按下面描述的格式输出页表。在exec.c中的return argc;之前插入if(p->pid==1) vmprint(p->pagetable) 语句来输出第一个进程的页表。如果你通过了make grade的pte printout测试,这个实验你将获得满分。
现在,当你启动xv6时,在第一个进程刚刚完成exec( )初始化时,它将输出以下的页表信息:
page table 0x0000000087f6e000
..0: pte 0x0000000021fda801 pa 0x0000000087f6a000
.. ..0: pte 0x0000000021fda401 pa 0x0000000087f69000
.. .. ..0: pte 0x0000000021fdac1f pa 0x0000000087f6b000
.. .. ..1: pte 0x0000000021fda00f pa 0x0000000087f68000
.. .. ..2: pte 0x0000000021fd9c1f pa 0x0000000087f67000
..255: pte 0x0000000021fdb401 pa 0x0000000087f6d000
.. ..511: pte 0x0000000021fdb001 pa 0x0000000087f6c000
.. .. ..510: pte 0x0000000021fdd807 pa 0x0000000087f76000
.. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000
第一行显示vmprint的参数,之后,每个PTE都有一行,包括引用树中较深的页表页的PTE。每个PTE行都有一些以“..”的缩进,表示它在树中的深度。每个PTE行显示了的PTE索引,包括页表页、PTE位以及从PTE提取的物理地址。不输出无效的PTE。在上面的示例中,顶级页表页具有0和255的映射。下一级只映射了索引0,该索引0映射了最后一级的0、1和2。
你的代码输出的物理地址与上面显示的可能不相同。但显示项数和虚拟地址应相同。
提示:
● 可以将vmprint( )放在kernel/vm.c中。
● 使用kernel/riscv.h文件末尾的宏。
● 函数freewalk可能是鼓舞人心的(可以仿照该函数来写vmprint)。
● 在kernel/defs.h中定义vmprint的原型,以便可以从exec.c调用它。
● 在printf调用中使用%p输出完整的64位十六进制PTE和地址,如示例所示。
根据文本[1]中的图3.4解释vmprint的输出。第0页包含什么?第二页是什么?在用户模式下运行时,进程是否可以读取/写入页1映射的内存?
步骤1:本实验实现一个vmprint()函数来遍历页表并打印,在xv6中有个freewalk()函数,实现递归释放页表页,我们可以模仿该函数来写vmprint()函数。
freewalk()函数代码如下,递归释放页表页。
// 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);
}
模仿freewalk()编写vmprint()函数,并添加在kernel/vm.c中。
void
printwalk(pagetable_t pagetable, uint level) {
char* prefix;
if (level == 2) prefix = "..";
else if (level == 1) prefix = ".. ..";
else prefix = ".. .. ..";
for(int i = 0; i < 512; i++){ // 每个页表有512项
pte_t pte = pagetable[i];
if(pte & PTE_V){ // 该页表项有效
uint64 pa = PTE2PA(pte); // 将虚拟地址转换为物理地址
printf("%s%d: pte %p pa %p\n", prefix, i, pte, pa);
if((pte & (PTE_R|PTE_W|PTE_X)) == 0){ // 有下一级页表
printwalk((pagetable_t)pa, level - 1);
}
}
}
}
void
vmprint(pagetable_t pagetable) {
printf("page table %p\n", pagetable);
printwalk(pagetable, 2);
}
步骤2:在kernel/defs.h中定义vmprint的原型,以便可以从exec.c调用它。
步骤3:在exec.c中的返回argc之前插入if (p->pid==1) vmprint(p->pagetable),以输出第一个进程的页表。
步骤4:make qemu, 可以看到以下输出,这说明实验成功。
分析:
page table 0x0000000087f6e000
根页表物理地址:0x0000000087f6e000
..0: pte 0x0000000021fda801 pa 0x0000000087f6a000
根页目录项0,PTE内容为0x0000000021fda801,表示下一级(第二级)页表的物理页号为:0x87f6a, Flags为:0x001,PTE_V有效。该物理页的起始物理地址为0x0000000087f6a000
.. ..0: pte 0x0000000021fda401 pa 0x0000000087f69000
第三级页目录页表位置。pte的物理页号:0x87f69,Flags是0x001.
.. .. ..0: pte 0x0000000021fdac1f pa 0x0000000087f6b000
进程的第0页,第0页的物理页号:0x87f6b,Flags是0x01f. 物理块起始地址:0x0000000087f6b000
.. .. ..1: pte 0x0000000021fda00f pa 0x0000000087f68000
进程的第1页,第1页的物理页号:0x87f67,Flags是0x01f. 物理块起始地址:0x0000000087f67000
.. .. ..2: pte 0x0000000021fd9c1f pa 0x0000000087f67000
进程的第2页,第2页的物理页号:0x87f67,Flags是0x01f. 物理块起始地址:0x0000000087f67000
..255: pte 0x0000000021fdb401 pa 0x0000000087f6d000
根页目录项255,下一级(第二级)页表的物理页号为:0x87f6d, Flags为:0x001,PTE_V有效。该物理页的起始物理地址为0x0000000087f6d000
.. ..511: pte 0x0000000021fdb001 pa 0x0000000087f6c000
第三级页目录页表位置。pte的物理页号:0x87f6c,Flags是0x001.
.. .. ..510: pte 0x0000000021fdd807 pa 0x0000000087f76000
进程的第510页,第510页的物理页号:0x87f76,Flags是0x007. 物理块起始地址:0x0000000087f76000
.. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000
进程的第511页,第511页的物理页号:0x80007,Flags是0x00b. 物理块起始地址:0x0000000080007000
一些垃圾收集器(自动内存管理的一种形式)可以从已访问(读或写)页面的信息中获益。在本部分的实验中,你将向xv6添加一个新特性,通过检查RISC-V页表中的访问位来获取信息并向用户空间报告这些信息。RISC-V硬件页面遍历器在解决TLB miss时在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()可方便地调试页表。
步骤1:在kernel/riscv.h中定义常量PTE_A。
步骤2:在kernel/sysproc.c中编写sys_pgaccess函数。
int
sys_pgaccess(void)
{
// lab pgtbl: your code here.
uint64 base;
uint64 mask;
int len;
pagetable_t pagetable = 0;
unsigned int procmask = 0 ;
pte_t *pte;
struct proc *p = myproc();
if(argaddr(0, &base) < 0 || argint(1, &len) < 0 || argaddr(2, &mask) < 0)
return -1;
if (len > sizeof(int)*8)
len = sizeof(int)*8;
for(int i=0; ipagetable;
if(base >= MAXVA)
panic("pgaccess");
for(int level = 2; level > 0; level--) {
pte = &pagetable[PX(level, base)];
if(*pte & PTE_V) {
pagetable = (pagetable_t)PTE2PA(*pte);
} else {
return -1;
}
}
pte = &pagetable[PX(0, base)];
if(pte == 0)
return -1;
if((*pte & PTE_V) == 0)
return -1;
if((*pte & PTE_U) == 0)
return -1;
if(*pte & PTE_A) {
procmask = procmask | (1L << i);
*pte = *pte & (~PTE_A);
}
base += PGSIZE;
}
pagetable = p->pagetable;
return copyout(pagetable, mask, (char *) &procmask, sizeof(unsigned int));
}
验证实验是否通过。
本实验通过了测试。最后运行make grade,查看能得多少分?
46分,满分!!!
参考资料:
[1] https://pdos.csail.mit.edu/6.828/2021/xv6/book-riscv-rev2.pdf
[2] https://github.com/riscv/riscv-isa-manual/releases/download/Ratified-IMFDQC-and-Priv-v1.11/riscv-privileged-20190608.pdf
[3]https://pdos.csail.mit.edu/6.828/2020/labs/pgtbl.html
[4] https://blog.csdn.net/u013577996/article/details/109582932
[5] https://www.cnblogs.com/weijunji/p/14338439.html
[6]https://zhuanlan.zhihu.com/p/280914560