Lab10 mmap

文件内存映射

在传统的I/0技术中,

  • 读取一个文件,文件数据会:磁盘->内核缓冲区->用户缓冲区
  • 如果进行了修改,则是用户缓冲区->内核缓冲区->磁盘

也就是说,每次读写都要 切换内核态/用户态 以及 在内核空间和用户空间之间拷贝数据。这对于处理文件读写密集型场景和大文件不友好。
而mmap技术:

  • 将 内核缓冲区中的文件数据 映射 到进程的地址空间,就能直接在用户空间修改文件内容,免去了一次拷贝过程。

mmap

实现处理文件内存映射的mmap 与 munmap 系统调用。
实验手册上提供了 mmap 和 munmap 的函数签名:
void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset);
可以假设 addr 始终为零,这意味着内核应决定映射文件的虚拟地址。mmap 返回该地址,或在失败时返回 0xffffffffffffffff。length 是要映射的字节数;它可能与文件的长度不同。prot 指示内存应被映射为可读、可写和/或可执行; 可以假设 prot 为 PROT_READ 或 PROT_WRITE 或两者兼有。flags 可能是 MAP_SHARED,表示对映射内存的修改应写回文件,或 MAP_PRIVATE,表示不应写回。无需实现 flags 中的其他位。fd 是要映射的文件的打开文件描述符。可以假设偏移量为零(它是文件中映射的起始位置)。
int munmap(void* addr, int length);
munmap 移除指定地址范围内的 mmap 映射。如果进程已修改内存且将其映射为 MAP_SHARED,则修改应首先写回文件。munmap 调用可能仅覆盖 mmap 映射区域的一部分,但可以假设它要么从开头取消映射,要么在结尾取消映射,要么取消整个区域的映射(但不会在区域中间留下空洞)。也就是说释放后的区域数目必须是0或1,

注册系统调用

//user/user.h
//System calls
void* mmap(void*, int, int, int, int, int);
int munmap(void*, int);

//user/usys.pl
entry("mmap");
entry("munmap");

//Makefile
$U/_mmaptest\

//kernel/syscall.h
#define SYS_mmap   22
#define SYS_munmap 23

//kernel/syscall.c
extern uint64 sys_mmap(void);
extern uint64 sys_munmap(void);

[SYS_mmap]    sys_mmap,
[SYS_munmap]  sys_munmap,

相关数据结构

根据手册,定义一个结构体vma来跟踪进程映射的内存区域。其中相关的标志在fcntl.h中已经定义好了。

//kernel/prco.h
struct vma{
  //  int valid;      //该虚拟内存区域是否被映射
    uint64 start;     //虚拟内存映射区域开始地址
    uint64 sz;        //虚拟内存映射区域大小
    int prot;         //内存被映射为读、写、读写
    int flags;        //内存的修改是否写回文件
    struct file* file;  //该虚拟区域映射的文件
    uint64 offset;    //文件偏移量
  };
  
#define NVMA 16

struct proc {
  struct spinlock lock;
...
  struct vma vmas[NVMA];
};

实现mmap

//kernel/sysfile.c
uint64 
sys_mmap(void)
{
  uint64 addr;
  int length, prot, flags, fd, offset;
  struct file* file;

  //传入参数
  if( argaddr(0, &addr) < 0 || argint(1, &length) < 0 || argint(2, &prot) < 0 
  || argint(3, &flags) < 0 || argfd(4, &fd, &file) < 0 || argint(5, &offset) < 0)
    return 0xffffffffffffffff;
  
  //检查参数
  if( length <=0 ) 
    return 0xffffffffffffffff;
  if( (!file->readable && (prot & PROT_READ)) //源文件不可读但映射可读
    || (!file->writable && (prot & PROT_WRITE) && (flags == MAP_SHARED) )//源文件不可写但映射可写
  )
    return 0xffffffffffffffff;

  struct proc* p = myproc();
  struct vma* v = 0;
  
  length = PGROUNDUP(length);//页面对齐

  //遍历进程的vams数组,寻找第一个空闲的vams
  for(int i = 0; i < NVMA; ++i){
    if( p->vmas[i].valid == 0){
      v = &p->vmas[i];
      v->valid = 1;
      //设置映射区域的属性
      v->start = p->sz;//使用进程空间的末尾
      p->sz += length;
      v->sz = length;
      v->prot = prot;
      v->flags = flags;
      v->file = file;
      v->offset = offset;
      //给文件增加引用记数
      filedup(file);
      return v->start;
    }
  }

  printf("没有空闲的映射内存区域\n");
  return 0xffffffffffffffff;
}

惰性分配

映射功能也使用惰性分配,实际的物理内存分配延迟到usertrap中:当 mmap 引发页面故障时,分配一页物理内存,将相关文件的 4096 字节的数据读入,并将其映射到用户地址空间。

