MIT 6.S081入门lab10 mmap

MIT 6.S081入门lab10 mmap

一、参考资料阅读与总结

1.Journaling the Linux ext2fs Filesystem

  • 文件系统可靠性: 磁盘崩溃前数据的稳定性;故障模式的可预测性;操作的原子性
    -论文核心: 将日志事务系统加入Linux的文件系统中;
  • 事务系统的要求: 元数据的更新;事务系统的顺序性;数据块写入磁盘的同一性(即保证之前的事务都完成了写入)
  • 文件系统相对于数据库系统的不同点: 没有事务终止;事务生命周期较短
  • 复合事务: (组提交)
  • 日志内容: 元数据;描述符;头标记块
  • 检查点设置: 创建新事务;刷新磁盘;等待文件系统完成操作;写入日志;提交磁盘;释放日志
  • 事务提交时文件系统的更新机制: 由于提交事务和文件系统更新是异步的,因此存在事务-文件系统的冲突提交问题,即新文件系统需要对正在提交的事务进行操作;解决方法是使用副本,保证刷新的一致性
  • 未来工作: 压缩日志更新;快速NFS支持

2. Virtual memory for applications

  • 核心动机: 用户级的程序是否能够和os一样,使用虚拟内存,即响应缺页错误
  • 需要的原语: Trap;Prot1和Unprot(访问权限);ProtN(批量操作节省TLB刷新);Dirty(脏页标记);Map2(多对一映射);
  • 现有机制: mmap和unmap(文件映射);mprotect(权限保护);sigaction(信号处理)
  • 实现方式: 虚拟机制:pagetable + virtual memory area;陷阱:User-level traps

二、涉及函数

  • 整体xv6的函数已经基本介绍完成,请参考之前的实现

三、课程视频观看笔记(lec16/lec17)

lec16

= 日志系统的广泛应用: 数据库,操作系统等,其主要目的是实现存储的崩溃恢复;

  • xv6日志系统回顾: begin_op() -> end_op() -> bcache buffer -> log -> loc(写入header头) -> write
    • 预写规则: 先写入更新到日志,再更新(更新的原子性的保证);
    • 释放规则: 只有在日志中所有内容被完全写入相对应的数据块中时,其日志缓存才能被释放;
  • xv6方案的问题: 速度缓慢, end_op()d的系统调用只能同步的发生,同时,每个块写入两次(日志+数据块)
  • ext3方案: 对无日志系统的修改升级(ext2) ,内存中:cache;事务块编号;硬件中:目录树+log
    主要区别,其能够同时进行多事务的提交和运行
  • ext3的日志系统:日志超级快(第一个有效事务的偏移量+序列号)+ 事务(块编号+块事务+提交块)...+ 事务块;
    • 细节: 描述符以魔数开始
  • ext3高性能核心: 异步系统调用;批处理;并发
    • 异步系统调用:
      即系统调用只到缓存更新,其可以允许I/O复用,同时能更好的实现批处理
      风险:崩溃可能导致数据的丢失 -> fsync调用保证及时写入(数据敏感应用)
    • 批处理:
      在任何时候,ext3中存在打开的一个大型的系统调用,在固定时间更新这个大的系统调用,从而实现批处理;
      允许写吸收(便于大型和重复系统调用,减少系统调用);
      便于磁盘调度(顺序写 + 便于磁盘系统调用)
    • 并发:
      允许多个系统调用并行: 由于系统调用是异步的,因此不必等待I/O完成
      允许多个日志事务(状态)存在: 提交中事务,写入事务,打开中事务等事务的不同状态共存,使用了COW策略实现了副本之间的复用
  • 源码结构:
    • 系统调用:开始并获取事务句柄-> 获取buffer -> 对buffer进行操作 -> 关闭事务句柄
    • 提交事务(内核后台线程实现): 阻塞新系统调用 -> 等待未完成系统调用 -> 打开新系统调用 -> 写入旧事务的描述符块入日志(元数据)-> 写入旧事务的数据入日志 -> 等待写入日志完成 -> 提交日志 -> 等待提交完成 -> (存档) -> 写入数据 ->复用日志
      为了预防死锁,系统调用需要实现事先声明大小;
    • 事务组成结构: 日志超级快 + 事务块,超级块指向了最旧事务的提交块
    • 日志恢复流程: 从超级块恢复最旧事务指针->通过事务的元数据(描述符块)确定事务的结尾(通过魔术数实现对描述符块和数据块,同时在数据块中通过以全0置换魔术数的方式保证魔术数的唯一性)
    • ext3在提交事务中阻塞新系统调用,并等待旧系统调用的原因: 避免新系统调用造成的对旧系统调用的影响,这会导致崩溃时候的原子性的破坏
  • 日志核心: 预写规则保证了多步提交的原子性
  • 日志加速: 不将数据写入日志,只写入元数据 -> 可能导致隔离性的损失 -> 通过调整写入顺序解决,即先写入数据,之后写入日志,然后提交。

