Lab10: mmap

本实验是实现一个内存映射文件的功能,将文件映射到内存中,从而在与文件交互时减少磁盘操作。

(1). 根据提示1,首先是配置mmapmunmap系统调用,此前已进行过多次类似流程,不再赘述。在kernel/fcntl.h中定义了宏,只有在定义了LAB_MMAP时这些宏才生效,而LAB_MMAP是在编译时在命令行通过gcc的-D参数定义的

  1. void* mmap(void* addr, int length, int prot, int flags, int fd, int offset);
  2. int munmap(void* addr, int length);

(2). 根据提示3,定义VMA结构体,并添加到进程结构体中

  1. #define NVMA 16
  2. // 虚拟内存区域结构体
  3. struct vm_area {
  4. int used; // 是否已被使用
  5. uint64 addr; // 起始地址
  6. int len; // 长度
  7. int prot; // 权限
  8. int flags; // 标志位
  9. int vfd; // 对应的文件描述符
  10. struct file* vfile; // 对应文件
  11. int offset; // 文件偏移,本实验中一直为0
  12. };
  13. struct proc {
  14. ...
  15. struct vm_area vma[NVMA]; // 虚拟内存区域
  16. }

(3). 在allocproc中将vma数组初始化为全0

  1. static struct proc*
  2. allocproc(void)
  3. {
  4. ...
  5. found:
  6. ...
  7. memset(&p->vma, 0, sizeof(p->vma));
  8. return p;
  9. }

(4). 根据提示2、3、4,参考lazy实验中的分配方法(将当前p->sz作为分配的虚拟起始地址,但不实际分配物理页面),此函数写在sysfile.c中就可以使用静态函数argfd同时解析文件描述符和struct file

  1. uint64
  2. sys_mmap(void) {
  3. uint64 addr;
  4. int length;
  5. int prot;
  6. int flags;
  7. int vfd;
  8. struct file* vfile;
  9. int offset;
  10. uint64 err = 0xffffffffffffffff;
  11. // 获取系统调用参数
  12. if(argaddr(0, &addr) < 0 || argint(1, &length) < 0 || argint(2, &prot) < 0 ||
  13. argint(3, &flags) < 0 || argfd(4, &vfd, &vfile) < 0 || argint(5, &offset) < 0)
  14. return err;
  15. // 实验提示中假定addr和offset为0,简化程序可能发生的情况
  16. if(addr != 0 || offset != 0 || length < 0)
  17. return err;
  18. // 文件不可写则不允许拥有PROT_WRITE权限时映射为MAP_SHARED
  19. if(vfile->writable == 0 && (prot & PROT_WRITE) != 0 && flags == MAP_SHARED)
  20. return err;
  21. struct proc* p = myproc();
  22. // 没有足够的虚拟地址空间
  23. if(p->sz + length > MAXVA)
  24. return err;
  25. // 遍历查找未使用的VMA结构体
  26. for(int i = 0; i < NVMA; ++i) {
  27. if(p->vma[i].used == 0) {
  28. p->vma[i].used = 1;
  29. p->vma[i].addr = p->sz;
  30. p->vma[i].len = length;
  31. p->vma[i].flags = flags;
  32. p->vma[i].prot = prot;
  33. p->vma[i].vfile = vfile;
  34. p->vma[i].vfd = vfd;
  35. p->vma[i].offset = offset;
  36. // 增加文件的引用计数
  37. filedup(vfile);
  38. p->sz += length;
  39. return p->vma[i].addr;
  40. }
  41. }
  42. return err;
  43. }

(5). 根据提示5,此时访问对应的页面就会产生页面错误,需要在usertrap中进行处理,主要完成三项工作:分配物理页面,读取文件内容,添加映射关系

  1. void
  2. usertrap(void)
  3. {
  4. ...
  5. if(cause == 8) {
  6. ...
  7. } else if((which_dev = devintr()) != 0){
  8. // ok
  9. } else if(cause == 13 || cause == 15) {
  10. #ifdef LAB_MMAP
  11. // 读取产生页面故障的虚拟地址,并判断是否位于有效区间
  12. uint64 fault_va = r_stval();
  13. if(PGROUNDUP(p->trapframe->sp) - 1 < fault_va && fault_va < p->sz) {
  14. if(mmap_handler(r_stval(), cause) != 0) p->killed = 1;
  15. } else
  16. p->killed = 1;
  17. #endif
  18. } else {
  19. ...
  20. }
  21. ...
  22. }
  23. /**
  24. * @brief mmap_handler 处理mmap惰性分配导致的页面错误
  25. * @param va 页面故障虚拟地址
  26. * @param cause 页面故障原因
  27. * @return 0成功,-1失败
  28. */
  29. int mmap_handler(int va, int cause) {
  30. int i;
  31. struct proc* p = myproc();
  32. // 根据地址查找属于哪一个VMA
  33. for(i = 0; i < NVMA; ++i) {
  34. if(p->vma[i].used && p->vma[i].addr <= va && va <= p->vma[i].addr + p->vma[i].len - 1) {
  35. break;
  36. }
  37. }
  38. if(i == NVMA)
  39. return -1;
  40. int pte_flags = PTE_U;
  41. if(p->vma[i].prot & PROT_READ) pte_flags |= PTE_R;
  42. if(p->vma[i].prot & PROT_WRITE) pte_flags |= PTE_W;
  43. if(p->vma[i].prot & PROT_EXEC) pte_flags |= PTE_X;
  44. struct file* vf = p->vma[i].vfile;
  45. // 读导致的页面错误
  46. if(cause == 13 && vf->readable == 0) return -1;
  47. // 写导致的页面错误
  48. if(cause == 15 && vf->writable == 0) return -1;
  49. void* pa = kalloc();
  50. if(pa == 0)
  51. return -1;
  52. memset(pa, 0, PGSIZE);
  53. // 读取文件内容
  54. ilock(vf->ip);
  55. // 计算当前页面读取文件的偏移量,实验中p->vma[i].offset总是0
  56. // 要按顺序读读取,例如内存页面A,B和文件块a,b
  57. // 则A读取a,B读取b,而不能A读取b,B读取a
  58. int offset = p->vma[i].offset + PGROUNDDOWN(va - p->vma[i].addr);
  59. int readbytes = readi(vf->ip, 0, (uint64)pa, offset, PGSIZE);
  60. // 什么都没有读到
  61. if(readbytes == 0) {
  62. iunlock(vf->ip);
  63. kfree(pa);
  64. return -1;
  65. }
  66. iunlock(vf->ip);
  67. // 添加页面映射
  68. if(mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)pa, pte_flags) != 0) {
  69. kfree(pa);
  70. return -1;
  71. }
  72. return 0;
  73. }

