Print a page table
该函数的作用是为了打印当前进程的页表,通过hint可以知道如何得知当前页表的PTE是否指向下一级页表, 写一个很简单的递归函数就行
void _vmprint(pagetable_t pagetable, int level) {
for (int i = 0; i < 512; i++) {
pte_t pte = pagetable[i];
if (pte & PTE_V) {
uint64 pa = PTE2PA(pte);
for (int j = 0; j < level; j++) {
if (j) printf(" ");
printf("..");
}
printf("%d: pte %p pa %p\n", i, pte, pa);
if ((pte & (PTE_R | PTE_W | PTE_X)) == 0) {
_vmprint((pagetable_t)pa, level+1);
}
}
}
}
void vmprint(pagetable_t pagetable) {
printf("page table %p\n", pagetable);
_vmprint(pagetable, 1);
}
A kernel page table per process
这个实验的主要目的就是为每一个进程单独的分配一个内核页表,为下一个做准备
在struct proc中添加一个内核页表字段
// kernel/proc.h
struct proc {
struct spinlock lock;
// p->lock must be held when using these:
enum procstate state; // Process state
struct proc *parent; // Parent process
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID
// these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
pagetable_t kpagetable; // kernel pagetable
struct trapframe *trapframe; // data page for trampoline.S
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)
};
实现一个修改版的kvminit创建核页表,并构建基础映射
//kernel/vm.c
pagetable_t ukvminit() {
pagetable_t kpagetable = (pagetable_t) kalloc();
memset(kpagetable, 0, PGSIZE);
ukvmmap(kpagetable, UART0, UART0, PGSIZE, PTE_R | PTE_W);
ukvmmap(kpagetable, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
ukvmmap(kpagetable, CLINT, CLINT, 0x10000, PTE_R | PTE_W);
ukvmmap(kpagetable, PLIC, PLIC, 0x400000, PTE_R | PTE_W);
ukvmmap(kpagetable, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
ukvmmap(kpagetable, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);
ukvmmap(kpagetable, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
return kpagetable;
}
void ukvmmap(pagetable_t kpagetable, uint64 va, uint64 pa, uint64 sz, int perm) {
if(mappages(kpagetable, va, sz, pa, perm) != 0)
panic("uvmmap");
}
分配自己的内核栈并建立映射
由于所有内核态的页表都共享一个内核页表,对于不同的内核进程,就需要创建不同的内核栈
xv6 在启动过程中,会在 procinit() 中为所有可能的 64 个进程位都预分配好内核栈 kstack,具体为在高地址空间里,每个进程使用一个页作为 kstack,并且两个不同 kstack 中间隔着一个无映射的 guard page 用于检测栈溢出错误。具体参考 xv6 book 的 Figure 3.3
根据hint在proc单独创建一个内核栈并建立映射
// kernel/proc.c
static struct proc *
allocproc(void) {
//......
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));
kvmmap_plus(p->kpagetable, va, (uint64) pa, PGSIZE, PTE_R | PTE_W);
p->kstack = va;
// 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;
}
取消为每个进程但独创建的内核页表
// kernel/proc.c
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();
}
修改scheduler()来加载进程的内核页表到核心的satp寄存器
这里有一个问题就是什么时候要切换satp寄存器呢,根据scheduler注释可知swth函数就直接切换进程了,所以要在这之前切换satp寄存器中的内容
// kernel/proc.c
void
scheduler(void)
{
struct proc *p;
struct cpu *c = mycpu();
c->proc = 0;
for(;;){
// Avoid deadlock by ensuring that devices can interrupt.
intr_on();
int found = 0;
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->kernelpgtbl));
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);
}
#if !defined (LAB_FS)
if(found == 0) {
intr_on();
asm volatile("wfi");
}
#else
;
#endif
}
}
释放内核页表以及内核栈
注意这里将进程的状态设置为UNUSED一定要在清理玩所有数据之后,不然会出现walk错误,一定要注意,之前也是在这里出问题的
static void
freeproc(struct proc *p)
{
if(p->trapframe)
kfree((void*)p->trapframe);
p->trapframe = 0;
// delete user pagetable
if(p->pagetable)
proc_freepagetable(p->pagetable, p->sz);
p->pagetable = 0;
p->sz = 0;
p->pid = 0;
p->parent = 0;
p->name[0] = 0;
p->chan = 0;
p->killed = 0;
p->xstate = 0;
// 释放进程的内核栈
if(p->kstack)
uvmunmap(p->kpagetable, p->kstack, 1, 1);
p->kstack = 0;
if(p->kpagetable) {
proc_freewalk(p->kpagetable);
}
p->kpagetable = 0;
p->state = UNUSED;
}
void proc_freewalk(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);
proc_freewalk((pagetable_t)child);
}
}
}
kfree((void*)pagetable);
}
simplify copyin/copyinstr
在上一个实验中,已经使得每一个进程都拥有独立的内核态页表了,这个实验的目标是,在进程的内核态页表中维护一个用户态页表映射的副本,这样使得内核态也可以对用户态传进来的指针(逻辑地址)进行解引用。这样做相比原来 copyin 的实现的优势是,原来的 copyin 是通过软件模拟访问页表的过程获取物理地址的,而在内核页表内维护映射副本的话,可以利用 CPU 的硬件寻址功能进行寻址,效率更高并且可以受快表加速。
先写一个复制userpage的函数
void u2kvmcopy(pagetable_t upagetable, pagetable_t kpagetable, uint64 oldsz, uint64 newsz) {
oldsz = PGROUNDUP(oldsz);
for (uint64 i = oldsz; i < newsz; i += PGSIZE) {
pte_t* pte_from = walk(upagetable, i, 0);
pte_t* pte_to = walk(kpagetable, i, 1);
if(pte_from == 0) panic("u2kvmcopy: src pte do not exist");
if(pte_to == 0) panic("u2kvmcopy: dest pte walk fail");
uint64 pa = PTE2PA(*pte_from);
// 这里很重要一定要 &(~PTE_U)
uint flag = (PTE_FLAGS(*pte_from)) & (~PTE_U);
*pte_to = PA2PTE(pa) | flag;
}
}
做内存映射
在fork中添加复制函数
int
fork(void)
{
int i, pid;
struct proc *np;
struct proc *p = myproc();
// Allocate process.
if((np = allocproc()) == 0){
return -1;
}
// Copy user memory from parent to child.
if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
freeproc(np);
release(&np->lock);
return -1;
}
np->sz = p->sz;
np->parent = p;
// copy saved user registers.
*(np->trapframe) = *(p->trapframe);
// Cause fork to return 0 in the child.
np->trapframe->a0 = 0;
// increment reference counts on open file descriptors.
for(i = 0; i < NOFILE; i++)
if(p->ofile[i])
np->ofile[i] = filedup(p->ofile[i]);
np->cwd = idup(p->cwd);
// 添加映射
u2kvmcopy(np->pagetable, np->kpagetable, 0, np->sz);
safestrcpy(np->name, p->name, sizeof(p->name));
pid = np->pid;
np->state = RUNNABLE;
release(&np->lock);
return pid;
}
在exec中添加映射以及检查边界是否越界
int
exec(char *path, char **argv)
{
// ....
// Load program into memory.
for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
if(readi(ip, 0, (uint64)&ph, off, sizeof(ph)) != sizeof(ph))
goto bad;
if(ph.type != ELF_PROG_LOAD)
continue;
if(ph.memsz < ph.filesz)
goto bad;
if(ph.vaddr + ph.memsz < ph.vaddr)
goto bad;
uint64 sz1;
if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz)) == 0)
goto bad;
if(sz1 >= PLIC) { // 添加检测,防止程序大小超过 PLIC
goto bad;
}
sz = sz1;
if(ph.vaddr % PGSIZE != 0)
goto bad;
if(loadseg(pagetable, ph.vaddr, ip, ph.off, ph.filesz) < 0)
goto bad;
}
iunlockput(ip);
end_op();
ip = 0;
p = myproc();
uint64 oldsz = p->sz;
// Allocate two pages at the next page boundary.
// Use the second as the user stack.
sz = PGROUNDUP(sz);
uint64 sz1;
if((sz1 = uvmalloc(pagetable, sz, sz + 2*PGSIZE)) == 0)
goto bad;
sz = sz1;
uvmclear(pagetable, sz-2*PGSIZE);
sp = sz;
stackbase = sp - PGSIZE;
u2kvmcopy(pagetable, p->kpagetable, 0, sz);
\\...
在growproc中添加映射并检查是否越界
int
growproc(int n)
{
uint sz;
struct proc *p = myproc();
sz = p->sz;
if(n > 0){
// 检查是否越界
if(PGROUNDUP(sz + n) >= PLIC) return -1;
if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
return -1;
}
// 添加映射
u2kvmcopy(p->pagetable, p->kpagetable, sz - n, sz);
} else if(n < 0){
sz = uvmdealloc(p->pagetable, sz, sz + n);
}
p->sz = sz;
return 0;
}
在userinit中添加映射
对于 init 进程,由于不像其他进程,init 不是 fork 得来的,所以需要在 userinit 中也添加同步映射的代码。
void
userinit(void)
{
struct proc *p;
p = allocproc();
initproc = p;
// allocate one user page and copy init's instructions
// and data into it.
uvminit(p->pagetable, initcode, sizeof(initcode));
p->sz = PGSIZE;
// 添加映射
u2kvmcopy(p->pagetable, p->kpagetable, 0, p->sz);
// prepare for the very first "return" from kernel to user.
p->trapframe->epc = 0; // user program counter
p->trapframe->sp = PGSIZE; // user stack pointer
safestrcpy(p->name, "initcode", sizeof(p->name));
p->cwd = namei("/");
p->state = RUNNABLE;
release(&p->lock);
}