Page fault(异常)

相关知识

page fault一般指程序访问内存时发生的错误,通过page fault会触发trap机制,进入内核状态。可以实现的一系列虚拟内存功能。这里相关的功能有:

  • lazy allocation

  • copy-on-write fork

  • demand paging

  • memory mapped files

lazy allocation(延迟分配内存)

先为用户分配一块虚拟地址空间,但并没有映射到物理内存,当需要使用时,才给它映射物理内存。需要注意的是,在映射之前,如果用户释放这块自己以为已经得到的内存,其实内核什么也没做。因为此时物理内存并没有映射,如果释放虚拟内存反而会产生错误。

一个例子便是bss段的内存映射。BSS区域包含了未被初始化或者初始化为0的全局或者静态变量。这里或许有许多许多个矩阵,但是所有的矩阵内容都为0。在物理内存中,我只需要分配一个page,这个page的内容全是0。然后将所有虚拟地址空间的全0的page都map到这一个物理page上。在程序启动的时候能节省大量的物理内存分配。之后在某个时间点,应用程序尝试写BSS中的一个page时,比如说需要更改一两个变量的值,我们会得到page fault。然后可以在物理内存中申请一个新的内存page,将其内容设置为0,更新这个page的mapping关系,首先PTE要设置成可读可写,然后将其指向新的物理page。

在lazy allocation中,如果物理内存已耗尽了该如何办?

如果内存耗尽了,一个选择是撤回page(evict page)。比如说将部分内存page中的内容写回到文件系统再撤回page。一旦你撤回并释放了page,那么你就有了一个新的空闲的page,你可以使用这个刚刚空闲出来的page,分配给刚刚的page fault handler,再重新执行指令。

copy-on-write fork

写时拷贝,创建子进程时,子进程和父进程先共用内存。为了确保进程间的隔离性,我们可以将这里的父进程和子进程的PTE的标志位都设置成只读的。

在某个时间点,当父进程和子进程任一个需要更改内存的内容时,我们会得到page fault。

在得到page fault之后,我们需要拷贝相应的物理page。假设现在是子进程在执行store指令,那么我们会分配一个新的物理内存page,然后将page fault相关的物理内存page拷贝到新分配的物理内存page中,并将新分配的物理内存page映射到子进程。这时,新分配的物理内存page只对子进程的地址空间可见,所以我们可以将相应的PTE设置成可读写,并且我们可以重新执行store指令。实际上,对于触发刚刚page fault的物理page,因为现在只对父进程可见,相应的PTE对于父进程也变成可读写的了。

copy-on-write fork

(1). 修改uvmcopy。每次创建子进程,便会调用此函数为子进程分配物理内存。改为不为子进程分配内存,而是使父子进程共享内存,但禁用PTE_W,同时标记PTE_cow,记得调用kaddrefcnt增加引用计数

(2). 在**kernel/riscv.h**中选取PTE中的保留位定义标记一个页面是否为COW Fork页面的标志位

// 记录应用了COW策略后fork的页面
#define PTE_F (1L << 8)

(3). 在**kalloc.c**中进行如下修改

  • 定义引用计数的全局变量ref,其中包含了一个自旋锁和一个引用计数数组,由于ref是全局变量,会被自动初始化为全0。

  • 这里使用自旋锁是考虑到这种情况:进程P1和P2共用内存M,M引用计数为2,此时CPU1要执行fork产生P1的子进程,CPU2要终止P2,那么假设两个CPU同时读取引用计数为2,执行完成后CPU1中保存的引用计数为3,CPU2保存的计数为1,那么后赋值的语句会覆盖掉先赋值的语句,从而产生错误

struct ref_stru {
  struct spinlock lock;
  int cnt[PHYSTOP / PGSIZE];  // 引用计数
} ref;
  • 在kinit中初始化ref的自旋锁

  • 修改kalloc和kfree函数,在kalloc中初始化内存引用计数为1,在kfree函数中对内存引用计数减1,如果引用计数为0时才真正删除

(4). 在**trap.c**中进行如下修改

在usertrap()函数中,判断page fault产生类型是否是由于写入内存错误。如果是,判断PTE的cow位是否被标志,如果是,则读取内存引用计数,如果为1,说明只有当前进程引用了该物理内存(其他进程此前已经被分配到了其他物理页面),就只需要改变PTE使能PTE_W;否则就分配物理页面,并将原来的内存引用计数减1

