MIT-6.S081-2020實驗(xv6-riscv64)十:mmap


實驗文檔

概述

這次實驗要求實現Linux中的mmap函數的一個子集,相當於在第五次實驗Lazy Allocation中加上了文件的操作。難度比較難定義,因為這個“子集”還是比較模糊的,如果僅僅只針對測試程序,做出一些簡化性的假設,難度就不會太大,但如果不做這些假設,難度就會非常高。

內容

為了簡化問題,首先做出一些假設:

  • 調用mmap的參數length是頁寬的倍數,調用munmap的參數addr和length也都是頁寬的倍數。
  • 單個進程調用mmap映射的虛擬地址空間總大小不超過1G(包括被munmap回收的那些地址空間)。
  • 單個進程當前被mmap的虛擬地址段數不超過16。

那么就可以寫代碼了,首先是vma結構體:

struct vma {
    uint64 addr, oaddr; int valid, length, prot, flags;
    struct file *fd;
};

其中valid表示當前vma是否已被使用,prot,flags為調用mmap時傳入的參數,addr和length為該地址段的當前首地址和長度,oaddr為初始首地址,就是這一段在最開始被映射時的首地址。因為munmap的存在,地址段的當前首地址會隨之改變,但在計算文件偏移量的時候,文件的0偏移對應的還是地址段的初始首地址,所以需要專門保存。然后在proc結構體加入兩個屬性:

  struct vma vmas[16];
  uint64 mmapsz;

mmapsz為被映射的地址空間大小(包括被munmap回收的那些地址空間,即munmap之后mmapsz不減小),這里定義所有mmap的地址空間為128G到129G之間,即從1L << 37開始。

接着就是sys_mmap:

uint64 sys_mmap(void) {                                                                                         int length, prot, flags; struct file *fd;
    if (argint(1, &length) < 0 || argint(2, &prot) < 0 || argint(3, &flags) || argfd(4, 0, &fd)) return -1;
    if (length % PGSIZE != 0 || (fd->writable == 0 && (prot & PROT_WRITE) && flags == MAP_SHARED)) return -1;
    struct proc *p = myproc();
    uint64 addr = (1L << 37) + p->mmapsz;
    for (int i = 0; i < 16; i++) if (!p->vmas[i].valid) {
        p->vmas[i].valid = 1; p->vmas[i].addr = p->vmas[i].oaddr = addr;
        p->vmas[i].length = length; p->vmas[i].prot = prot;
        p->vmas[i].flags = flags; p->vmas[i].fd = fd;
        filedup(fd); p->mmapsz += length; return addr;
    }
    return -1;
}

首先需要判斷一下權限,如果映射的內存段可以被寫且定義被寫的內存需要寫回文件,而文件又不能被寫,這就矛盾了,直接返回失敗。申請地址的時候這里大大簡化了,就是直接從1L << 37 + p->mmapsz開始,申請完p->mmapsz直接增加。賦值操作很清晰,沒啥說的。

然后是對缺頁中斷的處理,還是一樣,這里抽象成了函數,trap.c內的修改和Lazy Allocation實驗一模一樣,這里僅給出handle_page函數:

uint64 handle_page(uint64 va, struct proc *p) {
    if (va < (1L << 37) || va >= (1L << 57)) return -1;
    for (int i = 0; i < 16; i++)
        if (p->vmas[i].valid && va >= p->vmas[i].addr && va < p->vmas[i].addr + p->vmas[i].length) {
            int perm = PTE_U;
            if (p->vmas[i].prot & PROT_READ) perm |= PTE_R;
            if (p->vmas[i].prot & PROT_WRITE) perm |= PTE_W;
            uint64 base = PGROUNDDOWN(va);
            char *pa = kalloc(); if (pa == 0) return -1; memset(pa, 0, PGSIZE);
            mappages(p->pagetable, base, PGSIZE, (uint64)pa, perm);
            begin_op(); ilock(p->vmas[i].fd->ip);
            readi(p->vmas[i].fd->ip, 1, base, base - p->vmas[i].oaddr, PGSIZE);
            iunlock(p->vmas[i].fd->ip); end_op();
            return 0;
        }
    return -1;
}

地址合法性判斷就直接判斷當前地址是不是在128G到129G之間不是則返回。接着就是枚舉有效的vma,看看當前缺的頁落在哪段被映射的地址段內,找到之后就是頁表映射,一貫地,頁表映射還是需要注意對齊問題,需要對地址向下對齊頁寬。另外要注意的是申請來的物理空間一定要初始化為0,因為可能當前在文件中的偏移量已經超過了文件大小,這時會讀不出東西,需要自行給讀不出東西的位置填充0。然后就是讀文件,因為當前inode指針被文件描述符長期持有,而文件描述符又被vma長期持有,所以不用iput。

再接着是sys_munmap,比較麻煩,但幸好有之前那些假設,加上實驗文檔里的保證:

An munmap call might cover only a portion of an mmap-ed region, but you can assume that it will either unmap at the start, or at the end, or the whole region (but not punch a hole in the middle of a region).

即munmap一次只會unmap一段的一部分,同時這一部分要么是這一段的開始到中間,要么是中間到結束,不會是中間到中間挖個洞。這為我們大大降低了工作量。

