实验一 Print a page table (easy)
实验一其实比较简单,就是根据输出的格式要去打印pagetable中有效的页表项,一个dfs就可以解决了。
根据提示,首先在kernel / exec.c文件中添加对页表打印mprint方法的调用。
int
exec(char *path, char **argv)
{
// ...
if(p->pid==1)
vmprint(p->pagetable);
return argc; // this ends up in a0, the first argument to main(argc, argv)
}
然后在kernel / vm.c中添加vmprint方法
void dfs(pagetable_t pagetable, int level)
{
if(level < 0)
return;
pte_t pte;
for(int i = 0;i < 512; i ++){
if(pagetable[i] & PTE_V){
for(int j = 2; j >= level; j --){
if(j != 2) printf(" ");
printf("..");
}
pte = PTE2PA(pagetable[i]);
printf("%d: pte %p pa %p\n",i,pagetable[i],pte);
dfs((pagetable_t) pte, level - 1);
}
}
}
void
vmprint(pagetable_t pagetable){
printf("page table %p\n",pagetable);
dfs(pagetable, 2);
}
最后在kernel / defs.h中添加vmprint的声明
void vmprint(pagetable_t);
实验二 A kernel page table per process (hard)
实验二是为了给每个进程添加一个内核页表,其实一开始在写这个实验的时候一直没有理解为什么要这样,后来到实验三才明白,实验一和实验二都是在为实验三铺路。
首先为了给每个进程添加一个单独的内核页表,在proc结构体中添加kernel_pagetable属性
struct proc {
// ...
pagetable_t pagetable; // User page table
pagetable_t kernel_pagetable;
// ...
};
在kernel / vm.c 中仿照kvminit()函数添加一个创建内核页表的函数,并在defs.h中添加函数声明(PS:这里没有映射CLINT暂时未知,根据实验三的提示,内核态的虚拟地址是从PLIC开始的,没有映射CLINT)
// vm.c
pagetable_t
create_kpagetable()
{
pagetable_t pagetable = (pagetable_t) kalloc();
memset(pagetable, 0, PGSIZE);
// uart registers
mappages(pagetable, UART0, PGSIZE, UART0, PTE_R | PTE_W);
// virtio mmio disk interface
mappages(pagetable, VIRTIO0, PGSIZE, VIRTIO0, PTE_R | PTE_W);
// PLIC
mappages(pagetable,PLIC, 0x400000, PLIC, PTE_R | PTE_W);
// map kernel text executable and read-only.
mappages(pagetable,KERNBASE, (uint64)etext-KERNBASE, KERNBASE, PTE_R | PTE_X);
// map kernel data and the physical RAM we'll make use of.
mappages(pagetable,(uint64)etext, PHYSTOP-(uint64)etext, (uint64)etext, PTE_R | PTE_W);
// map the trampoline for trap entry/exit to
// the highest virtual address in the kernel.
mappages(pagetable,TRAMPOLINE, PGSIZE, (uint64)trampoline, PTE_R | PTE_X);
return pagetable;
}
// defs.h
pagetable_t create_kpagetable(void);
然后在allocproc函数中,该函数是初始化所有进程的函数,所以在函数需要添加对每个进程内核页表的初始化,即对create_kpagetable调用,另外每个进程的内核栈也改为在allocproc函数中实现。
static struct proc*
allocproc(void)
{
// ...
p->pagetable = proc_pagetable(p);
if(p->pagetable == 0){
freeproc(p);
release(&p->lock);
return 0;
}
p->kernel_pagetable = create_kpagetable();
if(p->pagetable == 0){
freeproc(p);
release(&p->lock);
return 0;
}
char *pa = kalloc();
if(pa == 0)
panic("kalloc");
uint64 va = KSTACK((int) (p - proc));
mappages(p->kernel_pagetable, va, PGSIZE, (uint64)pa, PTE_R | PTE_W);
p->kstack = va;
memset(&p->context, 0, sizeof(p->context));
p->context.ra = (uint64)forkret;
p->context.sp = p->kstack + PGSIZE;
return p;
}
void
procinit(void)
{
struct proc *p;
initlock(&pid_lock, "nextpid");
for(p = proc; p < &proc[NPROC]; p++) {
initlock(&p->lock, "proc");
}
}
然后在进程切换时,同时更改satp寄存器中的值,即每个进程的内核页表地址,根据提示,没有进程运行时scheduler(),
应当使用kernel_pagetable
void
scheduler(void)
{
// ...
p->state = RUNNING;
c->proc = p;
w_satp(MAKE_SATP(p->kernel_pagetable));
sfence_vma();
swtch(&c->context, &p->context);
kvminithart();
// ...
}
最后在释放进程时,需要同时释放内核页表和内核栈
// defs.h
void freewalk(pagetable_t pagetable);
// vm.c
static void
freeproc(struct proc *p)
{
if(p->trapframe)
kfree((void*)p->trapframe);
p->trapframe = 0;
if(p->pagetable)
proc_freepagetable(p->pagetable, p->sz);
if(p->kstack)
uvmunmap(p->kernel_pagetable, p->kstack, 1, 1);
if(p->kernel_pagetable)
proc_freekpagetable(p->kernel_pagetable, p->sz);
p->kstack = 0;
p->kernel_pagetable=0;
// ...
}
void
proc_freekpagetable(pagetable_t pagetable, uint64 sz)
{
uvmunmap(pagetable, UART0, 1, 0);
uvmunmap(pagetable, VIRTIO0, 1, 0);
uvmunmap(pagetable, PLIC, PGROUNDUP(0x400000)/PGSIZE, 0);
uvmunmap(pagetable, KERNBASE, PGROUNDUP((uint64)etext-KERNBASE)/PGSIZE, 0);
uvmunmap(pagetable, (uint64)etext, PGROUNDUP(PHYSTOP-(uint64)etext)/PGSIZE, 0);
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
//uvmunmap(pagetable, 0, PGROUNDUP(sz) / PGSIZE, 0);
freewalk(pagetable);
}
实验三 Simplify copyin/copyinstr(hard)
其实实验三才是本次实验的想要添加的功能,因为内核空间的虚拟地址和用户空间的虚拟地址不在同一个页表中,所以用户空间的虚拟地址,在内核空间下是无法直接使用的,必须先根据用户页表转化为物理地址,因此我们需要在内核态将用户态的虚拟地址空间在内核页表中也做映射,这样我们就可以在内核态可以直接访问用户态的地址。
首先在vm.c中添加用户态虚拟地址映射复制到内核页表的函数,同时在defs.h中添加函数的声明
// defs.h
int kvmcopy(pagetable_t, pagetable_t, uint64, uint64);
// vm.c
int
kvmcopy(pagetable_t user,pagetable_t kernel, uint64 oldsz, uint64 newsz)
{
pte_t *from, *to;
if(newsz > PLIC)
return -1;
oldsz = PGROUNDUP(oldsz);
for(uint64 i = oldsz; i < newsz; i += PGSIZE){
from = walk(user, i, 0);
if(!(*from & PTE_V))
panic("kvmcopy: from pte is not valid");
to = walk(kernel, i, 1);
*to = *from & (~PTE_U);
}
return 0;
}
同时添加取消映射的函数,这在之后的步骤中会用到
// defs.h
uint64 kvmdealloc(pagetable_t, uint64, uint64)
// vm.c
uint64
kvmdealloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz)
{
if(newsz >= oldsz)
return oldsz;
if(PGROUNDUP(newsz) < PGROUNDUP(oldsz)){
int npages = (PGROUNDUP(oldsz) - PGROUNDUP(newsz)) / PGSIZE;
uvmunmap(pagetable, PGROUNDUP(newsz), npages, 0);
}
return newsz;
}
根据提示,只有在exec(),fork(),sbrk()三个函数中,以及第一次创建的进程userinit中,在内核页表中添加用户页表项的映射
- exec
该方法是用户态进程调用exec系统调用执行特定程序的方法,需要注意的是这里需要释放原内核页表中用户页表映射表项
// exec.c
int
exec(char *path, char **argv)
{
// ...
safestrcpy(p->name, last, sizeof(p->name));
//unmap previous user mapping in kernel pagetable
uvmunmap(p->kernel_pagetable, 0, PGROUNDUP(p->sz) / PGSIZE, 0);
//add user mapping into kernel pagetable
if(kvmcopy(pagetable, p->kernel_pagetable, 0, sz) < 0)
goto bad;
// Commit to the user image.
oldpagetable = p->pagetable;
// ...
}
2. fork
// proc.c
int
fork(void)
{
// ...
// Copy user memory from parent to child.
if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
freeproc(np);
release(&np->lock);
return -1;
}
if(kvmcopy(np->pagetable, np->kernel_pagetable, 0, p->sz) < 0){
freeproc(np);
release(&np->lock);
return -1;
}
np->sz = p->sz;
// ...
}
3. sbrk
sys_sbrk本质上执行的是 proc.c / growproc函数
int
growproc(int n)
{
uint sz;
struct proc *p = myproc();
sz = p->sz;
if(n > 0){
if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
return -1;
}
if(kvmcopy(p->pagetable, p->kernel_pagetable, p->sz, p->sz + n) < 0) {
return -1;
}
} else if(n < 0){
sz = uvmdealloc(p->pagetable, sz, sz + n);
kvmdealloc(p->kernel_pagetable, p->sz, p->sz + n);
}
p->sz = sz;
return 0;
}
最后在释放内核页表时,添加上释放用户页表表项的过程
void
proc_freekpagetable(pagetable_t pagetable, uint64 sz)
{
uvmunmap(pagetable, UART0, 1, 0);
uvmunmap(pagetable, VIRTIO0, 1, 0);
uvmunmap(pagetable, PLIC, PGROUNDUP(0x400000)/PGSIZE, 0);
uvmunmap(pagetable, KERNBASE, PGROUNDUP((uint64)etext-KERNBASE)/PGSIZE, 0);
uvmunmap(pagetable, (uint64)etext, PGROUNDUP(PHYSTOP-(uint64)etext)/PGSIZE, 0);
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, 0, PGROUNDUP(sz) / PGSIZE, 0);
freewalk(pagetable);
}
更换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);
}
总节
在本次实验中踩了不少啃,包括不限于在改变释放进程所占用内存freeproc()函数时,因为入参p不是当前cpu中正在运行的函数,所以在释放内核页表中用户页表部分使用了myproc()->sz,导致没有用户页表部分没有释放全,所以在freewalk中报错;另外实验二一开始没有理解实验的意图,导致去除了全局内核页表而无法触发时钟中断,另外还有一些比较傻的错误。本次实验加深了用户页表和内核页表的理解以及各自地址空间内容的理解,并且在跟代码的过程中,对risc-v的一些非通用寄存器有了些理解。
另外不理解的地方还有 1. 为什么每个进程中的内核页表不需要映射CLINT 2.时钟中断是如何触发的,以及触发后的过程。