(6). 根据提示6实现munmap,且提示7中说明无需查看脏位就可写回

  1. uint64
  2. sys_munmap(void) {
  3. uint64 addr;
  4. int length;
  5. if(argaddr(0, &addr) < 0 || argint(1, &length) < 0)
  6. return -1;
  7. int i;
  8. struct proc* p = myproc();
  9. for(i = 0; i < NVMA; ++i) {
  10. if(p->vma[i].used && p->vma[i].len >= length) {
  11. // 根据提示,munmap的地址范围只能是
  12. // 1. 起始位置
  13. if(p->vma[i].addr == addr) {
  14. p->vma[i].addr += length;
  15. p->vma[i].len -= length;
  16. break;
  17. }
  18. // 2. 结束位置
  19. if(addr + length == p->vma[i].addr + p->vma[i].len) {
  20. p->vma[i].len -= length;
  21. break;
  22. }
  23. }
  24. }
  25. if(i == NVMA)
  26. return -1;
  27. // 将MAP_SHARED页面写回文件系统
  28. if(p->vma[i].flags == MAP_SHARED && (p->vma[i].prot & PROT_WRITE) != 0) {
  29. filewrite(p->vma[i].vfile, addr, length);
  30. }
  31. // 判断此页面是否存在映射
  32. uvmunmap(p->pagetable, addr, length / PGSIZE, 1);
  33. // 当前VMA中全部映射都被取消
  34. if(p->vma[i].len == 0) {
  35. fileclose(p->vma[i].vfile);
  36. p->vma[i].used = 0;
  37. }
  38. return 0;
  39. }

(7). 回忆lazy实验中,如果对惰性分配的页面调用了uvmunmap,或者子进程在fork中调用uvmcopy复制了父进程惰性分配的页面都会导致panic,因此需要修改uvmunmapuvmcopy检查PTE_V后不再panic

  1. if((*pte & PTE_V) == 0)
  2. continue;

(8). 根据提示8修改exit,将进程的已映射区域取消映射

  1. void
  2. exit(int status)
  3. {
  4. // Close all open files.
  5. for(int fd = 0; fd < NOFILE; fd++){
  6. ...
  7. }
  8. // 将进程的已映射区域取消映射
  9. for(int i = 0; i < NVMA; ++i) {
  10. if(p->vma[i].used) {
  11. if(p->vma[i].flags == MAP_SHARED && (p->vma[i].prot & PROT_WRITE) != 0) {
  12. filewrite(p->vma[i].vfile, p->vma[i].addr, p->vma[i].len);
  13. }
  14. fileclose(p->vma[i].vfile);
  15. uvmunmap(p->pagetable, p->vma[i].addr, p->vma[i].len / PGSIZE, 1);
  16. p->vma[i].used = 0;
  17. }
  18. }
  19. begin_op();
  20. iput(p->cwd);
  21. end_op();
  22. ...
  23. }

(9). 根据提示9,修改fork,复制父进程的VMA并增加文件引用计数

  1. int
  2. fork(void)
  3. {
  4. // increment reference counts on open file descriptors.
  5. for(i = 0; i < NOFILE; i++)
  6. ...
  7. ...
  8. // 复制父进程的VMA
  9. for(i = 0; i < NVMA; ++i) {
  10. if(p->vma[i].used) {
  11. memmove(&np->vma[i], &p->vma[i], sizeof(p->vma[i]));
  12. filedup(p->vma[i].vfile);
  13. }
  14. }
  15. safestrcpy(np->name, p->name, sizeof(p->name));
  16. ...
  17. }