Lab 3
vmprint(easy)
首先看一个 & 运算,&运算是按位运算。freewalk函数里面有这样一个判断,PTE_V = 1L<<0,如果pet与PTE_V进行&运算,PTE_V就会补0 , 0...01&pte,这样只有pte的最后一位为1,&运算才会为1,这样才能通过if判断。也就是说只有设置了有效位的页表项才能通过这个if判断。
if((pte & PTE_V))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的下一级映射了条目0、1和2。
每一个page directory里面的每一项有效的pte都可能指向一个新的page directory,也就是上面的 顶级页表的 0 和 255 指向的page directory可以是不同的。
接下来的事情就很简单了:
// vmprint for lab3 1
void
vmprint(pagetable_t pagetable) {
printf("page table %p\n",pagetable);
dovmprint(pagetable,1);
}
void
dovmprint(pagetable_t pagetable,int level) {
for(int i = 0;i < 512; i++) {
pte_t pte = pagetable[i];
// PTE_V: 1
if((pte & PTE_V)){
// switch (level)
// {
// case 1:
// printf("..");
// break;
// case 2:
// printf(".. ..");
// break;
// default:
// printf(".. .. ..");
// break;
// }
for(int j = 1;j <= level;j++) {
if (j == level) {
printf("..");
}else {
printf(".. ");
}
}
pagetable_t child = (pagetable_t)PTE2PA(pte);
printf("%d: pte %p pa %p\n",i,pte,child);
// 只有三级页表,如果再递归就错了。
if (level < 3) {
dovmprint(child,level+1);
}
}
}
}A kernel page table per process (hard)
Xv6有一个单独的用于在内核中执行程序时的内核页表。内核页表直接映射(恒等映射)到物理地址,也就是说内核虚拟地址x映射到物理地址仍然是x。Xv6还为每个进程的用户地址空间提供了一个单独的页表,只包含该进程用户内存的映射,从虚拟地址0开始。因为内核页表不包含这些映射,所以用户地址在内核中无效。因此,当内核需要使用在系统调用中传递的用户指针(例如,传递给write()的缓冲区指针)时,内核必须首先将指针转换为物理地址。本节和下一节的目标是允许内核直接解引用用户指针。
YOUR JOB
你的第一项工作是修改内核来让每一个进程在内核中执行时使用它自己的内核页表的副本。修改
struct proc来为每一个进程维护一个内核页表,修改调度程序使得切换进程时也切换内核页表。对于这个步骤,每个进程的内核页表都应当与现有的的全局内核页表完全一致。如果你的usertests程序正确运行了,那么你就通过了这个实验。
在
struct proc中为进程的内核页表增加一个字段为一个新进程生成一个内核页表的合理方案是实现一个修改版的
kvminit,这个版本中应当创造一个新的页表而不是修改kernel_pagetable。你将会考虑在allocproc中调用这个函数确保每一个进程的内核页表都关于该进程的内核栈有一个映射。在未修改的XV6中,所有的内核栈都在procinit中设置。你将要把这个功能部分或全部的迁移到
allocproc中修改
scheduler()来加载进程的内核页表到核心的satp寄存器(参阅kvminithart来获取启发)。不要忘记在调用完w_satp()后调用sfence_vma()没有进程运行时
scheduler()应当使用kernel_pagetable在
freeproc中释放一个进程的内核页表你需要一种方法来释放页表,而不必释放叶子物理内存页面。
调式页表时,也许
vmprint能派上用场修改XV6本来的函数或新增函数都是允许的;你或许至少需要在kernel/vm.c*和**kernel/proc.c*中这样做(但不要修改*kernel/vmcopyin.c*, *kernel/stats.c*, *user/usertests.c*, 和*user/stats.c***)
页表映射丢失很可能导致内核遭遇页面错误。这将导致打印一段包含
sepc=0x00000000XXXXXXXX的错误提示。你可以在*kernel/kernel.asm*通过查询XXXXXXXX来定位错误。
lab的要求我感觉每句话都要细细推敲,这个实验好难啊,再后面那个也挺难....。
这个实验其实就是要求我们为每一个进程维护一个内核页表,使得在该进程切入内核态时,可以直接使用进程的内核页表,而不用去使用vm.c中公用的内核页表,这样就可以在用户态和内核态中均实现虚拟内存。在源码中对应可参照的就是维护一个用户进程页表和初始的内核页表。
添加proc字段
按Hints第一步要求加上个kernelpt字段即可。
修改版kvminit
Hint2:为一个新进程生成一个内核页表的合理方案是实现一个修改版的kvminit,这个版本中应当创造一个新的页表而不是修改kernel_pagetable。
allocproc调用newkvmpt并添加映射
Hint2:你将会考虑在allocproc中调用这个函数
Hint3:确保每一个进程的内核页表都关于该进程的内核栈有一个映射。在未修改的XV6中,所有的内核栈都在procinit中设置。你将要把这个功能部分或全部的迁移到allocproc中
Hint3中提示要把 procinit 的部分功能迁移到allocproc中,其实就是要把添加映射那部分拿过来修改一下。
可以看上面procinit()的代码。在三段注释下面的代码中,为每个进程申请一个页大小的栈段kstack,在内存中把它映射到高处,上面是一个守护页,防止栈溢出影响其他进程,在xv6那本书的图里。
修改scheduler
Hint4:修改scheduler()来加载进程的内核页表到核心的satp寄存器(参阅kvminithart来获取启发)。不要忘记在调用完w_satp()后调用sfence_vma()
需要在切换进程前:加载进程的内核页表到核心的satp寄存器
在进程被切掉后:回归原来的内核状态
修改freeproc
Hint5: 在freeproc中释放一个进程的内核页表
还有一个就是要释放掉内核页表,需要递归调用清楚三级子页表:
Hint6:你需要一种方法来释放页表,而不必释放叶子物理内存页面。
修改kvmpa
最后make qemu的时候报错了... panic:kvmpa
找不到到底哪错了,百度一下果然很多人在这一步卡了,需要修改一下vm.c的kvmpa函数:(提示也没说,太坑了)
final:
Simplify copyin/copyinstr(hard)
copyin/copyinstr(hard)内核的copyin函数读取用户指针指向的内存。它通过将用户指针转换为内核可以直接解引用的物理地址来实现这一点。这个转换是通过在软件中遍历进程页表来执行的。在本部分的实验中,您的工作是将用户空间的映射添加到每个进程的内核页表(上一节中创建),以允许copyin(和相关的字符串函数copyinstr)直接解引用用户指针。
YOUR JOB
将定义在kernel/vm.c中的
copyin的主题内容替换为对copyin_new的调用(在kernel/vmcopyin.c中定义);对copyinstr和copyinstr_new执行相同的操作。为每个进程的内核页表添加用户地址映射,以便copyin_new和copyinstr_new工作。如果usertests正确运行并且所有make grade测试都通过,那么你就完成了此项作业。
此方案依赖于用户的虚拟地址范围不与内核用于自身指令和数据的虚拟地址范围重叠。Xv6使用从零开始的虚拟地址作为用户地址空间,幸运的是内核的内存从更高的地址开始。然而,这个方案将用户进程的最大大小限制为小于内核的最低虚拟地址。内核启动后,在XV6中该地址是0xC000000,即PLIC寄存器的地址;请参见kernel/vm.c中的kvminit()、kernel/memlayout.h和文中的图3-4。您需要修改xv6,以防止用户进程增长到超过PLIC的地址。
Hints:
先用对
copyin_new的调用替换copyin(),确保正常工作后再去修改copyinstr在内核更改进程的用户映射的每一处,都以相同的方式更改进程的内核页表。包括
fork(),exec(), 和sbrk().不要忘记在
userinit的内核页表中包含第一个进程的用户页表用户地址的PTE在进程的内核页表中需要什么权限?(在内核模式下,无法访问设置了
PTE_U的页面)别忘了上面提到的PLIC限制
替换copyin*_new
在vm.c中替换copyin和copyinstr。需要把两个函数定义写到 defs.h
通用的copy函数
在hint2中提示我们需要更改fork,exec,sbrk这些函数,所以可以提取一个公用的将用户空间的映射添加到每个进程的内核页表的函数:
看一下PGROUNDUP的作用:
PGROUNDUP和PGROUNDDOWN是将地址发送到PGSIZE的倍数的宏。这些通常用于获得页面对齐的地址。 PGROUNDUP将把地址四舍五入到PGSIZE的更高倍数,而PGROUNDDOWN会将其舍入到PGSIZE的较低倍数。让我们采取一个实例,如果PGROUNDUP被调用PGSIZE 1KB的系统与地址620
PGROUNDUP(620)上==>((620 +(1024 - 1))&〜(1023))= => 1024 因此地址620被向上舍入到1024
类似地,对于PGROUNDDOWN考虑
PGROUNDDOWN(2400)==>(2400 &〜(1023))==> 2048
修改fork等函数
对fork()的修改:
对exec()的修改:
对sbrk()的修改:通过系统调用可以看到sys_sbrk是调用growproc()的
Xv6使用从零开始的虚拟地址作为用户地址空间,幸运的是内核的内存从更高的地址开始。然而,这个方案将用户进程的最大大小限制为小于内核的最低虚拟地址。内核启动后,在XV6中该地址是0xC000000,即PLIC寄存器的地址;请参见kernel/vm.c中的kvminit()、kernel/memlayout.h和文中的图3-4。您需要修改xv6,以防止用户进程增长到超过PLIC的地址。
对userinit()的修改:
hint3:不要忘记在userinit的内核页表中包含第一个进程的用户页表
这样就ok啦,执行make grade测试或者make qemu ; ./usertests 测试:
Last updated