//kernel/sysfile.c
int vamlazyalloc(uint64 va){
  //先找到对应的vma
  struct proc* p = myproc();
  struct vma* v = 0;
  for(int i = 0; i < NVMA; ++i){
    v = &p->vmas[i];
    if( v->valid == 1 && v->start <= va && va < v->start + v->sz){
       break;
    }
    if( i == NVMA - 1){
      printf("vamlazyalloc没有空闲的vma\n");
      return -1;
    }
  }

  //分配物理页
  void* pa = kalloc();
  if( pa == 0)
    panic("err_vamlazyalloc_pa");
  memset(pa, 0, PGSIZE);

  //读入数据
  begin_op();
  ilock(v->file->ip);
  readi(v->file->ip, 0 ,(uint64)pa, v->offset + PGROUNDDOWN(va - v->start),PGSIZE);
  iunlock(v->file->ip);
  end_op();

  //映射
  if(mappages(p->pagetable, va, PGSIZE, (uint64)pa, PTE_R | PTE_W | PTE_U) < 0)
    panic("err_vamlazyalloc_map");

  return 1;
}

//kernel/defs.h

//sysfile.c
int vamlazyalloc(uint64);

//kernel/trap.c

else if((which_dev = devintr()) != 0){
    // ok
  } else if(r_scause() == 13 || r_scause() == 15){
    uint64 va = r_stval();
    if( p->sz < va || va > MAXVA )
      p->killed = 1;
    else
      if( vamlazyalloc(va) == -1)
        p->killed = 1;
  }

同时像lab 5一样,注释掉一部分panic

//kernel/vm.c
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
  uint64 a;
  pte_t *pte;

  if((va % PGSIZE) != 0)
    panic("uvmunmap: not aligned");

  for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
    if((pte = walk(pagetable, a, 0)) == 0)
    //  panic("uvmunmap: walk");
      continue;
    if((*pte & PTE_V) == 0)
    //  panic("uvmunmap: not mapped");
      continue;
    if(PTE_FLAGS(*pte) == PTE_V)
      panic("uvmunmap: not a leaf");
    if(do_free){
      uint64 pa = PTE2PA(*pte);
      kfree((void*)pa);
    }
    *pte = 0;
  }
}

//uvmcopy.c
  for(i = 0; i < sz; i += PGSIZE){
    if((pte = walk(old, i, 0)) == 0)
      panic("uvmcopy: pte should exist");

    if((*pte & PTE_V) == 0)
      //panic("uvmcopy: page not present");
      continue;

munmap

释放进程的映射区域。

//kernel/sysfile.c
uint64 
sys_munmap(void)
{
  uint64 addr;
  int length;

  struct proc* p = myproc();
  struct vma*  v = 0;
  
  if( argaddr(0, &addr) < 0 || argint(1, &length) < 0)
    return -1;
  if( length <= 0)
    return -1;
  
  if( p->sz + length > MAXVA)
    return -1;


  for(int i = 0; i < NVMA; ++i){
    v = &p->vmas[i];
    if( v->valid == 1 && v->start <= addr && addr < v->start + v->sz){
       break;
    }
    if( i == NVMA - 1){
      panic("err_munmap_va2vma");
    }
  }
  //页面对齐
  addr = PGROUNDDOWN(addr);
  length = PGROUNDUP(length);

  //MAP_SHARED需要写回
  if(v->flags & MAP_SHARED)
    filewrite(v->file, addr, length);

  uvmunmap(p->pagetable, addr, length/PGSIZE, 1);

  //要么从开头取消映射,要么在结尾处取消映射,要么取消整个区域的映射(但不会在区域中间留下空洞)
  //我理解的是要么取消前一部分,要么取消后一部分,要么取消整个区域
  if( addr == v->start && length == v->sz ){//取消整个vma
    fileclose(v->file);
    v->valid = 0;
  } else if(addr == v->start){//取消开头一部分
    v->start +=  length;
    v->sz -= length;
    v->offset += length;
  } else if( addr != v->start && addr + length == v->start + v->sz){//取消后一部分
    v->sz -= length;
  } else{
    panic("err_sysmunmap:非法取消映射区域");
  }
  
  return 0;
}

fork和exit

fork创建子进程时同样也要复制vma数组。

//kernel/proc.c
//fork()
  pid = np->pid;
  
  for(int i = 0; i < NVMA; i++) { 
    if(p->vmas[i].valid) {
      np->vmas[i] = p->vmas[i];
      if(p->vmas[i].file) 
        filedup(p->vmas[i].file); 
    }
  }

  np->state = RUNNABLE;

进程退出时也要取消映射,并写盘。

//kernel/proc.c
//eixt()
  for(int i = 0; i < NVMA; ++i){
    if( p->vmas[i].flags & MAP_SHARED){
      filewrite(p->vmas[i].file, p->vmas[i].start, p->vmas[i].sz);
    }
	
    uvmunmap(p->pagetable, p->vmas[i].start, p->vmas[i].sz/PGSIZE, 1);
    p->vmas[i].valid = 0;
  }
  
  begin_op();

运行mmaptest和usertests均能通过

$ mmaptest
mmap_test starting
test mmap f
test mmap f: OK
test mmap private
test mmap private: OK
test mmap read-only
test mmap read-only: OK
test mmap read/write
test mmap read/write: OK
test mmap dirty
test mmap dirty: OK
test not-mapped unmap
test not-mapped unmap: OK
test mmap two files
test mmap two files: OK
$ usertests
...
test forktest: OK
test bigdir: OK
ALL TESTS PASSED
posted @ 2025-08-08 14:34  名字好难想zzz  阅读(10)  评论(0)    收藏  举报