LAB 网页:https://pdos.csail.mit.edu/6.S081/2023/labs/pgtbl.html
任务1:Speed up system calls
根据网页,操作系统可以通过把部分数据放入用户空间的页表,来使得部分系统调用不用进入内核空间,从而提高速度。我们的第一个任务是:对 getpid() 系统调用实现这个优化(通过对页表插入映射的方式)。
以下是作业要求:
1.当一个进程被创建的时候,可以在 USYSCALL 位置插入一个只读的页。在页的开头,存入一个 usyscall 结构体,并且使用当前进程的 pid 初始化它。在实验中,ugetpid() 已经在用户空间中被实现,它会使用 USYSCALL 的映射。只要 ugetpid() 能通过测试,这个 lab 就满分。
暗示:
1.Choose permission bits that allow userspace to only read the page.
2.There are a few things that need to be done over the lifecycle of a new page. For inspiration, understand the trapframe handling in kernel/proc.c.
思考题:
Which other xv6 system call(s) could be made faster using this shared page? Explain how.
OK,根据作业要求,我们先来看看这个 ugetpid() 的实现,相关的测试代码位于 user/pgtbltest.c
void
err(char *why)
{printf("pgtbltest: %s failed: %s, pid=%d\n", testname, why, getpid());exit(1);
}void
ugetpid_test()
{int i;printf("ugetpid_test starting\n");testname = "ugetpid_test";// 执行 64 次 fork,判断 getpid() 是否等于 ugetpid()for (i = 0; i < 64; i++) {int ret = fork();if (ret != 0) {wait(&ret);if (ret != 0)exit(1);continue;}if (getpid() != ugetpid())err("missmatched PID");exit(0);}printf("ugetpid_test: OK\n");
}int
main(int argc, char *argv[])
{ugetpid_test();pgaccess_test();printf("pgtbltest: all tests succeeded\n");exit(0);
}
从代码来看,实际上就是重复调用 fork(),并且在子进程中调用 getpid() 和 ugetpid(),判断两者是否相等。
直接运行 make qemu
,发现能执行 pgtbltest,阅读 Makefile,发现 pgtbltest 默认就被作为用户程序编译进磁盘。但是 pgtbltest 虽能执行,却会报错,报错如下:
根据手册,scause = 0xd 是 “Load page fault”,缺页错误。说明用户程序访问了一块自己并不拥有的内存。
我们仔细读读 ugetpid() 的实现,如下:
// User memory layout.
// Address zero first:
// text
// original data and bss
// fixed-size stack
// expandable heap
// ...
// USYSCALL (shared with kernel)
// TRAPFRAME (p->trapframe, used by the trampoline)
// TRAMPOLINE (the same page as in the kernel)#define TRAPFRAME (TRAMPOLINE - PGSIZE)
#define USYSCALL (TRAPFRAME - PGSIZE)struct usyscall {int pid; // Process ID
};int
ugetpid(void)
{struct usyscall *u = (struct usyscall *)USYSCALL;return u->pid;
}
结构体 usyscall 其实就是一个 pid 整型,USYSCALL 是一个地址。从代码来看,ugetpid() 其实就是访问了一块虚拟内存。
现在思路大致明了了:在内核里,分配进程的时候,把该进程 pid 映射到该进程的页表上。
先来看分配进程的函数:kernel/proc.c : allocproc()
// Look in the process table for an UNUSED proc.
// If found, initialize state required to run in the kernel,
// and return with p->lock held.
// If there are no free procs, or a memory allocation fails, return 0.
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();p->state = USED;// Allocate a trapframe page.if((p->trapframe = (struct trapframe *)kalloc()) == 0){freeproc(p);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;}// 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;
}
可以看到,函数里分配了 pid,以及页表。我们现在进入分配页表的 proc_pagetable() 函数看看
// Create a user page table for a given process, with no user memory,
// but with trampoline and trapframe pages.
pagetable_t
proc_pagetable(struct proc *p)
{pagetable_t pagetable;// An empty page table.pagetable = uvmcreate();if(pagetable == 0)return 0;// map the trampoline code (for system call return)// at the highest user virtual address.// only the supervisor uses it, on the way// to/from user space, so not PTE_U.if(mappages(pagetable, TRAMPOLINE, PGSIZE,(uint64)trampoline, PTE_R | PTE_X) < 0){uvmfree(pagetable, 0);return 0;}// map the trapframe page just below the trampoline page, for// trampoline.S.if(mappages(pagetable, TRAPFRAME, PGSIZE,(uint64)(p->trapframe), PTE_R | PTE_W) < 0){uvmunmap(pagetable, TRAMPOLINE, 1, 0);uvmfree(pagetable, 0);return 0;}return pagetable;
}
可以看到这里使用 mappages 来映射用户页表。我们在这里加上 pid 的映射应该就行了。
我们依葫芦画瓢加上对 pid 的映射,如下:
#ifdef LAB_PGTBL// 把 USYSCALL 映射到用户页表,加速部分系统调用// 若出错,释放 TRAMPOLINE 和 TRAMPFRAME,再释放页表if(mappages(pagetable, USYSCALL, PGSIZE,(uint64)(&(p->pid)), PTE_R) < 0){uvmunmap(pagetable, TRAMPOLINE, 1, 0);uvmunmap(pagetable, TRAPFRAME, 1, 1);uvmfree(pagetable, 0);return 0;}
#endif
启动 make qemu
,发现报错,如下:
这是在 freewalk 函数中 panic。我们启动 qemu-gdb,对 panic,打断点,可以看到函数调用栈如下:
a
a
TODO: here
TODO: here