uint64 cowalloc(pagetable_t pagetable, uint64 va)
{
	pte_t *pte;
	uint64 pa;
  char* mem;

	if(cowpage(pagetable, va) == 0)
		return -1;

	pte = walk(pagetable, va, 0);  //子进程的页表pte
  pa = PTE2PA(*pte); //共用的物理地址

  //flags = PTE_FLAGS(*pte);
	
	// 清除PTE_V,否则在mappagges中会判定为remap
    if(krefcnt((char*)pa) == 1) {
    // 只剩一个进程对此物理地址存在引用
    // 则直接修改对应的PTE即可
    *pte |= PTE_W;
    *pte &= ~PTE_cow;
    return pa;
  } else {
    // 多个进程对物理内存存在引用
    // 需要分配新的页面,并拷贝旧页面的内容
    mem = kalloc();
    if(mem == 0)
      return 0;

    // 复制旧页面内容到新页
    memmove(mem, (char*)pa, PGSIZE);

    // 清除PTE_V,否则在mappagges中会判定为remap
    *pte &= ~PTE_V;

    // 为新页面添加映射
    if(mappages(pagetable, va, PGSIZE, (uint64)mem, (PTE_FLAGS(*pte) | PTE_W) & ~PTE_cow) != 0) {
      kfree(mem);
      *pte |= PTE_V;
      return 0;
    }

    // 将原来的物理内存引用计数减1
    kfree((char*)PGROUNDDOWN(pa));
    return (uint64)mem;
  }
}

(5). 在copyout(由内核copy到用户空间)中处理相同的情况,如果是COW页面,需要

也需要重新为子进程分配内存

mmap系统调用

void *mmap(void *addr, int length, int prot, int flags, int fd, int off);
int munmap(void *addr, int length);

/*addr:映射到的虚拟地址, 
  length:需要映射的字节数,可能和文件大小不相等,
  prot:表示映射到的内存的权限为PROT_READ还是PROT_WRITE。
  flag:可以是MAP_SHARED或MAP_PRIVATE,如果是前者就需要将内存中修改的相应部分写回文件中。
  fd: 需要映射的已打开的文件描述符。
*/
#define MAXVMA 16
struct vma {
  int valid;                // whether the vma is valid
  uint64 addr;              // starting virtual address of vma
  int len;                  // length of vma, unit: bytes
  int prot;                 // permission
  int flags;                // flag
  struct file *f;           // pointer to mapped file
  int off;                  // offset of the valid mapped address
  int valid_len;            // length of the valid mapped address
};
  1. 实现系统调用函数

先不分配物理内存,只是找到空闲的vma,初始化vma。

使用COW机制分配真实内存。

uint64 
sys_mmap(void)
{
  int length, prot, flags, fd;
  struct file *f;
  if (argint(1, &length)<0 || argint(2, &prot)<0 || argint(3, &flags)<0 || argfd(4, &fd, &f)<0) {
    return -1;
  }
  if (!f->writable && (prot & PROT_WRITE) && (flags & MAP_SHARED)) return -1; // to assert that readonly file could not be opened with PROT_WRITE && MAP_SHARED flags
  struct proc *p = myproc();
  struct vma *pvma = p->procvma;
    //查找空闲的vma
  for (int i = 0; i < MAXVMA; i++) {
    if(pvma[i].valid == 0) {  
      pvma[i].addr = p->sz;  //进程地址空间的最上方,mmap起始虚拟地址,addr+len为结束地址
      pvma[i].f = filedup(f);  // increment the refcount for f
      pvma[i].flags = flags;
      pvma[i].len = PGROUNDUP(length);  //映射的文件字节长度
      pvma[i].prot = prot;
      pvma[i].valid = 1; 
      pvma[i].off = 0;
      pvma[i].valid_len = pvma[i].len;
      p->sz += pvma[i].len; //更新进程已使用地址空间,预分配
      return pvma[i].addr;
    }
  }
  return -1;
}
  1. 出发page fault进入trap时,先判断由于访问虚拟地址引发trap的va是否合法(在vma管理的mmap地址范围内)分配内存,映射到虚拟内存。,将对应的文件内容用放入刚分配的(物理)内存中。

else if (r_scause() == 13 || r_scause() == 15) {
    uint64 va = r_stval();
    if (va < p->trapframe->sp || va >= p->sz) {
      goto exception;
    }
    struct vma *pvma = p->procvma;
    va = PGROUNDDOWN(va);

    for (int i = 0; i < MAXVMA; i++) {
      if (pvma[i].valid && va >= pvma[i].addr + pvma[i].off && va < pvma[i].addr + pvma[i].off + pvma[i].valid_len) {  
        // allocate one page in the physical memory
        char *mem = kalloc();
        if (mem == 0) goto exception;
        memset(mem, 0, PGSIZE);
        int flag = (pvma[i].prot << 1) | PTE_U; // PTE_R == 2 and PROT_READ == 1
        if (mappages(p->pagetable, va, PGSIZE, (uint64)mem, flag) != 0) {
          kfree(mem);
          goto exception;
        }   
        int off = va - pvma[i].addr;
        ilock(pvma[i].f->ip);
        readi(pvma[i].f->ip, 1, va, off, PGSIZE);
        iunlock(pvma[i].f->ip);
        break;
      }
    }
  }
  1. sys_munmap函数

首先需要找到对应的vma,然后根据unmap的大小和起点的不同进行讨论。如果是从vma有效部分的起点开始,当整个vma都被unmap掉时,需要标记这个打开的文件被关闭(但是现在还不能关闭,因为后面可能需要写回硬盘中的文件)。如果是从中间部分开始,则直接unmap一直到结尾