lec17

  • 应用程序的虚拟内存: OS使用虚拟内存提升了性能;用户程序是否可以实现同样类似的机制
  • 原语需求: trap(陷阱); prot1/unprot/protN(权限+节省TLB刷新);Dirty(脏页);map2(多对一映射)
  • 现代unix实现:
    • mmap机制(mmap和unmap): 位置,长度,权限位,映射地址,文件描述符,offset等:map2
    • mprotect: 调整权限:prot1/unprot/protN
    • signaction: 信号处理程序(segfault机制):trap
  • 虚拟实现: 页表 + 虚拟内存区域(权限)
  • 用户级别的trap: 类似于sigalarm,进入内核->将事件处理程序放入用户空间 -> 进入内核 -> 恢复;(由于mmu页表的隔离,因此依旧保证了隔离)
  • 例:
    • 庞大计算表(大于内存): 使用vm原语,实现cow,即访问到f(i) 后触发页面错误,现场计算之后存入表;同时,如果占用内存太大,根据策略清理相应表
    • 垃圾收集器:
      复制垃圾收集器:将堆分为两部分,from和to,首先从to分配内存,
      在to区域满后,翻转from和to区域,
      在from中根据其根指针确定有效内存,并在to中申请新的内存并复制(根指针和数据),
      之后丢弃from区域中的数据,使其成为空闲区域
      原始方法是利用scan和unsacned指针判定其在from区域还是to区域,从而确认位置,一次转移位置(阻塞)
    • Baker算法: 实时+增量:类似于COW,只保存root指针,只有在调用new时再转移,同时,在取消引用时,检查其是否在from区域,如果在,则进行复制(均摊操作)
    • 利用用户级别的原语,利用页面错误,将to区域设置为none(保护),实现检查和转发(复制)-> 将指针检查转移为硬件操作,同时简化了并行性
      同时利用收集器和app的双重映射(不同权限)来保证收集器可以实现对unsacned区域的读写
  • 现代操作系统: 5级页表+内核页表隔离 +ASID 等,其是不断变化的

