1
实验要求
xv6 有一个内核页表,每当运行在内核态时都会使用它。内核页表直接映射到物理地址,因此内核虚拟地址 x
映射到物理地址 x
。xv6 还为每个进程的用户地址空间提供了一个单独的页表,仅包含该进程的用户内存的映射,从虚拟地址 0
开始。由于内核页表不包含这些映射,用户地址在内核中无效。因此,当内核需要使用在系统调用中传递的用户指针时(例如,传递给 write()
的缓冲区指针),内核必须首先将指针转换为物理地址。本节和下一节的目标是允许内核直接取消对用户指针的引用。
您的第一项工作是修改内核,以便每个进程在内核中执行时都使用自己的内核页表副本。修改结构体 proc
为每个进程维护一个内核页表,修改调度器在切换进程时切换内核页表。对于这一步,每个进程内核页表应该与现有的全局内核页表相同。如果部分测试 usertests
运行正确,您就通过了此实验的这一部分。
阅读本实验说明开头提到的书中的那些章节和代码,了解虚拟内存的工作原理后,正确修改虚拟内存代码会更容易。页表设置中的错误可能由于缺少映射而导致,可能导致加载和存储影响意外的物理内存页面,并且可能会导致执行来自错误内存页面的指令。
2
实验提示
- 为进程的内核页表结构体
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
来找出错误发生的位置。
3
实验步骤
在 kernel/proc.h 的 proc
结构体中添加 pagetable_t kpagetable;
为每个进程维护一个内核页表。
// Per-process state
struct proc {
...
pagetable_t kpagetable; // User kernel page table
pagetable_t pagetable; // User page table
...
}
在 kernel/vm.c 将 kvminit()
函数部分抽取为一个构建内核页表映射的函数 kvmmake()
,每个进程都调这个函数构建内核页表,其中没有映射 CLINT
,因为用户地址空间为低于 PLIC
的部分,而 CLINT
也是低于 PLIC
的。
/*
* make a direct-map page table for the kernel.
*/
pagetable_t
kvmmake()
{
pagetable_t kernel_pagetable = (pagetable_t) kalloc();
memset(kernel_pagetable, 0, PGSIZE);
// uart registers
kvmmap(kernel_pagetable, UART0, UART0, PGSIZE, PTE_R | PTE_W);
// virtio mmio disk interface
kvmmap(kernel_pagetable, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
// PLIC
kvmmap(kernel_pagetable, PLIC, PLIC, 0x400000, PTE_R | PTE_W);
// map kernel text executable and read-only.
kvmmap(kernel_pagetable, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
// map kernel data and the physical RAM we'll make use of.
kvmmap(kernel_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.
kvmmap(kernel_pagetable, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
return kernel_pagetable;
}
在 kernel/vm.c 对 kvmmap()
函数进行修改,增加一个参数 pagetable_t kpagetable
,更改 mappages()
函数调用的第一个实参为 kvmmap()
函数传入的参数 kpagetable
。
void
kvmmap(pagetable_t kpagetable, uint64 va, uint64 pa, uint64 sz, int perm)
{
if(mappages(kpagetable, va, sz, pa, perm) != 0)
panic("kvmmap");
}
在 kernel/vm.c 对 kvminit()
函数进行修改,使其调用函数 kvmmake()
构建内核页表。
/*
* setup kernel_pagetable
*/
void
kvminit()
{
kernel_pagetable = kvmmake();
// CLINT
// kvmmap(kernel_pagetable, CLINT, CLINT, 0x10000, PTE_R | PTE_W);
mappages(kernel_pagetable, CLINT, 0x10000, CLINT, PTE_R | PTE_W);
}
在 kernel/proc.c 中对 procinit()
函数进行修改,去掉为每个进程分配内核堆栈的代码,不再 procinit()
函数中为进程分配内核堆栈。
// initialize the proc table at boot time.
void
procinit(void)
{
struct proc *p;
initlock(&pid_lock, "nextpid");
for(p = proc; p < &proc[NPROC]; p++) {
initlock(&p->lock, "proc");
// Allocate a page for the process's kernel stack.
// Map it high in memory, followed by an invalid
// guard page.
// char *pa = kalloc();
// if(pa == 0)
// panic("kalloc");
// uint64 va = KSTACK((int) (p - proc));
// kvmmap(va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
// p->kstack = va;
}
// kvminithart();
}
在 kernel/proc.c 中对 allocproc()
函数进行修改,用上面的 kvmmake()
方法创建自己的内核页表,同时把原本在 procinit()
里做的 kstack
映射改到 allocproc()
来。
static struct proc*
allocproc(void)
{
...
// Allocate a trapframe page.
...
// A kernel page table
if((p->kpagetable = kvmmake()) == 0) {
freeproc(p);
release(&p->lock);
return 0;
}
// Allocate a page for the process's kernel stack.
// Map it high in memory, followed by an invalid
// guard page.
char *pa = kalloc();
if(pa == 0)
panic("kalloc");
uint64 va = KSTACK((int)(p - proc));
kvmmap(p->kpagetable, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
p->kstack = va;
// An empty user page table.
...
}
在 kernel/proc.c 中修改 scheduler()
以将进程的内核页表加载到内核的 satp
寄存器中,参阅 kvminithart()
。
kernel/vm.c 中的 kvminithart()
函数如下:
// Switch h/w page table register to the kernel's page table,
// and enable paging.
void
kvminithart()
{
w_satp(MAKE_SATP(kernel_pagetable));
sfence_vma();
}
模仿此函数,对 kernel/proc.c 中的 scheduler()
函数进行修改。swtch
到新进程前把 satp
设置为新进程的内核页表,没运行进程的时候用 kernel_pagetable
。
void
scheduler(void)
{
...
for(;;){
...
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state == RUNNABLE) {
p->state = RUNNING;
c->proc = p;
// add code 1 here
w_satp(MAKE_SATP(p->kpagetable));
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;
// add code 2 here
// use kernel_pagetable when no process is running.
kvminithart();
found = 1;
}
release(&p->lock);
}
...
}
对 kernel/proc.c 中的 freeproc()
函数进行修改,在 freeproc()
函数里面释放进程内核页表,注意不要释放物理内存。
static void
freeproc(struct proc *p)
{
if(p->trapframe)
kfree((void*)p->trapframe);
p->trapframe = 0;
// add code here
if(p->kstack)
uvmunmap(p->kpagetable, p->kstack, 1, 1);
p->kstack = 0;
if(p->kpagetable)
kvmfree(p->kpagetable);
p->kpagetable = 0;
if(p->pagetable)
proc_freepagetable(p->pagetable, p->sz);
p->pagetable = 0;
...
}
其中 kvmfree()
模仿 freewalk()
,释放内核页表。
void
kvmfree(pagetable_t kpgtbl) {
for(int i = 0; i < 512; i++){
pte_t pte = kpgtbl[i];
if (pte & PTE_V){
kpgtbl[i] = 0;
if ((pte & (PTE_R|PTE_W|PTE_X)) == 0) {
uint64 child = PTE2PA(pte);
kvmfree((pagetable_t)child);
}
} else if(pte & PTE_V){
panic("kvmfree: leaf");
}
}
kfree((void*)kpgtbl);
}
在 kernel/vm.c 中修改 kvmpa()
函数,因为 kvmpa()
函数会在进程执行期间调用,修改为获取进程内核页表,而不是全局内核页表。因为调用了 myproc()
函数,所以需要添加头文件。
...
#include "spinlock.h"
#include "proc.h"
...
uint64
kvmpa(uint64 va)
{
uint64 off = va % PGSIZE;
pte_t *pte;
uint64 pa;
// change code here
pte = walk(myproc()->kpagetable, va, 0);
if(pte == 0)
panic("kvmpa");
if((*pte & PTE_V) == 0)
panic("kvmpa");
pa = PTE2PA(*pte);
return pa+off;
}
最后在 kernel/defs.h 中添加新增函数的声明以及修改 kvmmap()
函数的声明。
// proc.c
void kvmfree(pagetable_t);
// vm.c
pagetable_t kvmmake();
void kvmmap(pagetable_t, uint64, uint64, uint64, int);
void kvmfree(pagetable_t);
4
实验结果
编译并运行 xv6 进行测试。
$ make qemu
...
init: starting sh
$ usertests
usertests starting
test execout: OK
test copyin: OK
...
test fourteen: OK
test bigfile: OK
test dirfile: OK
test iref: OK
test forktest: OK
test bigdir: OK
ALL TESTS PASSED
退出 xv6 ,运行单元测试检查结果是否正确。
./grade-lab-pgtbl usertests
通过测试样例。
== Test usertests == (120.0s)
== 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