一 虚拟内存与页表
操作系统为每一个用户进程分配了虚拟的地址空间,每个进程根据自己的地址空间进行寻址,从而确保了进程与进程之间相互隔离。
寻址的过程通过页表来完成,页表的任务是实现虚拟地址到物理地址的映射,由硬件实现。页表分为单级页表与多级页表,单级页表非常庞大,占用内存空间。因此提出了多级页表的概念,多级页表将虚拟地址空间进一步划分,每一级页表的PPN指向了下一级页表目录的物理地址,最后一级页表则是指向了目标的物理地址,这样做可以减少不必要的页表目录在内存中的驻留,从而提高了内存空间的利用率。
通过多级页表进行寻址,需要多次访问内存空间,寻址的效率比较低下。TLB(translation look-aside buffer)的提出可以提高寻址的效率,TLB是一个记录PTE的缓存,记录了最近使用的虚拟地址到物理地址的映射,对于TLB中存在的PTE,不再需要访问页表来获得物理地址,从而提高了寻址的效率。
RISC-V使用的是三级页表,64位的地址空间,主要的性质如下:
- 每一个page directory的大小是一个page的大小,4KB
- 一个PTE的大小是64位,一个page directory有512个PTE
- The root page directory 的物理地址存放在寄存器satp中,每次CPU切换进程,会将进程的页表首地址存放到satp中
二 实验部分
2.1 print a page table
该实验部分要实现一个页表打印的功能,传入一个页表,打印出页表中有效的PTE对应的PTE值和PA值。递归遍历页表的PTE即可,代码如下。
void
vmprint(pagetable_t pagetable){
static int level=1;
pte_t pte;
if(level==1)
printf("page table %p\n",pagetable);
for(int i=0;i<512;i++){
if(pagetable[i]&PTE_V){
for(int j=0;j<level;j++){
if(j!=0) printf(" ");
printf("..");
}
pte=PTE2PA(pagetable[i]);
printf("%d: pte %p pa %p\n",i,pagetable[i],pte);
if(level!=3){
level++;
vmprint((pagetable_t)pte);
level--;
}
}
}
}
2.2 kernel pagetable per process
为每一个进程分配一个kernel pagetable,目的是为了当传入一个user address,kernel可以直接解引用user address。kernel地址空间示意图如下。
- kernel address space 是一种direct-map,物理地址和虚拟地址是一样的。
- trampoline和kstack映射在内核地址空间高位,并在每一个page下设置一个invalid的Guard page,确保不会overflow。
- 从0到0x0C000000(PLIC)是user address的mapping,这个映射由下一题实现。
1 在struct proc中添加kernel_pagetable 的变量
... ...
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
pagetable_t kernel_pagetable;
2 创建一个kernel_pagetable
//create kernel pagetable
pagetable_t
ukvminit()
{
pagetable_t pagetable = (pagetable_t) kalloc();
memset(pagetable, 0, PGSIZE);
// uart registers
ukvmmap(pagetable,UART0, UART0, PGSIZE, PTE_R | PTE_W);
// virtio mmio disk interface
ukvmmap(pagetable,VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
// CLINT
ukvmmap(pagetable,CLINT, CLINT, 0x10000, PTE_R | PTE_W);
// PLIC
ukvmmap(pagetable,PLIC, PLIC, 0x400000, PTE_R | PTE_W);
// map kernel text executable and read-only.
ukvmmap(pagetable,KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
// map kernel data and the physical RAM we'll make use of.
ukvmmap(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.
ukvmmap(pagetable,TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
return pagetable;
}
//实现虚拟地址到物理地址在pagetable的映射
void
ukvmmap(pagetable_t pagetable,uint64 va,uint64 pa,uint64 sz,int perm)
{
if(mappages(pagetable,va,sz,pa,perm)!=0)
panic("ukvmmap");
}
3 在allocproc中为进程创建kernel pagetable 并分配kernel stack
p->kernel_pagetable=ukvminit();
// 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));
ukvmmap(p->kernel_pagetable,va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
p->kstack = va;
4 进程调度时,将其kernel pagetable放到satp寄存器中,没有进程在cpu运行时,satp存放的是global kernel pagetable
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;
w_satp(MAKE_SATP(p->kernel_pagetable));
sfence_vma();
swtch(&c->context, &p->context);
kvminithart();
// Process is done running for now.
// It should have changed its p->state before coming back.
c->proc = 0;
found = 1;
}
release(&p->lock);
}
5 释放进程的kernel pagetable
//释放为进程分配的kernel stack的物理地址
if(p->kstack){
pte_t* pte=walk(p->kernel_pagetable,p->kstack,0);
kfree((void*)PTE2PA(*pte));
}
p->kstack=0;
//释放进程的kernel pagetable,但不释放页表指向的物理地址
if(p->kernel_pagetable)
freeprockvm(p->kernel_pagetable);
p->kernel_pagetable=0;
void
freeprockvm(pagetable_t pagetable){
for(int i=0;i<512;i++){
pte_t pte=pagetable[i];
if(pte&PTE_V){
pagetable[i]=0;
if((pte&(PTE_R|PTE_W|PTE_X))==0){
uint64 child=PTE2PA(pte);
freeprockvm((pagetable_t)child);
}
}
}
kfree((void*)pagetable);
}
2.3 Simplify copyin/copyinstr
接上一题,将user address映射到进程的kernel pagetable中,实现kernel直接解引用user address的目的。
1 user address 分配地址时,对应的kernel pagetable 也要添加相应的映射
uint64
kvmalloc(pagetable_t user,pagetable_t kernel, uint64 oldsz, uint64 newsz)
{
uint64 a;
if(newsz < oldsz)
return oldsz;
if(newsz>PLIC)
return -1;
oldsz = PGROUNDUP(oldsz);
for(a = oldsz; a < newsz; a += PGSIZE){
pte_t *pte1=walk(user,a,0);
pte_t *pte2=walk(kernel,a,1);
*pte2=(*pte1&(~PTE_U));
}
return newsz;
}
2 user address释放地址,对应的kernel pagetable 释放对应的映射
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;
}
3 fork创建了一个与父进程相同的子进程,将子进程的user address映射到其kernel pagetable中
kvmalloc(np->pagetable,np->kernel_pagetable,0,np->sz);
4 sbrk()调用growproc(),为user address分配和释放地址空间
int
growproc(int n)
{
uint sz;
struct proc *p = myproc();
sz = p->sz;
if(n > 0){
if(n+sz>PLIC)
return -1;
if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
return -1;
}
kvmalloc(p->pagetable,p->kernel_pagetable,p->sz,p->sz+n);
} 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;
}
5 exec将原来的user address替换成新的,因此需要对进程原来的mapping进行释放,再添加新的mapping
//unmap previous user mapping in kernel pagetable
uvmunmap(p->kernel_pagetable,0,PGROUNDUP(p->sz)/PGSIZE,0);
//add user mapping into kernel pagetable
if(kvmalloc(pagetable,p->kernel_pagetable,0,sz)==-1)
goto bad;
6 userinit中初始化第一个用户进程,添加mapping
//add user mapping into kernel pagetable
kvmalloc(p->pagetable,p->kernel_pagetable,0,PGSIZE);
7 simplify copyin and copyinstr
// Copy from user to kernel.
// Copy len bytes to dst from virtual address srcva in a given page table.
// Return 0 on success, -1 on error.
int
copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
{
return copyin_new(pagetable,dst,srcva,len);
}
// Copy a null-terminated string from user to kernel.
// Copy bytes to dst from virtual address srcva in a given page table,
// until a '\0', or max.
// Return 0 on success, -1 on error.
int
copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max)
{
return copyinstr_new(pagetable,dst,srcva,max);
}
2.4 实验结果