四、完成lab及其代码

  • 系统调用+修改Makefile

    Makefile
    ...
    UPROGS=\
    ...
    	$U/_mmaptest\
    
    user/usys.pl
    ...
    entry("mmap");
    entry("munmap");
    
    user/user.h
    // system calls
    ...
    void *mmap(void *, uint64 , int, int, int, uint64);
    int munmap(void *, uint64);
    
    kernel/syscall.h
    ...
    #define SYS_mmap 22
    #define SYS_munmap 23
    
    kernel/syscall.c
    ...
    extern uint64 sys_mmap(void);
    extern uint64 sys_munmap(void);
    
    static uint64 (*syscalls[])(void) = {
    ...
    [SYS_mmap]   sys_mmap,
    [SYS_munmap]   sys_munmap,
    };
    
  • 定义常量

    kernel/def.h
    ...
    struct vma;
    ...
    // syscall.c
    ...
    int            vmatrylazyalloc(uint64);
    ...
    // vm.c
    ...
    void             uvmaunmap(pagetable_t, uint64, uint64, struct vma *);
    ...
    
    kernel/memlayout.h
    ...
    #define  MMAPEND TRAPFRAME //start bottom
    
    kernel/param.h
    ...
    #define NVMA      16// Number of Virtual memory areas stack 
    
    kernel/riscv.h
    ...
    #define PTE_D (1L << 7) // dirty
    
  • 定义结构体

    kernel/proc.h
    ...
     //virtual memory areas
     struct vma {
      int valid; // is use?
      uint64 vastart; 
      uint64 sz;
      int prot;
      int flags;
      struct file *f;
      uint64 offset;
     };
    // Per-process state
    struct proc {
    ...
      struct vma vmas[NVMA];               // Virtual memory areas
    };
    
  • 核心实现:mmap,unmmap的实现+lazyalloc的实现

    kernel/sysfile.h
    ...
    #include "memlayout.h"
    ...
    
    uint64
    sys_mmap(void)
    {
      // printf("sys_mmap enter\n");
      uint64 addr, sz, offset;
      uint64 vmaend = MMAPEND;
      int i;
      int prot, flags, fd;
      struct file *f;
      struct proc *p = myproc();
      struct vma *vmaBuff = 0;
    
      if (argaddr(0, &addr) < 0 ||
    	argaddr(1, &sz) < 0 ||
    	argint(2, &prot) < 0 ||
    	argint(3, &flags) < 0 ||
    	argfd(4, &fd, &f) < 0 ||
    	argaddr(5, &offset) < 0 ||
    	sz == 0) { //j传参检查
    	  return -1;
    	}
    	//合法性检查
      if (((!f->readable) && (prot & (PROT_READ))) ||
    	((!f->writable) && (prot & (PROT_WRITE)) && !(flags & MAP_PRIVATE))) {
    	  return -1;
    	}
      //分配地址(实现addr == 0的情况)
      sz = PGROUNDUP(sz);
      for (i = 0; i < NVMA; i++) {
    	if(p->vmas[i].valid == 0) {
    	  if(vmaBuff == 0) {
    		vmaBuff = &p->vmas[i];
    		vmaBuff->valid = 1;
    	  }
    	} else if (p->vmas[i].vastart < vmaend) {
    		vmaend = PGROUNDDOWN(p->vmas[i].vastart);
    	  }
    	}
      //初始化
      if (vmaBuff != 0) {
    	vmaBuff->vastart = vmaend - sz;
    	vmaBuff->sz = sz;
    	vmaBuff->prot = prot;
    	vmaBuff->flags = flags;
    	vmaBuff->f = f;
    	vmaBuff->offset = offset;
    	filedup(vmaBuff->f);
    	return vmaBuff->vastart;
      }
    
      printf("mmap: no free vma\n");
      return -1;
    }
    
    struct vma *
    vmaMap(struct proc *p, uint64 va) { //寻找va所对应的vmas结构体
      int i;
    
      for (i = 0; i < NVMA; i++) {
    	if ((p->vmas[i].valid == 1) && (va >= p->vmas[i].vastart) && (va < (p->vmas[i].vastart + p->vmas[i].sz))) {
    	  return &p->vmas[i];
    	}
      }
      return 0;
    }
    
    uint64
    sys_munmap(void)
    {
      uint64 addr, sz;
      // int nummap;
      struct vma *vmaBuff = 0;
      struct proc *p = myproc();
      // printf("sys_munmap enter\n");
    
      if (argaddr(0, &addr) < 0|| argaddr(1, &sz) < 0 || sz == 0) {
    	return -1;
      } //检查传入参数
    
    
    //合法性检查
      if ((vmaBuff = vmaMap(p, addr)) == 0) {
    	return -1;
      }
      if ((addr > vmaBuff->vastart) && (addr + sz < vmaBuff->vastart +vmaBuff->sz) ) {
    	return -1;
      }
    
      //如果不准确释放会出现内存丢失或者重复释放
      uint64 addrAligned = addr;
      if (addr > vmaBuff->vastart) { 
    	addrAligned = PGROUNDUP(addr);
      }
      uint64 nummap = sz - (addrAligned - addr); // 为什么呢?
      printf("nummap: %d", nummap);
      if(nummap < 0)
    	nummap = 0;
      uvmaunmap(p->pagetable, addrAligned, nummap,vmaBuff);
    
      if(addr <= vmaBuff->vastart && addr + sz > vmaBuff->vastart) {
    	vmaBuff->offset += addr + sz - vmaBuff->vastart;
    	vmaBuff->vastart = addr + sz;
      }
    
      vmaBuff->sz -= sz;
    
      if(vmaBuff->sz <= 0) {
    	fileclose(vmaBuff->f);
    	vmaBuff->valid = 0;
      }
      return 0;
    
    }
    
    
    int
    vmatrylazyalloc(uint64 va) {
      struct proc *p = myproc();
      struct vma *vmaBuff;
      void *pa;
      int perm = PTE_U;
    
      if ((vmaBuff = vmaMap(p, va)) == 0) {
    	return -1;
      }//找到vma
      if ((pa = kalloc()) == 0) {
    	panic("vmatrylazyalloc:kalloc");
      } //分配内存
      va = PGROUNDDOWN(va);
      memset(pa, 0, PGSIZE);
      //读取文件映射到内存
      begin_op();
      ilock(vmaBuff->f->ip);
      if(readi(vmaBuff->f->ip, 0, (uint64)pa, vmaBuff->offset + PGROUNDDOWN(va - vmaBuff->vastart), PGSIZE) < 0) {
    	panic("vmatrylazyalloc:readi");
      }
      iunlock(vmaBuff->f->ip);
      end_op();
    
      // printf("vmatrylazyalloc:op\n");
      //用户状态位和内核状态位置的转换
      if (vmaBuff->prot & PROT_READ) {
    	perm |= PTE_R;
      }
      if (vmaBuff->prot & PROT_WRITE) {
    	perm |= PTE_W;
      }
      if (vmaBuff->prot & PROT_EXEC) {
    	perm |= PTE_X;
      }
      // printf("vmatrylazyalloc:mappages");
      if(mappages(p->pagetable, va, PGSIZE, (uint64)pa, perm) < 0) {
    	panic("vmatrylazyalloc:mappages");
      }
    
    
      return 0;
    }
    
    kernel/trap.c
    ...
    void
    usertrap(void)
    {
    ...
      } else if((which_dev = devintr()) != 0){
    	// ok
      } else if(r_scause() == 13 || r_scause () == 15){
    	  if(vmatrylazyalloc(r_stval()) != 0) {
    		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;
    	  } 
      } else {
    
    kernel/vm.c
    ...
    #include "fcntl.h"
    #include "spinlock.h"
    #include "sleeplock.h"
    #include "file.h"
    #include "proc.h"
    
    ...
    void
    uvmaunmap(pagetable_t pagetable, uint64 va, uint64 nbytes, struct vma *v)
    {
      uint64 a;
      pte_t *pte;
    
      for(a = va; a < va + nbytes; a += PGSIZE){
    	if((pte = walk(pagetable, a, 0)) == 0)
    	  panic("uvmaunmap: walk");
    	if(PTE_FLAGS(*pte) == PTE_V)
    	  panic("uvmunmap: not a leaf");
    	if(*pte & PTE_V) {
    	  uint64 pa = PTE2PA(*pte);
    	  if ((*pte & PTE_D) && (v->flags & MAP_SHARED)) {
    		begin_op();
    		ilock(v->f->ip);
    		uint64 aoffset = a - v->vastart;
    		if (aoffset < 0) {
    		  writei(v->f->ip, 0, pa + (- aoffset), v->offset, PGSIZE + aoffset);
    		} else if (aoffset + PGSIZE > v->sz) {
    		  writei(v->f->ip, 0, pa, v->offset + aoffset, v->sz - aoffset);
    		} else {
    		  writei(v->f->ip, 0, pa, v->offset + aoffset, PGSIZE);
    		}
    		iunlock(v->f->ip);
    		end_op();
    	  }
    	  kfree((void*) pa);
    	  *pte = 0;
    	}
      }
    }
    
  • proc相应函数修改

    kernel/pro.c
    static struct proc*
    allocproc(void)
    {
    ...
    found:
    ...
      //clear VMAs
      for (int i = 0; i < NVMA; i++) {
    	p->vmas[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++) {
    	if(p->vmas[i].valid) {
    	  uvmaunmap(p->pagetable, p->vmas[i].vastart, p->vmas[i].sz,&p->vmas[i]);
    	}
      }
      if(p->pagetable)
    	proc_freepagetable(p->pagetable, p->sz);
    ...
    }
    ...
    // Create a new process, copying the parent.
    // Sets up child kernel stack to return as if from fork() system call.
    int
    fork(void)
    {
    ...
    	for(i = 0; i < NVMA; i++) {
    	  struct vma *v = &p->vmas[i];
    	  if(v->valid) {
    		np->vmas[i] = *v;
    		filedup(v->f);
    	}
      }
      ...
    }
    

参考文献

「研读笔记」MIT 6.S081 Paper Virtual Memory Primitives for User Programs:https://zhuanlan.zhihu.com/p/610704237
Baker's garbage collection笔记:https://www.zhihu.com/column/c_1487476999569006592
2020版xv6手册:https://pdos.csail.mit.edu/6.S081/2020/xv6/book-riscv-rev1.pdf
28天速通MIT 6.S081操作系统公开课:https://zhuanlan.zhihu.com/p/630222721
MIT6.s081操作系统笔记:https://juejin.cn/post/7022347989683273765

posted @ 2024-04-02 20:57  David_Dong  阅读(12)  评论(0编辑  收藏  举报