然后判断是否是MAP_SHARED,如果是就需要从内存写回磁盘原文件

sys_munmap(void)
{
  uint64 addr;
  int length;
  if (argaddr(0, &addr) < 0 || argint(1, &length) < 0) {
    return -1;
  }
  struct proc *p = myproc();
  struct vma *pvma = p->procvma;
  int close = 0;
  // find the corresponding vma
  for (int i = 0; i < MAXVA; i++) {
    if (pvma[i].valid && addr >= pvma[i].addr && addr < pvma[i].addr + pvma[i].len) {
      addr = PGROUNDDOWN(addr);
      if (addr == pvma[i].addr + pvma[i].off) {
        // starting at begin of the valid address of vma
        if (length >= pvma[i].valid_len) {
          // whole vma is unmmaped
          pvma[i].valid = 0;
          length = pvma[i].valid_len;
          close = 1;
          p->sz -= pvma[i].len;
        } else {
          pvma[i].off += length;
          pvma[i].valid_len -= length;
        }
      } else {
        // starting at middle, should unmap until the end
        length = pvma[i].addr + pvma[i].off + pvma[i].valid_len - addr;
        pvma[i].valid_len -= length;
      }
      if (pvma[i].flags & MAP_SHARED) {
        // write the page back to the file
        if (_filewrite(pvma[i].f, addr, length, addr - pvma[i].addr) == -1) return -1; 
      }
      uvmunmap(p->pagetable, addr, PGROUNDUP(length)/PGSIZE, 0);  //取消内存映射
      if (close) fileclose(pvma[i].f);
      return 0;
    }
  }
  return -1;
} 

用到了log机制

/* f:文件指针
    addr:umap的地址
    n: umap的长度
    off: 从addr到vma起始地址的偏移
    */
int
_filewrite(struct file *f, uint64 addr, int n, uint off) {
  int r, ret = 0;

  if(f->writable == 0)
    return -1;

  if(f->type == FD_PIPE){
    ret = pipewrite(f->pipe, addr, n);
  } else if(f->type == FD_DEVICE){
    if(f->major < 0 || f->major >= NDEV || !devsw[f->major].write)
      return -1;
    ret = devsw[f->major].write(1, addr, n);
  } else if(f->type == FD_INODE){
    // write a few blocks at a time to avoid exceeding
    // the maximum log transaction size, including
    // i-node, indirect block, allocation blocks,
    // and 2 blocks of slop for non-aligned writes.
    // this really belongs lower down, since writei()
    // might be writing a device like the console.
    int max = ((MAXOPBLOCKS-1-1-2) / 2) * BSIZE; //一次写回的最大长度
    int i = 0;
    while(i < n){
      int n1 = n - i;
      if(n1 > max)
        n1 = max;
   
      begin_op();
      ilock(f->ip);
      if ((r = writei(f->ip, 1, addr + i, off, n1)) > 0)
        off += r;
      iunlock(f->ip);
      end_op();

      if(r != n1){
        // error from writei
        break;
      }
      i += r;
    }
    // ret = (i == n ? n : -1);
  } else {
    panic("filewrite");
  }
  return ret;
}

问题:释放内存处,

发现物理内存的释放必须是整页整页的释放,而之前munmap指定的addr可能正处于页中间,此时该页整体不应该被释放(或者是地址下移,凑够整页,再释放)

将uvmunmap改成vmaunmap

// Remove n BYTES (not pages) of vma mappings starting from va. va must be
// page-aligned. The mappings NEED NOT exist.
// Also free the physical memory and write back vma data to disk if necessary.
void
vmaunmap(pagetable_t pagetable, uint64 va, uint64 nbytes, struct vma *v)
{
  uint64 a;
  pte_t *pte;

  // printf("unmapping %d bytes from %p\n",nbytes, va);

  // borrowed from "uvmunmap"
  for(a = va; a < va + nbytes; a += PGSIZE){
    if((pte = walk(pagetable, a, 0)) == 0)
      panic("sys_munmap: walk");
    if(PTE_FLAGS(*pte) == PTE_V)
      panic("sys_munmap: not a leaf");
    if(*pte & PTE_V){
      uint64 pa = PTE2PA(*pte);
      if((*pte & PTE_D) && (v->flags & MAP_SHARED)) { // dirty, need to write back to disk
        begin_op();
        ilock(v->f->ip);
        uint64 aoff = a - v->vastart; // offset relative to the start of memory range
        if(aoff % PFSIZE != 0) { // if the first page is not a full 4k page
          向下多删一段,凑够整页
        } else if(aoff + PGSIZE > v->sz){  // if the last page is not a full 4k page
          向下多删一段,凑够整页
        } else { // full 4k pages
          writei(v->f->ip, 0, pa, v->offset + aoff, PGSIZE);
        }
        iunlock(v->f->ip);
        end_op();
      }
      kfree((void*)pa);
      *pte = 0;
    }
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值