建议先看一下这个blog,写得很好
https://www.cnblogs.com/huxiao-tee/p/4660352.html
https://juejin.cn/post/7016498891365302302
根据上面解释的

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。 mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同数据不通的繁琐过程。因此mmap效率更高。

  • mmap函数是将文件地址映射到虚拟内存中,返回映射后的地址,同时记录该进程映射到的文件信息。
  • munmap函数就是取消进程地址空间中,文件地址某一部分的映射。

image.png

image.png
为了尽量使得 map 的文件使用的地址空间不要和进程所使用的地址空间产生冲突,我们选择将 mmap 映射进来的文件 map 到尽可能高的位置,也就是刚好在 trapframe 下面。并且若有多个 mmap 的文件,则向下生长。

  1. // kernel/memlayout.h
  2. // map the trampoline page to the highest address,
  3. // in both user and kernel space.
  4. #define TRAMPOLINE (MAXVA - PGSIZE)
  5. // map kernel stacks beneath the trampoline,
  6. // each surrounded by invalid guard pages.
  7. #define KSTACK(p) (TRAMPOLINE - ((p)+1)* 2*PGSIZE)
  8. // User memory layout.
  9. // Address zero first:
  10. // text
  11. // original data and bss
  12. // fixed-size stack
  13. // expandable heap
  14. // ...
  15. // mmapped files
  16. // TRAPFRAME (p->trapframe, used by the trampoline)
  17. // TRAMPOLINE (the same page as in the kernel)
  18. #define TRAPFRAME (TRAMPOLINE - PGSIZE)
  19. // MMAP 所能使用的最后一个页+1
  20. #define MMAPEND TRAPFRAME


接下来定义 vma 结构体,其中包含了 mmap 映射的内存区域的各种必要信息,比如开始地址、大小、所映射文件、文件内偏移以及权限等。
并且在 proc 结构体末尾为每个进程加上 16 个 vma 空槽。

  1. // kernel/proc.h
  2. struct vma {
  3. int valid;
  4. uint64 vastart;
  5. uint64 sz;
  6. struct file *f;
  7. int prot;
  8. int flags;
  9. uint64 offset;
  10. };
  11. #define NVMA 16
  12. // Per-process state
  13. struct proc {
  14. struct spinlock lock;
  15. // p->lock must be held when using these:
  16. enum procstate state; // Process state
  17. struct proc *parent; // Parent process
  18. void *chan; // If non-zero, sleeping on chan
  19. int killed; // If non-zero, have been killed
  20. int xstate; // Exit status to be returned to parent's wait
  21. int pid; // Process ID
  22. // these are private to the process, so p->lock need not be held.
  23. uint64 kstack; // Virtual address of kernel stack
  24. uint64 sz; // Size of process memory (bytes)
  25. pagetable_t pagetable; // User page table
  26. struct trapframe *trapframe; // data page for trampoline.S
  27. struct context context; // swtch() here to run process
  28. struct file *ofile[NOFILE]; // Open files
  29. struct inode *cwd; // Current directory
  30. char name[16]; // Process name (debugging)
  31. struct vma vmas[NVMA]; // virtual memory areas
  32. };

