准备环境
按照官方指南准备环境,即
$ git fetch
$ git checkout pgtbl
$ make clean
如果提示之前的修改没有提交的话可以把之前的修改去掉(备份一份)。
1.Print a page table
编写vmprint函数,接收一个pagetable_t
作为参数,按照格式打印页表内容,对于这个功能,主要是打印第一个进程的页表。
这个lab并不难,难度也是easy,首先搞懂要打印什么,就是打印页表的pte内容,然后对应的物理地址,物理地址可以通过PTE2PA
得到,PTE2PA
就是右移十位把标志位去掉,然后左移十二位得到物理地址。然后搞懂freewalk函数怎么写的,我们模仿写就行了。只是要按格式打印,即不同级的页表格式不同,所以要加一个表示级数的参数,不过vmprint函数只能有一个参数,所以额外写一个辅助函数,整体如下:
void vmprint(pagetable_t pagetable) {
printf("page table %p\n", pagetable);
helpvmprint(pagetable, 1);
}
void helpvmprint(pagetable_t pagetable, int deep) {
if (deep > 3) {
return;
}
for (int i = 0; i < 512; ++i) {
pte_t pte = pagetable[i];
if (pte & PTE_V) {
uint64 child = PTE2PA(pte);
if (deep == 1) {
printf("..");
} else if (deep == 2) {
printf(".. ..");
} else if (deep == 3) {
printf(".. .. ..");
}
printf("%d: pte %p pa %p\n", i, pte, child);
helpvmprint((pagetable_t)child, deep + 1);
}
}
}
在kernel/defs.h中加上这两个函数的声明
void vmprint(pagetable_t);
void helpvmprint(pagetable_t, int);
启动qemu可以看到
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
执行命令
sudo ./grade-lab-pgtbl pte print
得到
make: 'kernel/kernel' is up to date.
== Test pte printout == pte printout: OK (1.0s)
编写成功!
2.A kernel page table per process
内核只有一个页表,现在要让每个线程都有一个内核页表副本,并且在内核态运行的时候使用这个副本进行虚拟地址到内核地址的转换。
总的来说分三步完成
step1
初始化每个线程的内核页表副本,这个副本要和内核页表相同,所以仿照kvminit
写一个函数生成一个与内核页表相同的页表,如下:
pagetable_t
kvmprocessinit()
{
pagetable_t pagetable = (pagetable_t) kalloc();
memset(pagetable, 0, PGSIZE);
// uart registers
kvmprocessmap(pagetable, UART0, UART0, PGSIZE, PTE_R | PTE_W);
// virtio mmio disk interface
kvmprocessmap(pagetable, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
// CLINT
kvmprocessmap(pagetable, CLINT, CLINT, 0x10000, PTE_R | PTE_W);
// PLIC
kvmprocessmap(pagetable, PLIC, PLIC, 0x400000, PTE_R | PTE_W);
// map kernel text executable and read-only.
kvmprocessmap(pagetable, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
// map kernel data and the physical RAM we'll make use of.
kvmprocessmap(pagetable, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);
// map the trampoline for trap entry/exit to
// the highest virtual address in the kernel.
kvmprocessmap(pagetable, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
return pagetable;
}
在allocproc
中调用这个函数,给每个进程一个内核页表副本(主要proc结构体中要加一个元素指向内核页表副本)。
然后是内核栈需要在现在这个副本有对应的映射,在procinit
函数里面有对应的内核栈映射代码,我们可以把这个代码迁移到allocproc
中,但是我们在virtio_disk_rw
函数中翻译内核栈虚拟地址的时候使用的kvmpa
这个函数,这个函数用的是内核页表来翻译,所以内核页表还是要有各个进程的内核栈映射,所以在procinit
里面的代码不改,我们将内核栈对应的虚拟地址和物理地址之间的映射直接存到每个进程的内核页表副本中,也是在allocproc
中完成,所以现在的allocproc
为:
static struct proc*
allocproc(void)
{
struct proc *p;
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state == UNUSED) {
goto found;
} else {
release(&p->lock);
}
}
return 0;
found:
p->pid = allocpid();
// Allocate a trapframe page.
if((p->trapframe = (struct trapframe *)kalloc()) == 0){
release(&p->lock);
return 0;
}
// An empty user page table.
p->pagetable = proc_pagetable(p);
if(p->pagetable == 0){
freeproc(p);
release(&p->lock);
return 0;
}
// 给每个进程分配内核页表副本
p->kernelpage = kvmprocessinit();
if (p->kernelpage == 0) {
freeproc(p);
release(&p->lock);
return 0;
}
// 之前是procinit里面把每个进程的内核栈映射在内核页表里面对应好了,现在每个进程都有自己的内核页表了
// 所以需要重新给自己的内核页表映射栈
kvmprocessmap(p->kernelpage, p->kstack, kvmpa(p->kstack), PGSIZE, PTE_R | PTE_W);
// Set up new context to start executing at forkret,
// which returns to user space.
memset(&p->context, 0, sizeof(p->context));
p->context.ra = (uint64)forkret;
p->context.sp = p->kstack + PGSIZE;
return p;
}
step2
切换进程的时候也要切换satp寄存器,把要运行的进程对应的内核页表副本物理地址加载到satp中去方便使用,直接在scheduler
中更改,要在上下文切换之前切换satp寄存器,否则切换了上下文运行的就是另外的指令了,不用在执行完进程后调用kvminithart
函数,因为除了内核栈内核页表副本和内核页表是相同的,scheduler
也没有用到进程的内核栈,scheduler
如下:
void
scheduler(void)
{
struct proc *p;
struct cpu *c = mycpu();
c->proc = 0;
for(;;){
// Avoid deadlock by ensuring that devices can interrupt.
intr_on();
int found = 0;
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state == RUNNABLE) {
// Switch to chosen process. It is the process's job
// to release its lock and then reacquire it
// before jumping back to us.
p->state = RUNNING;
c->proc = p;
// 切换satp寄存器
w_satp(MAKE_SATP(p->kernelpage));
sfence_vma();
swtch(&c->context, &p->context);
// Process is done running for now.
// It should have changed its p->state before coming back.
c->proc = 0;
found = 1;
}
release(&p->lock);
}
if (found == 0) {
kvminithart();
}
#if !defined (LAB_FS)
if(found == 0) {
intr_on();
asm volatile("wfi");
}
#else
;
#endif
}
}
step3
最后就是freeproc的时候也要释放内核页表副本所占的页表了,注意是副本所占的页表,而不是副本页表指向的页面,所有副本页表和内核页表指向的页面是相同的,不能因为某一个进程释放那些页面,释放的方式可以参考freewalk,不过我们不需要用uvmunmap
先清除映射,直接释放有效的页面即可,参考freewalk
编写freeprocesswalk
,如下:
void freeprocesswalk(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) {
uint64 child = PTE2PA(pte);
freeprocesswalk((pagetable_t)child);
pagetable[i] = 0;
}
}
kfree((void*)pagetable);
}
并且在freeproc中使用它。
在命令行中使用命令
sudo ./grade-lab-pgtbl usertests
验证编写是否正确
出现
make: 'kernel/kernel' is up to date.
== Test usertests == (122.5s)
== Test usertests: copyin ==
usertests: copyin: OK
== Test usertests: copyinstr1 ==
usertests: copyinstr1: OK
== Test usertests: copyinstr2 ==
usertests: copyinstr2: OK
== Test usertests: copyinstr3 ==
usertests: copyinstr3: OK
== Test usertests: sbrkmuch ==
usertests: sbrkmuch: OK
== Test usertests: all tests ==
usertests: all tests: OK
编写成功!
3.Simplify copyin
/copyinstr
首先是要搞清楚在什么地方需要将用户页表的内容复制到内核页表来。因为第一个进程是后面所有进程的祖先,所以我们首先把第一个进程用户页表的内容复制过来,然后再页表内容发生改变和产生新进程的地方重新复制。即四个地方:userinit,fork,sbrk,exec。
首先是复制页表的函数,其实这个函数还是很好编写的,只要会用walk函数,如下:
int
copypages(pagetable_t src, pagetable_t dst, uint64 begin, uint64 end)
{
uint64 va;
pte_t *srcpte;
pte_t *dstpte;
begin = PGROUNDDOWN(begin);
for (va = begin; va < end; va += PGSIZE) {
if ((srcpte = walk(src, va, 0)) == 0) {
return -1;
}
if (!(*srcpte & PTE_V)) {
return -1;
}
if ((dstpte = walk(dst, va, 1)) == 0) {
return -1;
}
*dstpte = (*srcpte) & (~PTE_U);
}
return 0;
}
然后是userinit
void
userinit(void)
{
struct proc *p;
p = allocproc();
initproc = p;
// allocate one user page and copy init's instructions
// and data into it.
uvminit(p->pagetable, initcode, sizeof(initcode));
p->sz = PGSIZE;
// prepare for the very first "return" from kernel to user.
p->trapframe->epc = 0; // user program counter
p->trapframe->sp = PGSIZE; // user stack pointer
safestrcpy(p->name, "initcode", sizeof(p->name));
p->cwd = namei("/");
p->state = RUNNABLE;
// 添加一行即可
copypages(p->pagetable, p->kernelpage, 0, p->sz);
release(&p->lock);
}
fork:
int
fork(void)
{
int i, pid;
struct proc *np;
struct proc *p = myproc();
// Allocate process.
if((np = allocproc()) == 0){
return -1;
}
// Copy user memory from parent to child.
if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
freeproc(np);
release(&np->lock);
return -1;
}
np->sz = p->sz;
np->parent = p;
// copy saved user registers.
*(np->trapframe) = *(p->trapframe);
// Cause fork to return 0 in the child.
np->trapframe->a0 = 0;
// increment reference counts on open file descriptors.
for(i = 0; i < NOFILE; i++)
if(p->ofile[i])
np->ofile[i] = filedup(p->ofile[i]);
np->cwd = idup(p->cwd);
safestrcpy(np->name, p->name, sizeof(p->name));
pid = np->pid;
np->state = RUNNABLE;
if (copypages(np->pagetable, np->kernelpage, 0, np->sz) == -1) {
return -1;
}
release(&np->lock);
return pid;
}
exec
if(p->pid==1)
vmprint(p->pagetable);
if (copypages(p->pagetable, p->kernelpage, 0, p->sz) == -1) {
goto bad;
}
return argc;
最需要注意的就是sbrk,其对应的函数是growproc,在这里面要修改三处地方,一个是增长后的内存地址不能超过PLIC,一个是增长时复制页表,还有一个是减少内存时直接取消内核页表匹配相应的部分,如下
int
growproc(int n)
{
uint sz;
struct proc *p = myproc();
sz = p->sz;
if(n > 0){
if(sz + n > PLIC || (sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
return -1;
}
if (copypages(p->pagetable, p->kernelpage, sz - n, sz) == -1) {
return -1;
}
} else if(n < 0){
sz = uvmdealloc(p->pagetable, sz, sz + n);
uvmunmap(p->kernelpage, PGROUNDUP(sz), (PGROUNDUP(p->sz) - PGROUNDUP(sz)) / PGSIZE, 0);
}
p->sz = sz;
return 0;
}
然后将copyin,copyinstr的内容改为对copyin_new和copyinstr_new的调用
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);
}
最后用sudo ./grade-lab-pgtbl usertests
和sudo make grade
测试即可。