Lab 3:Page Table
课程地址:https://pdos.csail.mit.edu/6.828/2020/schedule.html Lab 地址:https://pdos.csail.mit.edu/6.828/2020/labs/pgtbl.html 代码仓库:https://github.com/ajackchan/my-xv6-riscv/tree/pgtbl
Print a page table
编写一个 vmprint()
函数,接收 pagetable_t
参数,递归打印页表的有效条目,包括页表索引、PTE 信息和物理地址,使用缩进表示不同页表级别。在 exec.c
中调用该函数,打印第一个进程的页表,并确保输出格式与样例一致。
1. 在 kernel/defs.h
中添加 vmprint
函数声明 1 2 3 4 5 6 // kernel/defs.h ...... int copyout(pagetable_t, uint64, char *, uint64); int copyin(pagetable_t, char *, uint64, uint64); int copyinstr(pagetable_t, char *, uint64, uint64); int vmprint(pagetable_t pagetable); // 添加函数声明
2. 实现 vmprint()
和 pgtblprint()
函数 我们在 kernel/vm.c
中实现这两个函数,pgtblprint()
函数负责递归遍历三级页表并打印,而 vmprint()
则作为入口调用 pgtblprint()
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 // kernel/vm.c // 递归打印页表的函数,类似于 freewalk,但用于打印页表内容 int pgtblprint(pagetable_t pagetable, int depth) { // 页表中有 2^9 = 512 个 PTE for (int i = 0; i < 512; i++) { pte_t pte = pagetable[i]; // 读取当前页表项 if (pte & PTE_V) { // 如果页表项有效 // 打印页表项,按照要求的格式,先打印缩进 printf(".."); for (int j = 0; j < depth; j++) { printf(" .."); } // 打印页表索引、PTE 值、物理地址 printf("%d: pte %p pa %p\n", i, pte, PTE2PA(pte)); // 如果该 PTE 指向的是下一级页表而非叶子节点,则递归打印子页表 if ((pte & (PTE_R | PTE_W | PTE_X)) == 0) { // 该 PTE 指向下一级页表 uint64 child = PTE2PA(pte); // 获取子页表地址 pgtblprint((pagetable_t)child, depth + 1); // 递归进入子页表 } } } return 0; } // 页表打印函数入口 int vmprint(pagetable_t pagetable) { printf("page table %p\n", pagetable); // 打印页表的基地址 return pgtblprint(pagetable, 0); // 调用递归打印函数,深度从0开始 }
3. 在 exec.c
中调用 vmprint
在 exec.c
的 exec()
函数中,在进程执行后插入对 vmprint()
的调用,以便打印进程的页表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // kernel/exec.c int exec(char *path, char **argv) { // 原有代码部分 ... // 在进程页表初始化之后,返回之前打印页表信息 if (p->pid == 1) { // 只打印第一个进程 (init 进程) 的页表 vmprint(p->pagetable); } return argc; // this ends up in a0, the first argument to main(argc, argv) bad: if (pagetable) proc_freepagetable(pagetable, sz); if (ip) { iunlockput(ip); end_op(); } return -1; }
A kernel page table per process
修改内核,使每一个进程进入内核态后,都能有自己的独立内核页表
1. 在 proc
结构体中添加 kernelpgtbl
字段 修改 kernel/proc.h
,为每个进程添加 kernelpgtbl
字段,用于存储进程的独立内核页表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // kernel/proc.h // Per-process state 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 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) pagetable_t kernelpgtbl; // Kernel page table for the process };
2. 抽象出内核页表的映射逻辑 在 kernel/vm.c
中实现 kvm_map_pagetable
函数,用于将系统所需的固定内存映射到每个进程的内核页表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // kernel/vm.c void kvm_map_pagetable(pagetable_t pgtbl) { // 映射 UART 寄存器 kvmmap(pgtbl, UART0, UART0, PGSIZE, PTE_R | PTE_W); // 映射 virtio 硬盘接口 kvmmap(pgtbl, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W); // 映射 CLINT kvmmap(pgtbl, CLINT, CLINT, 0x10000, PTE_R | PTE_W); // 映射 PLIC kvmmap(pgtbl, PLIC, PLIC, 0x400000, PTE_R | PTE_W); // 映射内核代码区(只读和可执行) kvmmap(pgtbl, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X); // 映射内核数据区(可读写) kvmmap(pgtbl, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W); // 映射 trampoline 用于 trap entry/exit kvmmap(pgtbl, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X); }
3. 实现新的内核页表初始化函数 实现 kvminit_newpgtbl
,用于为每个进程创建新的内核页表:
1 2 3 4 5 6 7 pagetable_t kvminit_newpgtbl() { pagetable_t pgtbl = (pagetable_t) kalloc(); // 分配页表 memset(pgtbl, 0, PGSIZE); // 清空页表 kvm_map_pagetable(pgtbl); // 映射系统所需内存 return pgtbl; }
4. 修改 kvminit
函数 在 kvminit
中调用 kvminit_newpgtbl()
,为内核初始化全局页表:
1 2 3 void kvminit() { kernel_pagetable = kvminit_newpgtbl(); // 创建全局内核页表 }
5. 分配进程的内核页表和内核栈 在 allocproc()
中,为新创建的进程分配独立的内核页表和内核栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 // kernel/proc.c 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(); // 分配 trapframe 页 if((p->trapframe = (struct trapframe *)kalloc()) == 0) { release(&p->lock); return 0; } // 创建用户页表 p->pagetable = proc_pagetable(p); if(p->pagetable == 0){ freeproc(p); release(&p->lock); return 0; } // 创建进程专属内核页表 p->kernelpgtbl = kvminit_newpgtbl(); // 为进程分配内核栈 char *pa = kalloc(); if(pa == 0) panic("kalloc"); uint64 va = KSTACK((int)0); // 固定地址映射 kvmmap(p->kernelpgtbl, va, (uint64)pa, PGSIZE, 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; }
6. 切换进程的内核页表 在调度器 scheduler()
中切换到进程的内核页表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 // kernel/proc.c void scheduler(void) { struct proc *p; struct cpu *c = mycpu(); c->proc = 0; for(;;) { intr_on(); for(p = proc; p < &proc[NPROC]; p++) { acquire(&p->lock); if(p->state == RUNNABLE) { p->state = RUNNING; c->proc = p; // 切换到进程内核页表 w_satp(MAKE_SATP(p->kernelpgtbl)); sfence_vma(); swtch(&c->context, &p->context); // 切换回全局内核页表 kvminithart(); c->proc = 0; } release(&p->lock); } } }
7. 释放进程的内核页表和内核栈 在 freeproc()
函数中,释放进程的内核栈和内核页表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // kernel/proc.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); p->pagetable = 0; // 释放内核栈 void *kstack_pa = (void *)kvmpa(p->kernelpgtbl, p->kstack); kfree(kstack_pa); p->kstack = 0; // 释放进程内核页表 kvm_free_kernelpgtbl(p->kernelpgtbl); p->kernelpgtbl = 0; p->state = UNUSED; }
8. 递归释放页表 实现 kvm_free_kernelpgtbl()
函数,递归释放页表中的所有映射:
1 2 3 4 5 6 7 8 9 10 11 12 13 // kernel/vm.c void kvm_free_kernelpgtbl(pagetable_t pagetable) { for(int i = 0; i < 512; i++) { pte_t pte = pagetable[i]; if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0) { uint64 child = PTE2PA(pte); kvm_free_kernelpgtbl((pagetable_t)child); // 递归释放子页表 pagetable[i] = 0; } } kfree((void*)pagetable); // 释放页表本身 }
9. 修改 virtio_disk_rw
函数 修改 virtio_disk_rw
中的 kvmpa
调用,使用当前进程的 kernelpgtbl
:
1 2 3 4 5 6 7 // virtio_disk.c void virtio_disk_rw(struct buf *b, int write) { // 使用当前进程的内核页表 disk.desc[idx[0]].addr = (uint64) kvmpa(myproc()->kernelpgtbl, (uint64) &buf0); // 其他代码不变 }
Simplify copyin/copyinstr
将 copyin
和 copyinstr
的实现替换为调用新的 copyin_new
和 copyinstr_new
函数,并为每个进程的内核页表添加用户地址映射,确保这些新函数能够正确处理用户指针的解引用。
1. 工具方法的实现 实现 kvmcopymappings
和 kvmdealloc
,用于同步用户页表和内核页表的映射。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 // kernel/vm.c // 将 src 页表的一部分页映射关系拷贝到 dst 页表中。 // 只拷贝页表项,不拷贝实际的物理页内存。 // 成功返回 0,失败返回 -1 int kvmcopymappings(pagetable_t src, pagetable_t dst, uint64 start, uint64 sz) { pte_t *pte; uint64 pa, i; uint flags; for(i = PGROUNDUP(start); i < start + sz; i += PGSIZE) { if((pte = walk(src, i, 0)) == 0) panic("kvmcopymappings: pte should exist"); if((*pte & PTE_V) == 0) panic("kvmcopymappings: page not present"); pa = PTE2PA(*pte); flags = PTE_FLAGS(*pte) & ~PTE_U; // 设置非用户权限 if(mappages(dst, i, PGSIZE, pa, flags) != 0) { goto err; } } return 0; err: uvmunmap(dst, PGROUNDUP(start), (i - PGROUNDUP(start)) / PGSIZE, 0); return -1; } // 将程序内存从 oldsz 缩减到 newsz,用于内核页表内程序内存映射与用户页表程序内存映射之间的同步。 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; }
2. 去除 CLINT 映射 修改 kvm_map_pagetable
函数,去除 CLINT 的映射,并在 kvminit
函数中为全局内核页表单独添加 CLINT 的映射。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 // kernel/vm.c void kvm_map_pagetable(pagetable_t pgtbl) { // uart registers kvmmap(pgtbl, UART0, UART0, PGSIZE, PTE_R | PTE_W); // virtio mmio disk interface kvmmap(pgtbl, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W); // 去掉 CLINT 的映射 // kvmmap(pgtbl, CLINT, CLINT, 0x10000, PTE_R | PTE_W); // PLIC kvmmap(pgtbl, PLIC, PLIC, 0x400000, PTE_R | PTE_W); // 映射内核代码区 kvmmap(pgtbl, KERNBASE, KERNBASE, (uint64)etext - KERNBASE, PTE_R | PTE_X); // 映射内核数据区 kvmmap(pgtbl, (uint64)etext, (uint64)etext, PHYSTOP - (uint64)etext, PTE_R | PTE_W); // 映射 trampoline kvmmap(pgtbl, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X); } void kvminit() { kernel_pagetable = kvminit_newpgtbl(); // 全局内核页表仍然需要映射 CLINT kvmmap(kernel_pagetable, CLINT, CLINT, 0x10000, PTE_R | PTE_W); }
3. 在 exec
中增加内存范围检测 在 exec
函数中增加检测,确保用户程序内存不会超过 PLIC 的地址范围。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 // kernel/exec.c int exec(char *path, char **argv) { // 其他代码省略... // 加载程序到内存 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; // 检查是否超出 PLIC 范围 if (sz1 >= 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; } // 清除旧的内核页表映射,重新映射程序内存到内核页表 uvmunmap(p->kernelpgtbl, 0, PGROUNDUP(oldsz) / PGSIZE, 0); kvmcopymappings(pagetable, p->kernelpgtbl, 0, sz); // 提交新页表 oldpagetable = p->pagetable; p->pagetable = pagetable; p->sz = sz; p->trapframe->epc = elf.entry; p->trapframe->sp = sp; proc_freepagetable(oldpagetable, oldsz); return argc; bad: iunlockput(ip); end_op(); return -1; }
4. 同步页表映射 在 fork()
、growproc()
、userinit()
中同步用户页表和内核页表的映射。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 // kernel/proc.c // fork 中添加映射同步 int fork(void) { // 其他代码省略... if (uvmcopy(p->pagetable, np->pagetable, p->sz) < 0 || kvmcopymappings(np->pagetable, np->kernelpgtbl, 0, p->sz) < 0) { freeproc(np); release(&np->lock); return -1; } np->sz = p->sz; // 其他代码省略... } // growproc 中同步映射变化 int growproc(int n) { uint sz = myproc()->sz; if (n > 0) { uint64 newsz; if ((newsz = uvmalloc(myproc()->pagetable, sz, sz + n)) == 0) return -1; // 内核页表中的映射同步 if (kvmcopymappings(myproc()->pagetable, myproc()->kernelpgtbl, sz, n) != 0) { uvmdealloc(myproc()->pagetable, newsz, sz); return -1; } sz = newsz; } else if (n < 0) { uvmdealloc(myproc()->pagetable, sz, sz + n); sz = kvmdealloc(myproc()->kernelpgtbl, sz, sz + n); } myproc()->sz = sz; return 0; } // userinit 中的映射同步 void userinit(void) { struct proc *p = allocproc(); // 初始化代码,加载用户程序 uvminit(p->pagetable, initcode, sizeof(initcode)); p->sz = PGSIZE; // 同步程序内存映射到进程的内核页表中 kvmcopymappings(p->pagetable, p->kernelpgtbl, 0, p->sz); // 其他代码省略... }
5. 替换 copyin
和 copyinstr
实现 将 copyin
和 copyinstr
实现替换为调用新的 copyin_new
和 copyinstr_new
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 // kernel/vm.c // 新的函数声明 int copyin_new(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len); int copyinstr_new(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max); // 将 copyin 和 copyinstr 替换为新的实现 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); }