实现 mmap 系统调用。函数原型请参考 man mmap。函数的功能是在进程的 16 个 vma 槽中,找到可用的空槽,并且顺便计算所有 vma 中使用到的最低的虚拟地址(作为新 vma 的结尾地址 vaend,开区间),然后将当前文件映射到该最低地址下面的位置(vastart = vaend - sz)。
最后记得使用 filedup(v->f);,将文件的引用计数增加一。

  1. // kernel/sysfile.c
  2. uint64
  3. sys_mmap(void)
  4. {
  5. uint64 addr, sz, offset;
  6. int prot, flags, fd; struct file *f;
  7. if(argaddr(0, &addr) < 0 || argaddr(1, &sz) < 0 || argint(2, &prot) < 0
  8. || argint(3, &flags) < 0 || argfd(4, &fd, &f) < 0 || argaddr(5, &offset) < 0 || sz == 0)
  9. return -1;
  10. if((!f->readable && (prot & (PROT_READ)))
  11. || (!f->writable && (prot & PROT_WRITE) && !(flags & MAP_PRIVATE)))
  12. return -1;
  13. sz = PGROUNDUP(sz);
  14. struct proc *p = myproc();
  15. struct vma *v = 0;
  16. uint64 vaend = MMAPEND; // non-inclusive
  17. // mmaptest never passed a non-zero addr argument.
  18. // so addr here is ignored and a new unmapped va region is found to
  19. // map the file
  20. // our implementation maps file right below where the trapframe is,
  21. // from high addresses to low addresses.
  22. // Find a free vma, and calculate where to map the file along the way.
  23. for(int i=0;i<NVMA;i++) {
  24. struct vma *vv = &p->vmas[i];
  25. if(vv->valid == 0) {
  26. if(v == 0) {
  27. v = &p->vmas[i];
  28. // found free vma;
  29. v->valid = 1;
  30. }
  31. } else if(vv->vastart < vaend) {
  32. vaend = PGROUNDDOWN(vv->vastart);
  33. }
  34. }
  35. if(v == 0){
  36. panic("mmap: no free vma");
  37. }
  38. v->vastart = vaend - sz;
  39. v->sz = sz;
  40. v->prot = prot;
  41. v->flags = flags;
  42. v->f = f; // assume f->type == FD_INODE
  43. v->offset = offset;
  44. filedup(v->f);
  45. return v->vastart;
  46. }

映射之前,需要注意文件权限的问题,如果尝试将一个只读打开的文件映射为可写,并且开启了回盘(MAP_SHARED),则 mmap 应该失败。否则回盘的时候会出现回盘到一个只读文件的错误情况。
由于需要对映射的页实行懒加载,仅在访问到的时候才从磁盘中加载出来,这里采用和 lab5: Lazy Page Allocation 类似的方式实现。具体请参考 lab5 笔记。

  1. // kernel/trap.c
  2. void
  3. usertrap(void)
  4. {
  5. int which_dev = 0;
  6. // ......
  7. } else if((which_dev = devintr()) != 0){
  8. // ok
  9. } else {
  10. uint64 va = r_stval();
  11. if((r_scause() == 13 || r_scause() == 15)){ // vma lazy allocation
  12. if(!vmatrylazytouch(va)) {
  13. goto unexpected_scause;
  14. }
  15. } else {
  16. unexpected_scause:
  17. printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
  18. printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
  19. p->killed = 1;
  20. }
  21. }
  22. // ......
  23. usertrapret();
  24. }
  1. // kernel/trap.c
  2. void
  3. usertrap(void)
  4. {
  5. int which_dev = 0;
  6. // ......
  7. } else if((which_dev = devintr()) != 0){
  8. // ok
  9. } else {
  10. uint64 va = r_stval();
  11. if((r_scause() == 13 || r_scause() == 15)){ // vma lazy allocation
  12. if(!vmatrylazytouch(va)) {
  13. goto unexpected_scause;
  14. }
  15. } else {
  16. unexpected_scause:
  17. printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
  18. printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
  19. p->killed = 1;
  20. }
  21. }
  22. // ......
  23. usertrapret();
  24. }

