首先切换pgtbl分支
git checkout pgtbl make clean
Print a page table
定义一个vmprint()函数,应当接受一个pagetable_t作为参数,会按照如下的格式进行打印页表
在exec.c中的return argc之前插入if(p->pid == 1) vmprint(p->pagetable)
,以打印第一个进程的页表
当启动xv6时,那么就会打印输出来描述第一个进程刚刚完成exec()->init时的页表:
-
第一行
page table 0x0000000087f64000
显示vmprint的参数 -
之后每一行对应一个PTE,包含树中所指的页表页的PTE
-
每个PTE行都有一些 . ,表示其在树中的深度
-
每一行显示其在页表页面中的PTE索引,PTE比特位以及PTE所对应的物理地址
1.首先根据提示在exec.c中的return argc之前插入if(p->pid==1) vmprint(p->pagetable)
2.看kernel/vm.c中有freewalk方法,用来释放页表页面内存的
根据这个逻辑可以仿照出来一个vmprint(pagetable_t pagetable)
函数,
pagetable_t是一个页面指针,xv6使用三级页表,每个页面大小为4096字节,在kernel/riscv.h中规定
不过由于要打印 .. 所以考虑递归需要多传入一个参数用来记录层级
所以另外实现一个递归函数_vmprint(pagetable_t pagetable, int level)
3.添加vmprint函数定义放到kernel/defs.h里面
void vmprint(pagetable_t);
make qemu启动系统测试
A kernel page table per process
xv6本来在内核有一个单独的内核页表,直接恒等映射到物理地址,同时xv6每个进程用户地址空间都准备了一个单独页表,虚拟地址从0开始,但是由于内核页表不包含这些映射,那么从用户态进入内核态时就用不了这些用户地址,故当内核需要使用在系统调用中传递的用户指针时,内核必须将指针直接转换成物理地址。这效率不高,所以本个实验就是允许内核直接解引用用户指针,相当于为每个用户进程多加一个内核页表,当切换到内核态时直接切换到内核页表
其实相当于用空间换时间的思想,为每个进程都维护一个内核页表就不需要用一个内核公共页表每次切换进程的时候都要转换虚拟地址到物理地址,而是直接切换不同进程的内核页表即可
1.在kernel/proc.h里面的struct proc加上内核页表的字段
2.在vm.c中添加新的方法proc_kpt_init,该方法用于在allocproc中初始化进程的内核页表,同时这个函数还需要一个辅助函数uvmmap,该函数和kvmmap几乎一致,不过kvmmap是对xv6内核页表进行映射,而uvmmap将用于进程的内核页表进行映射
3.将proc_kpt_init和uvmmap函数定义到kernel/defs.h里面
void uvmmap(pagetable_t pagetable, uint64 va, uint64 pa, uint64 sz, int perm);
pagetable_t proc_kpt_init();
4.在kernel/proc.c里面的allocproc调用
注意此处还补充了内核栈页表的映射,之前这一部分是在procinit里面进行的,但现在每个进程都有自己的内核页表,就不需要在全局内核页表里面进行映射了,故将之前那一部分注释
需要补充的是,KSTACK是根据进程索引来拿到虚拟内存中内核栈地址的,在memlayout.h里面有定义
#define KSTACK(p) (TRAMPOLINE - ((p)+1)* 2*PGSIZE)
每个栈有两个页,一个页面作为保护页,一个则是有效页,多减一个1是为了分配的时候正好把该页填充,栈是从上往下增长,所以提前开出一页来进行物理映射
uvmmap:
mappages:
5.修改scheduler()
来加载进程的内核页表到SATP寄存器
首先仿照之前切换全局内核页表的kvminithart()
写一个类似的函数用来切换进程自己的内核页表proc_inithart
kernel/vm.c:
将proc_inithart添加到kernel/defs.h里面
void proc_inithart(pagetable_t kpt);
然后在scheduler()里面调用
但在结束的时候,需要切换回原先的kernel_pagetable
。直接调用调用上面的kvminithart()
即可
swtch(&c->context, &p->context);: 这个函数用于上下文切换。它保存当前进程(或内核线程)的上下文(寄存器状态)到 c->context,并加载进程 p 的上下文(寄存器状态)到当前硬件线程。上下文切换后,硬件线程将开始执行进程 p 的代码。
执行完后就切换回来全局内核页表
6.在freeproc
中释放一个进程的内核页表,首先释放页表内的内核栈,调用uvmunmap
可以解除映射,最后的一个参数(do_free
)为一的时候,会释放实际内存。
同时还要删除进程内核页表的本身的内存,但不删除页表指向的物理内存,因为映射的物理内存是和内核进程以及其他所有进程共享的
所以在proc.c中实现一个完成上述逻辑的函数proc_freekernelpt
然后再freeproc中调用:
7.修改vm.c中的kvmpa(将虚拟地址转换成物理地址),将原先的全局内核页表kernel_pagetable
改成myproc()->kernelpt
,使用进程的内核页表。
测试程序
make qemu
usertests
Simplify copyin/copyinstr
内核的copyin
函数读取用户指针指向的内存。它通过将用户指针转换为内核可以直接解引用的物理地址来实现这一点
但现在要求将用户空间的映射添加到每个进程的内核页表以方便copyin可以直接解引用用户指针
其实就是实现将用户空间的映射添加到每个进程的内核页表,将进程的用户态页表复制一份到进程的内核态页表即可
1.首先在kernel/vm.c添加复制函数。需要注意内核模式下无法访问PTE_U = 1的页面,所以要将其设置成0
将其定义添加到def.h头文件当中
void u2kvmcopy(pagetable_t pagetable, pagetable_t kernelpt, uint64 oldsz, uint64 newsz);
2.在内核更改进程用户映射的每一处(fork()
, exec()
, 和sbrk()
)
sbrk() 在kernel/sysproc.c中找到sys_sbrk(void),发现这其中只有growproc
是负责将用户内存增加或缩小 n 个字节,防止用户进程增长到超过PLIC
的地址,我们需要给它加个限制
int
copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
{
return copyin_new(pagetable, dst, srcva, len);
}
int
copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max)
{
return copyinstr_new(pagetable, dst, srcva, max);
}
3.替换掉原来的copyin()
和 copyinstr()
copyin_new和copyinstr_new函数实现在vmcopyin.c文件中,此时第一个参数进程用户页表其实没用到,因为进程的内核态页表已经包含了用户态的内存映射,进入到内核态里面stap寄存器已经指向当前进程内核态页表,无需转换,直接copy即可
memmove((void *) dst, (void )srcva*, len);
dst[i] = s[i];
最后将其添加到kernel/defs.h中
// vmcopyin.c
int copyin_new(pagetable_t, char *, uint64, uint64);
int copyinstr_new(pagetable_t, char *, uint64, uint64);
测试