学习资料:
https://pdos.csail.mit.edu/6.828/2020/schedule.html
https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/
https://th0ar.gitbooks.io/xv6-chinese/content/
https://www.bilibili.com/video/BV19k4y1C7kA
参考资料
https://blog.miigon.net/posts/s081-lab3-page-tables/#print-a-page-table-easy
https://zhuanlan.zhihu.com/p/625962093
kern/memlayout.h
,捕获物理内存布局。kern/vm.c
,其中包含大部分虚拟内存 (VM) 代码。kernel/kalloc.c
,其中包含分配和释放物理内存的代码。
Print a page table (easy)
Define a function called vmprint(). It should take a pagetable_t
argument, and print that pagetable in the format described below.
Insert if(p->pid==1) vmprint(p->pagetable) in exec.c just before the
return argc, to print the first process’s page table. You receive full
credit for this assignment if you pass the pte printout test of make
grade.
添加一个打印页表的内核函数,以如如下格式打印出传进的页表
page table 0x0000000087f6e000
..0: pte 0x0000000021fda801 pa 0x0000000087f6a000
.. ..0: pte 0x0000000021fda401 pa 0x0000000087f69000
.. .. ..0: pte 0x0000000021fdac1f pa 0x0000000087f6b000
.. .. ..1: pte 0x0000000021fda00f pa 0x0000000087f68000
.. .. ..2: pte 0x0000000021fd9c1f pa 0x0000000087f67000
..255: pte 0x0000000021fdb401 pa 0x0000000087f6d000
.. ..511: pte 0x0000000021fdb001 pa 0x0000000087f6c000
.. .. ..510: pte 0x0000000021fdd807 pa 0x0000000087f76000
.. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000
RISC-V 的逻辑地址寻址是采用三级页表的形式,9 bit 一级索引找到二级页表,9 bit 二级索引找到三级页表,9 bit 三级索引找到内存页,最低 12 bit 为页内偏移(即一个页 4096 bytes)。具体可以参考 xv6 book 的 Figure 3.2。
本函数需要模拟如上的 CPU 查询页表的过程,对三级页表进行递归,然后按照一定格式输出
我们需要新建一个输出函数,在函数的头文件中进行声明
// 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); // 添加函数声明
因为需要递归打印页表,而 xv6 已经有一个递归释放页表的函数 freewalk(),将其复制一份,并将释放部分代码改为打印即可:
// kernel/vm.c
int pgtblprint(pagetable_t pagetable, int depth) {
// there are 2^9 = 512 PTEs in a page table.
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(" ..");
}
printf("%d: pte %p pa %p\n", i, pte, PTE2PA(pte));
// 如果该节点不是叶节点,递归打印其子节点。
if((pte & (PTE_R|PTE_W|PTE_X)) == 0){
// this PTE points to a lower-level page table.
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);
}
在exec.c中进行调用
// exec.c
int
exec(char *path, char **argv)
{
// ......
vmprint(p->pagetable); // 按照实验要求,在 exec 返回之前打印一下页表。
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;
}
最后进行输出测试
$ ./grade-lab-pgtbl pte printout
make: `kernel/kernel' is up to date.
== Test pte printout == pte printout: OK (1.6s)
A kernel page table per process (hard)
Your first job is to modify the kernel so that every process uses its own copy of the kernel page table when executing in the kernel. Modify struct proc to maintain a kernel page table for each process, and modify the scheduler to switch kernel page tables when switching processes. For this step, each per-process kernel page table should be identical to the existing global kernel page table. You pass this part of the lab if usertests runs correctly.
xv6 原本的设计是,用户进程在用户态使用各自的用户态页表,但是一旦进入内核态(例如使用了系统调用),则切换到内核页表(通过修改 satp 寄存器,trampoline.S)。然而这个内核页表是全局共享的,也就是全部进程进入内核态都共用同一个内核态页表:
本 Lab 目标是让每一个进程进入内核态后,都能有自己的独立内核页表。
创建进程内核页表与内核栈
首先在进程的结构体 proc 中,添加一个 kernelpgtbl,用于存储进程专享的内核态页表。
// 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 (在 proc 中添加该 field)
};
接下来修改 kvminit。内核需要依赖内核页表内一些固定的映射的存在才能正常工作,例如 UART 控制、硬盘界面、中断控制等。而 kvminit 原本只为全局内核页表 kernel_pagetable 添加这些映射。我们抽象出来一个可以为任何我们自己创建的内核页表添加这些映射的函kvm_map_pagetable()。
void kvm_map_pagetable(pagetable_t pgtbl) {
// 将各种内核需要的 direct mapping 添加到页表 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);
// map kernel text executable and read-only.
kvmmap(pgtbl, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
// map kernel data and the physical RAM we'll make use of.
kvmmap(pgtbl, (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.
kvmmap(pgtbl, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
}
pagetable_t
kvminit_newpgtbl()
{
pagetable_t pgtbl = (pagetable_t) kalloc();
memset(pgtbl, 0, PGSIZE);
kvm_map_pagetable(pgtbl);
return pgtbl;
}
/*
* create a direct-map page table for the kernel.
*/
void
kvminit()
{
kernel_pagetable = kvminit_newpgtbl(); // 仍然需要有全局的内核页表,用于内核 boot 过程,以及无进程在运行时使用。
}
// ......
// 将某个逻辑地址映射到某个物理地址(添加第一个参数 pgtbl)
void
kvmmap(pagetable_t pgtbl, uint64 va, uint64 pa, uint64 sz, int perm)
{
if(mappages(pgtbl, va, sz, pa, perm) != 0)
panic("kvmmap");
}
// kvmpa 将内核逻辑地址转换为物理地址(添加第一个参数 kernelpgtbl)
uint64
kvmpa(pagetable_t pgtbl, uint64 va)
{
uint64 off = va % PGSIZE;
pte_t *pte;
uint64 pa;
pte = walk(pgtbl, va, 0);
if(pte == 0)
panic("kvmpa");
if((*pte & PTE_V) == 0)
panic("kvmpa");
pa = PTE2PA(*pte);
return pa+off;
}
kvm_map_pagetable函数必须要在kvminit_newpgtbl函数之前,还有将这些函数在defs文件中进行声明,不然后续运行容易报错。
这里还有一个重要的需要处理,要为每个进程创建都属于自己的内核栈。 原本的 xv6 设计中,所有处于内核态的进程都共享同一个页表,即意味着共享同一个地址空间。由于 xv6 支持多核/多进程调度,同一时间可能会有多个进程处于内核态,所以需要对所有处于内核态的进程创建其独立的内核态内的栈,也就是内核栈,供给其内核态代码执行过程。
xv6 在启动过程中,会在 procinit() 中为所有可能的 64 个进程位都预分配好内核栈 kstack,具体为在高地址空间里,每个进程使用一个页作为 kstack,并且两个不同 kstack 中间隔着一个无映射的 guard page 用于检测栈溢出错误。具体参考 xv6 book 的 Figure 3.3。
在 xv6 原来的设计中,内核页表本来是只有一个的,所有进程共用,所以需要为不同进程创建多个内核栈,并 map 到不同位置(见 procinit() 和 KSTACK 宏)。
而我们的新设计中,每一个进程都会有自己独立的内核页表,并且每个进程也只需要访问自己的内核栈,而不需要能够访问所有 64 个进程的内核栈。所以可以将所有进程的内核栈 map 到其各自内核页表内的固定位置(不同页表内的同一逻辑地址,指向不同物理内存)。
//proc.c
// initialize the proc table at boot time.
void
procinit(void)
{
struct proc *p;
initlock(&pid_lock, "nextpid");
for(p = proc; p < &proc[NPROC]; p++) {
initlock(&p->lock, "proc");
// 这里删除了为所有进程预分配内核栈的代码,变为创建进程的时候再创建内核栈,见 allocproc()
}
kvminithart();
}
然后,在创建进程的时候,为进程分配独立的内核页表,以及内核栈
// 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();
// Allocate a trapframe page.
if((p->trapframe = (struct trapframe *)kalloc()) == 0){
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;
}
// 新加部分 start //
// 为新进程创建独立的内核页表,并将内核所需要的各种映射添加到新页表上
p->kernelpgtbl = kvminit_newpgtbl();
// printf("kernel_pagetable: %p\n", p->kernelpgtbl);
// 分配一个物理页,作为新进程的内核栈使用
char *pa = kalloc();
if(pa == 0)
panic("kalloc");
uint64 va = KSTACK((int)0); // 将内核栈映射到固定的逻辑地址上
// printf("map krnlstack va: %p to pa: %p\n", va, pa);
kvmmap(p->kernelpgtbl, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
p->kstack = va; // 记录内核栈的逻辑地址,其实已经是固定的了,依然这样记录是为了避免需要修改其他部分 xv6 代码
// 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;
}
到这里进程独立的内核页表就创建完成了,但是用户进程进入内核态后依然会使用全局共享的内核页表,因此还需要在 scheduler() 中进行相关修改。
切换到进程内核页表
在调度器将 CPU 交给进程执行之前,切换到该进程对应的内核页表:
// 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
}
}
这样子,每个进程执行的时候,就都会在内核态采用自己独立的内核页表了。
// 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;
p->sz = 0;
p->pid = 0;
p->parent = 0;
p->name[0] = 0;
p->chan = 0;
p->killed = 0;
p->xstate = 0;
// 释放进程的内核栈
void *kstack_pa = (void *)kvmpa(p->kernelpgtbl, p->kstack);
// printf("trace: free kstack %p\n", kstack_pa);
kfree(kstack_pa);
p->kstack = 0;
// 注意:此处不能使用 proc_freepagetable,因为其不仅会释放页表本身,还会把页表内所有的叶节点对应的物理页也释放掉。
// 这会导致内核运行所需要的关键物理页被释放,从而导致内核崩溃。
// 这里使用 kfree(p->kernelpgtbl) 也是不足够的,因为这只释放了**一级页表本身**,而不释放二级以及三级页表所占用的空间。
// 递归释放进程独享的页表,释放页表本身所占用的空间,但**不释放页表指向的物理页**
kvm_free_kernelpgtbl(p->kernelpgtbl);
p->kernelpgtbl = 0;
p->state = UNUSED;
}
kvm_free_kernelpgtbl() 用于递归释放整个多级页表树,也是从 freewalk() 修改而来。
// kernel/vm.c
// 递归释放一个内核页表中的所有 mapping,但是不释放其指向的物理页
void
kvm_free_kernelpgtbl(pagetable_t pagetable)
{
// there are 2^9 = 512 PTEs in a page table.
for(int i = 0; i < 512; i++){
pte_t pte = pagetable[i];
uint64 child = PTE2PA(pte);
if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){ // 如果该页表项指向更低一级的页表
// 递归释放低一级页表及其页表项
kvm_free_kernelpgtbl((pagetable_t)child);
pagetable[i] = 0;
}
}
kfree((void*)pagetable); // 释放当前级别页表所占用空间
}
注意到我们的修改影响了其他代码: virtio 磁盘驱动 virtio_disk.c 中调用了 kvmpa() 用于将虚拟地址转换为物理地址,这一操作在我们修改后的版本中,需要传入进程的内核页表。对应修改即可。
// virtio_disk.c
#include "proc.h" // 添加头文件引入
// ......
void
virtio_disk_rw(struct buf *b, int write)
{
// ......
disk.desc[idx[0]].addr = (uint64) kvmpa(myproc()->kernelpgtbl, (uint64) &buf0); // 调用 myproc(),获取进程内核页表
// ......
}
进行最后测试
./grade-lab-pgtbl usertests
riscv64-unknown-elf-objdump -S kernel/kernel > kernel/kernel.asm
riscv64-unknown-elf-objdump -t kernel/kernel | sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$/d' > kernel/kernel.sym
== Test usertests == (229.8s)
== Test usertests: copyin ==
usertests: copyin: OK
== Test usertests: copyinstr1 ==
usertests: copyinstr1: OK
== Test usertests: copyinstr2 ==
usertests: copyinstr2: OK
== Test usertests: copyinstr3 ==
usertests: copyinstr3: OK
== Test usertests: sbrkmuch ==
usertests: sbrkmuch: OK
== Test usertests: all tests ==
usertests: all tests: OK
难点
难点一:释放页表的时候没有释放干净,要进行递归释放,
不能使用proc_freepagetable函数,这个不仅会释放页表,页表对应的叶节点物理页也会被释放
难点2:修改kvminit函数时,使用了kvminit_newpgtbl,这个函数要在后面才出现,要放在上面先声明一下才可以使用或者调换顺序
难点3:kvminit_newpgtbl和kvm_free_kernelpgtbl这些自己创建的函数要在内核的头文件defs.h里面声明。
simplify copyin/copyinstr
,我们需要保证每个进程的pagetable和kernelpgtbl的前半段映射一直保持一致, 这样我们才能在切入内核态时直接使用硬件支持的虚拟/物理内存地址寻址. 按照实验手册的提醒, 在fork(), sbrk(), exec()这些使得进程的pagetable发生增长/缩减的地方, 我们需要将kernelpgtbl与之进行同步更新.
首先我们写一个helper函数, 来将一段内存映射从pagetable复制到kpagetable.
# kernel/vm.c
// Same as mappages without panic on remapping
// 和mappages一模一样, 只不过不再panic remapping, 直接强制复写
int umappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm) {
uint64 a, last;
pte_t *pte;
a = PGROUNDDOWN(va);
last = PGROUNDDOWN(va + size - 1);
for(;;){
if((pte = walk(pagetable, a, 1)) == 0)
return -1;
*pte = PA2PTE(pa) | perm | PTE_V;
if(a == last)
break;
a += PGSIZE;
pa += PGSIZE;
}
return 0;
}
// copying from old page to new page from
// begin in old page to new in old page
// and mask off PTE_U bit
// 将从begin到end的虚拟地址的映射, 从oldpage复制到newpage
int
pagecopy(pagetable_t oldpage, pagetable_t newpage, uint64 begin, uint64 end) {
pte_t *pte;
uint64 pa, i;
uint flags;
begin = PGROUNDUP(begin);
for (i = begin; i < end; i += PGSIZE) {
if ((pte = walk(oldpage, i, 0)) == 0)
panic("pagecopy walk oldpage nullptr");
if ((*pte & PTE_V) == 0)
panic("pagecopy oldpage pte not valid");
pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte) & (~PTE_U); // 把U flag抹去
if (umappages(newpage, i, PGSIZE, pa, flags) != 0) {
goto err;
}
}
return 0;
err:
uvmunmap(newpage, 0, i / PGSIZE, 1);
return -1;
}
紧接着, 我们在fork(), exec(), sbrk() 和userinit()的相应位置进行pagetable和kernelpgtbl的同步.
fork()
# kernel/proc.c
// Create a new process, copying the parent.
// Sets up child kernel stack to return as if from fork() system call.
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;
}
np->sz = p->sz;
if (pagecopy(np->pagetable, np->kernelpgtbl, 0, np->sz) != 0) {
freeproc(np);
release(&np->lock);
return -1;
}
np->parent = p;
...
return pid;
}
exec()
# kernel/exec.c
int
exec(char *path, char **argv)
{
...
// Commit to the user image.
oldpagetable = p->pagetable;
p->pagetable = pagetable;
p->sz = sz;
p->trapframe->epc = elf.entry; // initial program counter = main
p->trapframe->sp = sp; // initial stack pointer
proc_freepagetable(oldpagetable, oldsz);
// 复制新的kernel page并刷新TLB
if (pagecopy(p->pagetable, p->kernelpgtbl, 0, p->sz) != 0) {
goto bad;
}
// 因为load进来了新的program, 刷新一下内存映射
w_satp(MAKE_SATP(p->kernelpgtbl));
sfence_vma();
...
return argc; // this ends up in a0, the first argument to main(argc, argv)
}
sbrk()
# kernel/proc.c
// Grow or shrink user memory by n bytes.
// Return 0 on success, -1 on failure.
int
growproc(int n)
{
uint sz;
struct proc *p = myproc();
sz = p->sz;
if(n > 0){
// 内核页的虚拟地址不能溢出PLIC
if (sz + n > PLIC || (sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
return -1;
}
if (pagecopy(p->pagetable, p->kernelpgtbl, p->sz, sz) != 0) {
// 增量同步[old size, new size]
return -1;
}
} else if(n < 0){
sz = uvmdealloc(p->pagetable, sz, sz + n);
if (sz != p->sz) {
// 缩量同步[new size, old size]
uvmunmap(p->kernelpgtbl, PGROUNDUP(sz), (PGROUNDUP(p->sz) - PGROUNDUP(sz)) / PGSIZE, 0);
}
}
ukvminithard(p->kernelpgtbl);
p->sz = sz;
return 0;
}
在创建第一个进程的时候也要设置映射userinit()
# kernel/proc.c
// Set up first user process.
void
userinit(void)
{
...
uvminit(p->pagetable, initcode, sizeof(initcode));
p->sz = PGSIZE;
pagecopy(p->pagetable, p->kernelpgtbl, 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
...
}
替换 copyin、copyinstr 实现
// 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);
}
然后想要满分的话,还要加一个answers-pgtbl.txt文件。
编译后进行lab批分, 顺利通过.
vagrant@developer:/vagrant/xv6-labs/xv6-labs-2020$ make grade
== Test pte printout ==
$ make qemu-gdb
pte printout: OK (27.3s)
== Test answers-pgtbl.txt == answers-pgtbl.txt: OK
== Test count copyin ==
$ make qemu-gdb
count copyin: OK (3.8s)
== Test usertests ==
$ make qemu-gdb
(219.0s)
== Test usertests: copyin ==
usertests: copyin: OK
== Test usertests: copyinstr1 ==
usertests: copyinstr1: OK
== Test usertests: copyinstr2 ==
usertests: copyinstr2: OK
== Test usertests: copyinstr3 ==
usertests: copyinstr3: OK
== Test usertests: sbrkmuch ==
usertests: sbrkmuch: OK
== Test usertests: all tests ==
usertests: all tests: OK
== Test time ==
time: OK
Score: 66/66
这个实验感觉是目前为止花费时间最多,做起来感觉最难的了,simplify copyin/copyinstr这块刚开始打算直接for循环然后加判断做的,发现走不通,后来看了其他大佬的,才算是勉强完成了,先继续往前走了. 之后学到后面对xv6有更深刻的理解后再回来看看。