MIT 6.S081 lab10
感觉这个 lab
还是挺有意思的,但是感觉没有前面几个 lab
难,感觉前面几个 lab
的难度太大了,这个 lab
花了2
天左右就完成了,当前中间还是有些点比较有疑问的问题,后续还需要进一步来思考的空间。在这个 lab
中的学习内容比较多,主要学习了以下几个知识重点:
mmap
的实现原理:如果仅仅实现mmap
的话,原理比较简单。每次发起系统调用mmap
时,就会在该进程的内核空间中申请一块内存并与之对应WMA AREA
相对应,并更新当前进程的页表,如果我们使用lazy alloc
,此时并不会马上更新物理内存的内容,也并未申请实际的物理页与之相对应。- 当进程读取到该
VMA
对应的内存区域时,会发生缺页中断,当进程捕获到该中断时会检测缺页的地址是否属于某个VMA
,如果该地址合法则申请新的物理页并将其进行mapping
到VMA
给定的地址空间中。同时将给定的fd
中对应的内容读取到该物理页中,此时对于用户空间的应用则可以读取到相应的fd
对应的内容,我们可以将同一个fd
中对应的内容map
到多个进程中。实际上每个fd
可能对应的是某个文件或者设备,我们如果将其映射到内存的地址空间中就可以很方便和快速的进行读写与共享。 - 当我们试图对
VMA
对应的内存进行修改时,修改的内容暂时保存在物理内存中,比如可能多个应用程序对某个文件进行多次写操作,但这些执行都保存在内存中并未实际写入到文件中,当我们执行unmap
操作时,操作系统才可能将这些修改内容一次性的全部写入到文件中,这是一种非常高效的共享机制。 - 使得用户空间的程序能够处理进程捕获的
page fault
,这就需要每个用户进程在内核空间存在一个对应的内核线程来传递这些trap
。用户空间中能够处理这样trap
,并同时能够对物理页进行操纵。用户空间的handler
处理完成后,再返回内核。
重要的是本章节提到了 Baker's Real-Time Copying Garbage Collector
的运行工作原理:
当然其中的原理还是挺复杂,需要专门的一个章节来完成。
lab10 mmap
本章的 lab
比较起来就不算是特别难的 lab
了。但是 lab
后面的 Optional challenges
难度就非常大了,可能需要花费非常多的时间来完成了。所有的源代码都放在github。
题目
The mmap and munmap system calls allow UNIX programs to exert detailed control over their address spaces. They can be used to share memory among processes, to map files into process address spaces, and as part of user-level page fault schemes such as the garbage-collection algorithms discussed in lecture. In this lab you'll add mmap and munmap to xv6, focusing on memory-mapped files. |
思考
mmap
的实现: 在这个lab
中mmap
的实现就非常简单了,我们首先观察一下该函数的模型void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);- 它接受
6
个参数:addr
指定的映射地址;length
指定的映射的内存长度;prot
指定的读写权限;flags
指定的该映射的模式,如果为MAP_SHARED
表示映射的内存会回写到指定的fd
中, 还有其他的标志位;fd
指定的文件的描述符;offset
指定的文件的偏移量;
给定的 lab
只要求完成基本的功能即可,即不考虑文件的 偏移量
并且不指定映射的地址空间,实际我们实现时只需要在该进程的虚拟地址空间末尾增减 length
长度,然后将其新增加的尾部地址空间映射到指定 fd
上即可,实现非常简单了。里面有几个难点如下:
首先我们直接将该进程的地址空间增减
length
长度,当然这里涉及到内存对齐的问题,这是由于进程每次只能向内核申请以物理页为最小单位的内存,所以这里需要处理涉及到内存对齐的问题,即映射的地址空间的起始位置要对齐PG_SIZE
。我们可以参考cow lab
中的实现,直接将内存空间增大即可。第二步我们需要在该进程中记录映射的区域,即所有的
vma
,由于给定的lab
中最大的vma
只有16
个,我们可以采用数组或者链表来实现,实现的linux
实现就非常复杂了,实际处理应该是用的reb-black tree
来记录所有的vma
,因为涉及到vma
的查找,此时利用高效的二分搜索才能提高查找效率。第三步,比较重要的是我们要完成真正的映射,前面提到过我们仅仅增加了进程的地址空间但并未分配实际的物理页,当存在某些程序读取到该未分配的地址空间时就会触发缺页中断,此时我们就需要在
user trap
中进行处理,此时我们就查找该进程的vma
,查找缺页的地址是否在vma
记录中,- 如果在
vma
中,则我们从堆中申请一个新的物理页,并从vma
中记录的fd
中读取一个页长度的内容到新加入的物理页中。此时需要注意的是我们计算读取内从的offset
,稍微需要一点技巧。 - 如果该地址不在
vma
中,则将直接返回缺页中断。
- 如果在
以上即完成了基本的
map
功能。munmap
的实现: 首先我们观察一下munmap
的函数原型如下:munmap(addr, length)
给定的参数只有两个指定的地址与长度,我们在进行
umap
操作时,首先会计算物理页的长度,由于题目给定的限定条件为umanp
操作只会从起始地址到结束地址依次进行umap
而不会存在从中间进行umap
的操作,因此就比较容易实现。- 首先我们从
vma
中查找给定的addr
是否合法,如果合法则将其从中去除,并查看该vma
的对应的flag
,如果flag
为MAP_SHARED
则将其写回到文件中,当然这里实际应该需要处理一下文件的offset
的问题,存在一定的瑕疵。 - 上述操作完成后,则将该段地址对应的物理页内存全部释放掉。当然我比较异疑惑的一点,对应的物理内存页释放掉,但是进程的地址空间实际没有进行变化,按道理此时进程的地址空间也应该释放掉一部分,否则每次访问该对应的地址都会出现
page fault
的问题。
- 首先我们从
对于
fork
子进程时,子进程创建时需要复制父进程的vma
表项,当进程退出进行exit
时需要释放vma
,并将其所有的内容可以写回需要写回到文件中。
代码
增加
sys_map
与sys_umap
的系统调用。static uint64 (*syscalls[])(void) = {
[SYS_fork] sys_fork,
[SYS_exit] sys_exit,
[SYS_wait] sys_wait,
[SYS_pipe] sys_pipe,
[SYS_read] sys_read,
[SYS_kill] sys_kill,
[SYS_exec] sys_exec,
[SYS_fstat] sys_fstat,
[SYS_chdir] sys_chdir,
[SYS_dup] sys_dup,
[SYS_getpid] sys_getpid,
[SYS_sbrk] sys_sbrk,
[SYS_sleep] sys_sleep,
[SYS_uptime] sys_uptime,
[SYS_open] sys_open,
[SYS_write] sys_write,
[SYS_mknod] sys_mknod,
[SYS_unlink] sys_unlink,
[SYS_link] sys_link,
[SYS_mkdir] sys_mkdir,
[SYS_close] sys_close,
[SYS_mmap] sys_mmap,
[SYS_munmap] sys_munmap
};vma
结构的定义如下,并在proc
结构体中增加vma
表项,每次vma
都对应记录地址映射的内容。
/* vam entry */ |
sys_map
与sys_umap
的实现
int map_readfile(int vma_offset, int offset, uint64 addr, int length)
{
struct proc * p = myproc();
struct file *pf = p->vma[vma_offset].pf;
int ret = 0;
ilock(pf->ip);
ret = readi(pf->ip, 0, addr, offset, length);
iunlock(pf->ip);
return ret;
}
int map_writefile(struct file *pf, int offset, uint64 addr, int length)
{
int ret = 0;
begin_op();
ilock(pf->ip);
ret = writei(pf->ip, 1, addr, offset, length);
ilock(pf->ip);
end_op();
return ret;
}
int map_vma_init(struct vmaEntry *vma) {
for (int i = 0; i < MAX_ENTRY_SIZE; i++) {
vma[i].valid = 0;
}
return 0;
}
int map_vma_copy(struct vmaEntry *src, struct vmaEntry *dst) {
for (int i = 0; i < MAX_ENTRY_SIZE; i++) {
if (src[i].valid) {
dst[i].addr = src[i].addr;
dst[i].length = src[i].length;
dst[i].permissions = src[i].permissions;
dst[i].flag = src[i].flag;
dst[i].pf = src[i].pf;
dst[i].offset = src[i].offset;
dst[i].valid = 1;
dst[i].pf = filedup(dst[i].pf);
} else {
dst[i].valid = 0;
}
}
return 0;
}
int map_vma_query(uint64 va) {
struct proc *p = myproc();
for (int i = 0; i < MAX_ENTRY_SIZE; i++) {
if (p->vma[i].valid) {
int offset = va - p->vma[i].addr;
if (offset >= 0 && offset < p->vma[i].length) {
return i;
}
}
}
return -1;
}
int map_vma_add(uint64 addr, struct file *f, int len, int prot, int flag, int offset) {
struct proc * p = myproc();
/* record the vma of the current process */
f->ref++;
for (int i = 0; i < MAX_ENTRY_SIZE; i++) {
if (!p->vma[i].valid) {
p->vma[i].valid = 1;
p->vma[i].addr = addr;
p->vma[i].length = len;
p->vma[i].permissions = prot;
p->vma[i].flag = flag;
p->vma[i].pf = f;
p->vma[i].offset = offset & ~(PGSIZE - 1);
break;
}
}
return 0;
}
int map_vma_remove(int vma_offset) {
struct proc *p = myproc();
/* remove all the pages from process */
if ((p->vma[vma_offset].permissions & PROT_WRITE) && p->vma[vma_offset].flag == MAP_SHARED) {
filewrite(p->vma[vma_offset].pf, p->vma[vma_offset].addr, p->vma[vma_offset].length);
}
uvmunmap(p->pagetable, p->vma[vma_offset].addr, p->vma[vma_offset].length / PGSIZE, 0);
/* remove all mapped pages */
p->vma[vma_offset].pf->ref--;
p->vma[vma_offset].valid = 0;
return 0;
}
int map_unmap(uint64 addr, int length)
{
int vma_offset = map_vma_query(addr);
if (vma_offset == -1) {
return -1;
}
struct proc *p = myproc();
/* remove all the pages from process */
if (p->vma[vma_offset].flag == MAP_SHARED) {
filewrite(p->vma[vma_offset].pf, p->vma[vma_offset].addr, p->vma[vma_offset].length);
}
uvmunmap(p->pagetable, p->vma[vma_offset].addr, length / PGSIZE, 0);
p->vma[vma_offset].length -= length;
p->vma[vma_offset].addr += length;
/* remove all mapped pages */
if (p->vma[vma_offset].length == 0) {
p->vma[vma_offset].pf->ref--;
p->vma[vma_offset].valid = 0;
}
return 0;
}
/* map files into process address spaces */
uint64
sys_mmap(void)
{
uint64 addr;
int len;
int prot;
int flag;
int offset;
int fd;
struct file *f;
/* parse syscall args */
if(argaddr(0, &addr) < 0 || argint(1, &len) < 0 || argint(2, &prot) < 0 || \
argint(3, &flag) < 0 || argint(4, &fd) < 0 || argint(5, &offset) < 0) {
return -1;
}
/* fd to file struct */
if(fd < 0 || fd >= NOFILE || (f = myproc()->ofile[fd]) == 0) {
return -1;
}
/* we check the file readable and writeable */
if (f->writable == 0 && flag == MAP_SHARED && (prot & PROT_WRITE)) {
return -1;
}
/* we add the map address at the end of virtual address */
addr = myproc()->sz;
struct proc * p = myproc();
if(len > 0){
if(p->sz + len > MAXVA) {
return -1;
}
p->sz += len;
}
map_vma_add(addr, f, len, prot, flag, offset);
return addr;
}
/* unmap files into process address spaces */
uint64
sys_munmap(void)
{
uint64 addr;
int len;
/* parse syscall args */
if(argaddr(0, &addr) < 0 || argint(1, &len) < 0) {
return -1;
}
/* we add the map address at the end of virtual address */
int vma_offset = map_vma_query(addr);
if (vma_offset == -1) {
return -1;
}
/* remove all the pages from process */
map_unmap(addr, len);
return 0;
}
```
4. `usertrap` 的实现:需要检测缺页中断,并去按照不同的缺页中断去处理,主要检测缺页的地址是否属于 `vma` 表项。
```C++
uint64 pcode = r_scause();
uint64 va = PGROUNDDOWN(r_stval());
// uint64 epc = r_sepc();
// page fault, valid virtual address, 记录trap发生的原因
if (pcode == 15 || pcode == 13) {
if(va >= p->sz || va <= p->trapframe->sp || va > MAXVA) {
p->killed = 1;
goto fail;
}
int vma_offset = map_vma_query(va);
if (vma_offset >= 0) {
// alloc a page
char *mem = kalloc();
if(mem == 0){
p->killed = 1;
goto fail;
}
memset(mem, 0, PGSIZE);
int offset = p->vma[vma_offset].offset + PGROUNDDOWN(va - p->vma[vma_offset].addr);
printf("get mmap page fault! va = %p\n", va);
map_readfile(vma_offset, offset, (uint64)mem, PGSIZE);
// mapper a page for va
if(mappages(p->pagetable,PGROUNDDOWN(va), PGSIZE, (uint64)mem, (p->vma[vma_offset].permissions << 1) | PTE_U) != 0){
kfree(mem);
printf("get mmap page fault! va = %p\n", va);
p->killed = 1;
goto fail;
}
// p->trapframe->epc += epc;
}
}else {
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
p->killed = 1;
}fork
操作时同时复制vma
表项。map_vma_init(np->vma);
map_vma_copy(p->vma, np->vma);
for (int i = 0; i < 16; i++) {
if (p->vma[i].valid) {
np->vma[i].pf = filedup(p->vma[i].pf);
}
}exit
操作时,同时移除vma
表项。// remove all map virtual address
for (int i = 0; i < 16; i++) {
if (p->vma[i].valid) {
map_vma_remove(i);
}
}
总结
总的来说这个 lab
还是非常有挑战性的,是个好的 lab
,用户空间的 page fault
处理确实是个非常有效的利器,后期会继续深入学习以下 GC
的处理流程。
欢迎关注和打赏,感谢支持!
- 关注我的博客: http://mikemeng.org/
- 关注我的知乎:https://www.zhihu.com/people/da-hua-niu
- 关注我的微信公众号: 公务程序猿
扫描二维码,分享此文章