到这里应该可以通过 mmap 测试了,接下来实现 munmap 调用,将一个 vma 所分配的所有页释放,并在必要的情况下,将已经修改的页写回磁盘。

  1. // kernel/sysfile.c
  2. uint64
  3. sys_munmap(void)
  4. {
  5. uint64 addr, sz;
  6. if(argaddr(0, &addr) < 0 || argaddr(1, &sz) < 0 || sz == 0)
  7. return -1;
  8. struct proc *p = myproc();
  9. struct vma *v = findvma(p, addr);
  10. if(v == 0) {
  11. return -1;
  12. }
  13. if(addr > v->vastart && addr + sz < v->vastart + v->sz) {
  14. // trying to "dig a hole" inside the memory range.
  15. return -1;
  16. }
  17. uint64 addr_aligned = addr;
  18. if(addr > v->vastart) {
  19. addr_aligned = PGROUNDUP(addr);
  20. }
  21. int nunmap = sz - (addr_aligned-addr); // nbytes to unmap
  22. if(nunmap < 0)
  23. nunmap = 0;
  24. vmaunmap(p->pagetable, addr_aligned, nunmap, v); // custom memory page unmap routine for mmapped pages.
  25. if(addr <= v->vastart && addr + sz > v->vastart) { // unmap at the beginning
  26. v->offset += addr + sz - v->vastart;
  27. v->vastart = addr + sz;
  28. }
  29. v->sz -= sz;
  30. if(v->sz <= 0) {
  31. fileclose(v->f);
  32. v->valid = 0;
  33. }
  34. return 0;
  35. }

这里首先通过传入的地址找到对应的 vma 结构体(通过前面定义的 findvma 方法),然后检测了一下在 vma 区域中间“挖洞”释放的错误情况,计算出应该开始释放的内存地址以及应该释放的内存字节数量(由于页有可能不是完整释放,如果 addr 处于一个页的中间,则那个页的后半部分释放,但是前半部分不释放,此时该页整体不应该被释放)。
计算出来释放内存页的开始地址以及释放的个数后,调用自定义的 vmaunmap 方法(vm.c)对物理内存页进行释放,并在需要的时候将数据写回磁盘。将该方法独立出来并写到 vm.c 中的理由是方便调用 vm.c 中的 walk 方法。
在调用 vmaunmap 释放内存页之后,对 v->offset、v->vastart 以及 v->sz 作相应的修改,并在所有页释放完毕之后,关闭对文件的引用,并完全释放该 vma。

  1. // kernel/vm.c
  2. #include "fcntl.h"
  3. #include "spinlock.h"
  4. #include "sleeplock.h"
  5. #include "file.h"
  6. #include "proc.h"
  7. // Remove n BYTES (not pages) of vma mappings starting from va. va must be
  8. // page-aligned. The mappings NEED NOT exist.
  9. // Also free the physical memory and write back vma data to disk if necessary.
  10. void
  11. vmaunmap(pagetable_t pagetable, uint64 va, uint64 nbytes, struct vma *v)
  12. {
  13. uint64 a;
  14. pte_t *pte;
  15. // printf("unmapping %d bytes from %p\n",nbytes, va);
  16. // borrowed from "uvmunmap"
  17. for(a = va; a < va + nbytes; a += PGSIZE){
  18. if((pte = walk(pagetable, a, 0)) == 0)
  19. panic("sys_munmap: walk");
  20. if(PTE_FLAGS(*pte) == PTE_V)
  21. panic("sys_munmap: not a leaf");
  22. if(*pte & PTE_V){
  23. uint64 pa = PTE2PA(*pte);
  24. if((*pte & PTE_D) && (v->flags & MAP_SHARED)) { // dirty, need to write back to disk
  25. begin_op();
  26. ilock(v->f->ip);
  27. uint64 aoff = a - v->vastart; // offset relative to the start of memory range
  28. if(aoff < 0) { // if the first page is not a full 4k page
  29. writei(v->f->ip, 0, pa + (-aoff), v->offset, PGSIZE + aoff);
  30. } else if(aoff + PGSIZE > v->sz){ // if the last page is not a full 4k page
  31. writei(v->f->ip, 0, pa, v->offset + aoff, v->sz - aoff);
  32. } else { // full 4k pages
  33. writei(v->f->ip, 0, pa, v->offset + aoff, PGSIZE);
  34. }
  35. iunlock(v->f->ip);
  36. end_op();
  37. }
  38. kfree((void*)pa);
  39. *pte = 0;
  40. }
  41. }
  42. }

