0
点赞
收藏
分享

微信扫一扫

OS Lab 3.2 - A kernel page table per process (hard)

boomwu 2022-11-25 阅读 60

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.ckernel/proc.c 中执行此操作。(但是,不要修改 kernel/vmcopyin.ckernel/stats.cuser/usertests.cuser/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




举报

相关推荐

0 条评论