Lab3 page tables
https://blog.csdn.net/lllllllllljg/article/details/137727416,参考了很多这篇
在这个实验中,你将探索页表并对其进行修改,以简化从用户空间复制数据到内核空间的功能。
在开始编码之前,请阅读 xv6 书籍的第 3 章,以及相关文件:
kern/memlayout.h
,该文件捕获了内存的布局。kern/vm.c
,该文件包含大部分虚拟内存 (VM) 的代码。kernel/kalloc.c
,该文件包含分配和释放物理内存的代码。
要转换到新分支
$ git fetch
$ git checkout pgtbl
$ make clean
Print a page table(easy)
目的
定义一个名为 vmprint()
的函数。该函数应接受一个 pagetable_t
类型的参数,并按照下面描述的格式打印该页表。
在 exec.c
中,在 return argc
之前插入 if(p->pid==1) vmprint(p->pagetable)
,以打印第一个进程的页表。如果你通过了 make grade
的页面表打印测试,就可以获得这项作业的全部分数
提示
- 你可以将
vmprint()
放在kernel/vm.c
中。 - 使用
kernel/riscv.h
文件末尾的宏。 freewalk
函数可能会对你有所启发。- 在
kernel/defs.h
中定义vmprint
的原型,以便从exec.c
调用它。 - 使用
%p
来打印出完整的 64 位十六进制 PTE 和地址,如示例所示。
做法
先看一下打印的样例。
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
开头是页表的起始地址page table 0x0000000087f6e000,然后后面格式就是pte值和指向的物理地址pa。
然后显然是多级页表,举个例子逐层分析。就举第二行… …0: pte 0x0000000021fda401 pa 0x0000000087f69000。他的pa显然是下一层的页表项指向的物理页框。
根据提示前三条,在 kernel/vm.c
中编写vmprint,显然是一个递归函数。
void vmprint_helper(pagetable_t pagetable, int level)
{for(int i=0;i<512;i++){pte_t pte = pagetable[i];// PTE是否有效if(pte & PTE_V){// 打印前面 .和当前pteprintf("..");for(int j=1;j<level;j++)printf(" ..");printf("%d: pte %p pa %p\n",i,pte,PTE2PA(pte));// 判断是否不为最低级的目录pagetable_t child = (pagetable_t)PTE2PA(pte);if ((pte & (PTE_R | PTE_W | PTE_X)) == 0) { vmprint_helper(child, level + 1);}}}
}
void vmprint(pagetable_t pagetable)
{// 根打印printf("page table %p\n",pagetable);// 递归打印vmprint_helper(pagetable,1);
}
然后去defs.h里添加定义
void vmprint(pagetable_t pagetable)
最后在启动的exec.c文件中启动函数里添加打印,就在最后return argc前添加。
if(p->pid==1)vmprint(p->pagetable);
测试
./grade-lab-pgtbl pte
通过
A kernel page table per process (hard)
目的
背景
Xv6内核有一个单一的内核页表,当内核执行时会使用它。这个内核页表是对物理地址的直接映射,因此内核虚拟地址x映射到物理地址x。
Xv6还为每个进程的用户地址空间维护了一个单独的页表,该页表仅包含该进程用户内存的映射,从虚拟地址0开始。因为内核页表不包含这些映射,所以用户地址在内核中是无效的。
因此,当内核需要使用通过系统调用传递的用户指针(例如,传递给
write()
的缓冲区指针)时,内核必须先将指针转换为物理地址。本节和下一节的目标是允许内核直接解引用用户指针。
第一个任务是修改内核,使得每个进程在执行内核代码时使用它自己的内核页表。
-
修改
struct proc
,为每个进程维护一个内核页表 -
修改调度程序,在切换进程时切换内核页表。
在这一步中,每个进程的内核页表应该与现有的全局内核页表完全相同。如果
usertests
能够正确运行,说明你已经通过了这一部分的任务。
提示
-
给
struct proc
添加一个字段,用于存储进程的内核页表。 -
为新进程创建内核页表的一个合理方法是实现一个修改版的
kvminit
函数,使其创建一个新的页表,而不是修改kernel_pagetable
。你需要在allocproc
中调用这个函数。 -
确保每个进程的内核页表中包含该进程的内核栈映射。在未经修改的 xv6 中,所有的内核栈是在
procinit
中设置的。你需要将其中的一部分或全部功能迁移到allocproc
中。 -
修改
scheduler()
函数,使其在加载进程时将进程的内核页表加载到核心的satp
寄存器中(参考kvminithart
)。在调用w_satp()
后,不要忘记调用sfence_vma()
。 -
当没有进程在运行时,
scheduler()
应该使用kernel_pagetable
。在
freeproc
中释放进程的内核页表。你需要一个方法来释放页表,而不同时释放页表中的叶节点物理内存页。调试页表时,
vmprint
可能会很有用。你可以修改 xv6 的函数或添加新函数;你可能至少需要修改
kernel/vm.c
和kernel/proc.c
中的内容。(但不要修改kernel/vmcopyin.c
、kernel/stats.c
、user/usertests.c
和user/stats.c
。)缺少页表映射可能会导致内核发生页故障。它会打印一个错误信息,其中包含
sepc=0x00000000XXXXXXXX
。你可以通过在kernel/kernel.asm
中查找XXXXXXXX
来找到故障发生的位置。
做法
虽然不明白为什么每个程序的页表都要有一个内核表,先做吧。
根据第一个和第二个提示,应该能明白大概思路。先再pro.c中的的pro结构体添加字段pagetable_t kpt;然后再vm.c中实现一个修改版的 kvminit
函数,使其创建一个新的页表。然后逻辑根kvmint一样,但注意要重新定义一下knmmap,它是直接对全局内核页表操作的。
void proc_kvmmap(pagetable_t kpt, uint64 va, uint64 pa, uint64 sz, int perm){if(mappages(kpt, va, sz, pa, perm) != 0)panic("proc_kvmmap");
}
pagetable_t proc_kpt_init()
{// 先申请一个pagetablepagetable_t kpt = (pagetable_t) kalloc();memset(kpt, 0, PGSIZE);// uart registersproc_kvmmap(kpt,UART0, UART0, PGSIZE, PTE_R | PTE_W);// virtio mmio disk interfaceproc_kvmmap(kpt,VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);// CLINTproc_kvmmap(kpt,CLINT, CLINT, 0x10000, PTE_R | PTE_W);// PLICproc_kvmmap(kpt,PLIC, PLIC, 0x400000, PTE_R | PTE_W);// map kernel text executable and read-only.proc_kvmmap(kpt,KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);// map kernel data and the physical RAM we'll make use of.proc_kvmmap(kpt,(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.proc_kvmmap(kpt,TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);return kpt;
}
函数定义完后记得把函数声明添加到defs.h
文件中。然后在proc.c中 allocproc
中调用这个函数。注意第三条提示,参考procinit函数,因为还要内核页表中包含该进程的内核栈映射。
static struct proc*
allocproc(void)
{......// An empty user page table.p->pagetable = proc_pagetable(p);if(p->pagetable == 0){freeproc(p);release(&p->lock);return 0;}p->kpt = proc_kpt_init();// 申请内核栈,确保每一个进程的内核页表都关于该进程的内核栈有一个映射char *pa = kalloc();if(pa == 0)panic("kalloc");uint64 va = KSTACK((int) (p - proc));proc_kvmmmap(p->kpt, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);p->kstack = va;......
}
然后是完成切换进程时的功能。详情看第4和第5条。
// vm.c中添加并记得添加到def.h中声明
void proc_kvminithart(pagetable_t kpt)
{w_satp(MAKE_SATP(kpt));sfence_vma();
}// 然后在proc.c中scheduler()函数中调用
void
scheduler(void)
{
......p->state = RUNNING;c->proc = p;// 加载进程的内核页表到核心的satp寄存器proc_kvminithart(p->kpt);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;
......}
然后是freeproc,在vm.c添加free_proc_kpt,参照freewalk,记得在def.h中添加声明,最后记得在proc.c中freeproc函数中添加使用
// vm.c
void
free_proc_kpt(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];if(pte & PTE_V){// this PTE points to a lower-level page table.uint64 child = PTE2PA(pte);pagetable[i] = 0;if((pte & (PTE_R|PTE_W|PTE_X)) == 0){// 说明不是第三级,进行递归free_proc_kpt((pagetable_t)child);}} }kfree((void*)pagetable);
}// proc.c
static void
freeproc(struct proc *p)
{
......// 释放一个进程的内核栈if(p->kstack){uvmunmap(p->kpt, p->kstack, 1, 1);}p->kstack = 0;// 释放内核页表free_proc_kpt(p->kpt);p->kpt = 0;p->sz = 0;
......
}
测试
./grade-lab-pgtbl usertests
然后报了些错,就是一些字母多打了或少打了和def.h少填了一个声明。
然后一直启动不了,尝试make qemu,报错panic: virtio_disk_intr status
详情看这篇https://blog.csdn.net/lllllllllljg/article/details/137727416
然后就通过了
Simplify copyin/copyinstr(hard)
目的
背景:在内核中,
copyin
函数通过用户指针指向的内存读取数据。它通过将用户指针转换为内核可以直接解引用的物理地址来实现这一点。具体来说,它通过软件遍历进程的页表完成这种转换。
在这个实验部分,任务是将用户地址的映射添加到每个进程的内核页表中(这些页表是在上一部分中创建的),从而使 copyin
(以及相关的字符串函数 copyinstr
)能够直接解引用用户指针。
你需要将 kernel/vm.c
中 copyin
的主体替换为对 kernel/vmcopyin.c
中定义的 copyin_new
的调用;对 copyinstr
和 copyinstr_new
也做相同的替换。你需要为用户地址添加映射到每个进程的内核页表,以确保 copyin_new
和 copyinstr_new
能够正常工作。如果 usertests
正确运行且所有 make grade
测试通过,你就完成了这个任务。
plus:该方案依赖于用户虚拟地址范围与内核用于其自身指令和数据的虚拟地址范围不重叠。在 xv6 中,用户地址空间的虚拟地址从零开始,而内核的内存虚拟地址从较高的地址开始。然而,这种方案限制了用户进程的最大大小不能超过内核最低虚拟地址。在内核引导后,该地址在 xv6 中是 0xC000000
,即 PLIC(平台级中断控制器)寄存器的地址;参考 kernel/vm.c
中的 kvminit()
、kernel/memlayout.h
和《xv6 文本》第 3-4 图。你需要修改 xv6 以防止用户进程的大小超过 PLIC 地址。
提示
-
首先替换
copyin()
为对copyin_new
的调用,并确保其工作,然后再处理copyinstr
。 -
在每个内核改变进程用户映射的地方,同步修改进程的内核页表。这些地方包括
fork()
、exec()
和sbrk()
。 -
不要忘记在
userinit
中将第一个进程的用户页表包含到其内核页表中。 -
不要忘记上述提到的 PLIC 限制。
做法
大概读一下目的,用copyin_new 来取代copyin,用copyinstr_new取代copyinstr,其中需要在修改原用户页表时也同步修改新建的内核页表。
首先是按照第一个提示替换
// 先得在def.h中引入两个替换函数的定义
// vmcopyin.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中原代码注释掉,并调用替换后的代码
// 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);
}
然后在fork()
、exec()
和 sbrk()
这三个地方,同步修改进程的内核页表。这里明确,是因为进程切换了,所以要实现将用户空间的映射添加到每个进程的内核页表。这里仿照uvmcopy()函数,而且记得在defs.h中添加声明。
void
u2k_vmcopy(pagetable_t pagetable, pagetable_t kpt, uint64 oldsz, uint64 newsz){pte_t *pte_from;pte_t *pte_to;// 将 oldsz 向上对齐到页面边界,确保从对齐后的地址开始进行遍历。oldsz = PGROUNDUP(oldsz);// 遍历用户空间的虚拟地址for(uint64 i = oldsz; i < newsz; i += PGSIZE){if((pte_from = walk(pagetable, i, 0)) == 0)panic("u2k_vmcopy: pte should exist");if((pte_to = walk(kpt, i, 1)) == 0){panic("u2k_vmcopy: pte walk fail");}// 在内核模式下,无法访问设置了PTE_U的页面,// 所以接下来要获得pagetable中虚拟地址为i的pte的标志位uint64 pa = PTE2PA(*pte_from);uint flags = (PTE_FLAGS(*pte_from)) & (~PTE_U);*pte_to = PA2PTE(pa) | flags;}
}
然后实在三个函数中调用
// exec.c
int
exec(char *path, char **argv)
{
......stackbase = sp - PGSIZE;// 添加复制逻辑u2k_vmcopy(pagetable, p->kpt, 0, sz);// Push argument strings, prepare rest of stack in ustack.for(argc = 0; argv[argc]; argc++) {......
}
// proc.c
int
fork(void)
{
......np->sz = p->sz;u2k_vmcopy(np->pagetable, np->kpt, 0, np->sz);np->parent = p;
......
}
// sbrk()->sys_sbrk()->growproc(), 在kernel/sysproc.c里面找到sys_sbrk(void),可以知道只有growproc是负责将用户内存增加或缩小 n 个字节。在内存增加时,需要判断一下会不会超过PLIC限制,不超过再复制一份映射到内核页表.
// proc.c
int
growproc(int n)
{uint sz;struct proc *p = myproc();sz = p->sz;if(n > 0){// 加上PLIC限制if(PGROUNDUP(sz+n) >= PLIC){return -1;}if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {return -1;}// 复制一份到内核页表u2k_vmcopy(p->pagetable, p->kpt, sz - n, sz);} else if(n < 0){sz = uvmdealloc(p->pagetable, sz, sz + n);}p->sz = sz;return 0;}
最后是userinit
// proc.c
void
userinit(void)
{
......p->sz = PGSIZE;u2k_vmcopy(p->pagetable, p->kpt, 0, p->sz);// prepare for the very first "return" from kernel to user.
......
}
测试
./grade-lab-pgtbl usertests
然后通过了。