这里的实现大致上和 uvmunmap 相似,查找范围内的每一个页,检测其 dirty bit (D) 是否被设置,如果被设置,则代表该页被修改过,需要将其写回磁盘。注意不是每一个页都需要完整的写回,这里需要处理开头页不完整、结尾页不完整以及中间完整页的情况。
xv6中本身不带有 dirty bit 的宏定义,在 riscv.h 中手动补齐:

  1. // kernel/riscv.h
  2. #define PTE_V (1L << 0) // valid
  3. #define PTE_R (1L << 1)
  4. #define PTE_W (1L << 2)
  5. #define PTE_X (1L << 3)
  6. #define PTE_U (1L << 4) // 1 -> user can access
  7. #define PTE_G (1L << 5) // global mapping
  8. #define PTE_A (1L << 6) // accessed
  9. #define PTE_D (1L << 7) // dirty

最后需要做的,是在 proc.c 中添加处理进程 vma 的各部分代码。

  • 让 allocproc 初始化进程的时候,将 vma 槽都清空
  • freeproc 释放进程时,调用 vmaunmap 将所有 vma 的内存都释放,并在需要的时候写回磁盘
  • fork 时,拷贝父进程的所有 vma,但是不拷贝物理页 ```cpp // kernel/proc.c

static struct proc* allocproc(void) { // ……

// Clear VMAs for(int i=0;ivmas[i].valid = 0; }

return p; }

// free a proc structure and the data hanging from it, // including user pages. // p->lock must be held. static void freeproc(struct proc p) { if(p->trapframe) kfree((void)p->trapframe); p->trapframe = 0; for(int i = 0; i < NVMA; i++) { struct vma *v = &p->vmas[i]; vmaunmap(p->pagetable, v->vastart, v->sz, v); } 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; p->state = UNUSED; }

// Create a new process, copying the parent. // Sets up child kernel stack to return as if from fork() system call. int fork(void) { // ……

// copy vmas created by mmap. // actual memory page as well as pte will not be copied over. for(i = 0; i < NVMA; i++) { struct vma v = &p->vmas[i]; if(v->valid) { np->vmas[i] = v; filedup(v->f); } }

safestrcpy(np->name, p->name, sizeof(p->name));

pid = np->pid;

np->state = RUNNABLE;

release(&np->lock);

return pid; }

``` 由于 mmap 映射的页并不在 [0, p->sz) 范围内,所以其页表项在 fork 的时候并不会被拷贝。我们只拷贝了 vma 项到子进程,这样子进程尝试访问 mmap 页的时候,会重新触发懒加载,重新分配物理页以及建立映射。
执行结果
image.png

自述

首先我们要定义一个文件映射结构体vma,里面包含了mmap映射的内存区域的各种必要信息,比如开始地址、大小、所映射文件、文件内偏移以及权限等,然后再proc进程的结构体中引入。

实现 mmap 系统调用。函数原型请参考 man mmap。函数的功能是在进程的 16 个 vma 槽中,找到可用的空槽,并且顺便计算所有 vma 中使用到的最低的虚拟地址(作为新 vma 的结尾地址 vaend,开区间),然后将当前文件映射到该最低地址下面的位置(vastart = vaend - sz) 在缺页中断的时候,会去看该va是不是在vma中,va > vma.start && va < vma.start + vma.size,如果是的话,在去kalloc分配物理内存,在调用给定的readi去读取磁盘数据到物理内存当中,再用mappages进行虚实地址的映射。

我们在自己去写一个sys_mmap,去接收测试用例传过来的参数,地址起始位置,size,file结构体等参数,并且在这文件引用计数上加1,根据进程的file去找到对应的文件inode,。