uint64 sys_munmap(void) {
    uint64 addr; int length;
    if (argaddr(0, &addr) < 0 || argint(1, &length) < 0) return -1;
    if (addr % PGSIZE != 0 || length % PGSIZE != 0) return -1;
    struct proc *p = myproc();
    for (int i = 0; i < 16; i++) {
        if (p->vmas[i].valid
            && (addr + length == p->vmas[i].addr + p->vmas[i].length)
            || addr == p->vmas[i].addr)) {
            for (int j = 0; j < length; j += PGSIZE) {
                if (walkaddr(p->pagetable, addr + j) == 0) continue;
                if (p->vmas[i].flags == MAP_SHARED) {
                    begin_op(); ilock(p->vmas[i].fd->ip);
                    writei(p->vmas[i].fd->ip, 1, addr + j, addr + j - p->vmas[i].oaddr, PGSIZE);
                    iunlock(p->vmas[i].fd->ip); end_op();
                }
                uvmunmap(p->pagetable, j + addr, 1, 1);
            }
            if (addr + length == p->vmas[i].addr + p->vmas[i].length)
                p->vmas[i].length = addr - p->vmas[i].addr;
            else {
                p->vmas[i].addr = addr + length;
                p->vmas[i].length -= length;
            }
            if (p->vmas[i].length == 0) {
                fileclose(p->vmas[i].fd); p->vmas[i].valid = 0;
            }
            return 0;
        }
    }
    return 0;
}

首先找到有效的vma,這里的區間覆蓋只有兩種情況,很好判斷,然后是逐頁寫文件及uvmunmap,如果當前頁還沒被訪問過(沒有被handle_page分配物理內存)就跳過,接着更新該段的addr和length,如果該段完全被解映射完了(length==0),就把文件描述符關閉並把vma標記為無效(未使用)。

這次實驗的大頭完成了,但是和Lazy Allocation實驗一樣,為了處理缺頁對進程相關的代碼還需要做一些額外的修改,首先是fork函數,需要把vma和1L << 37以上的內存復制到子進程:

  if(mmapcopy(p->pagetable, np->pagetable, p->mmapsz) < 0){
    freeproc(np);
    release(&np->lock);
    return -1;
  }
  np->mmapsz = p->mmapsz;
  for (int i = 0; i < 16; i++) if (p->vmas[i].valid) {
      np->vmas[i] = p->vmas[i]; filedup(np->vmas[i].fd);
  }

vm.c新定義了一個mmapcopy函數,內容和uvmcopy差不多,但是修改了遍歷范圍並把缺頁的panic跳掉了:

int mmapcopy(pagetable_t old, pagetable_t new, uint64 sz) {
  pte_t *pte;
  uint64 pa, i;
  uint flags;
  char *mem;

  for(i = 1L << 37; i < (1L << 37) + sz; i += PGSIZE) {
    if((pte = walk(old, i, 0)) == 0) continue;
    if((*pte & PTE_V) == 0) continue;
    pa = PTE2PA(*pte);
    flags = PTE_FLAGS(*pte);
    if((mem = kalloc()) == 0)
      goto err;
    memmove(mem, (char*)pa, PGSIZE);
    if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){
      kfree(mem);
      goto err;
    }
  }
  return 0;

 err:
  uvmunmap(new, i, (i - (1L << 37)) >> PGSHIFT, 1);
  return -1;
}

回到proc.c,然后是釋放進程需要做的處理:

  p->mmapsz = 0;
  for (int i = 0; i < 16; i++) p->vmas[i].valid = 0;

這時運行會發現freewalk函數panic:freewalk: leaf,這是因為freewalk希望所有虛擬地址已經被解綁並釋放對應的物理空間了,該函數只負責釋放頁表。而調用它的uvmfree只解綁了進程中0到p->sz的空間,mmap所用的1L << 37上面的空間沒被解綁,所以我們需要手動解綁,修改proc_freepagetable函數:

void
proc_freepagetable(pagetable_t pagetable, uint64 sz, uint64 mmapsz)
{
  uvmunmap(pagetable, TRAMPOLINE, 1, 0);
  uvmunmap(pagetable, TRAPFRAME, 1, 0);
  uvmunmap(pagetable, 1L << 37, mmapsz >> PGSHIFT, 1);
  uvmfree(pagetable, sz);
}

注意到這里proc_freepagetable的參數多了一個,需要在defs.h里修改函數頭部,同時調用這個函數的其他地方也需要修改,包括proc.c和exec.c里的幾行代碼,這里就不說了。另外和Lazy Allocation實驗一樣,需要把uvmunmap里關於缺頁的panic跳掉,這里也從略。


總結一下,這個實驗還是有點麻煩的,但因為只是作為單次實驗,實驗文檔和測試程序的要求也不高,實際上這個實驗拓寬來搞,可以做為課設級別的大實驗,比如消除mmap和munmap參數是頁寬的限制,這樣物理內存的分配和釋放都需要修改成能處理任意大小空間的形式,那么就得設計鏈表+淘汰算法或伙伴算法來維護空閑空間;比如地址段超過16個,就得設計內核級的動態分配數組的方式,還是依賴物理內存調度的支持;比如更大量的mmap空間支持,就需要重新考慮如何分配地址空間,可能還需要用到某些數據結構;比如完整的mmap參數支持,代碼量就更大了,甚至可以發展成完整的硬盤虛擬內存技術……


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM