关于c:MIT-6181068286S081-操作系统工程-Lab10-mmap

4次阅读

共计 6565 个字符,预计需要花费 17 分钟才能阅读完成。

Lab: mmap (hard)

mmapmunmap 零碎调用容许 UNIX 程序对其地址空间进行具体管制。它们可用于在过程之间共享内存,将文件映射到过程地址空间,以及作为用户级页面谬误计划(如课程中探讨的垃圾回收算法)的一部分。在本试验中,你将向 xv6 增加 mmapmunmap,重点关注内存映射文件。

运行 man 2 mmap 可失去手册中 mmap 的申明:

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

要求

能够通过多种形式调用 mmap,但此练习只须要与文件内存映射相干的性能子集。

  • 您能够假如 addr 将始终为零,这意味着内核应决定映射文件的虚拟地址。mmap 返回该地址,如果失败,则返回0xffffffffffffffff
  • length是要映射的字节数; 它可能与文件的长度不同。
  • prot 批示内存是否应映射为可读、可写和 / 或可执行; 您能够假如 protPROT_READ PROT_WRITE 或两者兼而有之。
  • flags要么是 MAP_SHARED,这意味着对映射内存的批改应该写回文件,要么是MAP_PRIVATE,这意味着它们不应该写回文件。你不用在flags 中实现任何其余位。
  • fd 是要映射的文件的关上文件描述符。
  • 您能够假如偏移量为零(文件中要映射的终点)。

对于映射同一 MAP_SHARED 文件的不同过程,禁止共享物理页。

munmap(addr,length)应该删除指定地址范畴内的 mmap 映射。如果过程批改了内存且为 MAP_SHARED 映射,则应首先将批改写入文件。munmap 调用可能只笼罩已被映射的区域的一部分,但您能够假如它会在开始时、结尾或整个区域勾销映射(但不会在区域两头打一个洞)。

您应该实现足够的 mmapmunmap 性能,以使 mmaptest 测试程序失常工作。无需实现mmaptest 不应用 mmap 的性能。

实现后,应会看到以下输入:

$ 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
mmap_test: ALL OK
fork_test starting
fork_test OK
mmaptest: all tests succeeded
$ usertests -q
usertests starting
...
ALL TESTS PASSED
$ 

提醒

  1. 首先向 UPROGS 增加 _mmaptest,以及mmapmunmap零碎调用,以便使 user/mmaptest.c 可能编译。当初,只需从 mmapmunmap 返回谬误。咱们在 kernel/fcntl.h 中为您定义了 PROT_READ 等。运行 mmaptest,这将在第一次 mmap 调用时失败。
  2. 惰性地填写页表,以响应页面谬误。也就是说,mmap 不应调配物理内存或读取文件。相同,请在 usertrap 中(或由 usertrap 调用)的页面错误处理代码中执行此操作,就像在惰性页面调配试验中一样。惰性的起因是确保大文件的 mmap 是疾速的,并且大于物理内存的文件的 mmap 是可能的。
  3. 跟踪 mmap 为每个过程映射的内容。定义与第 15 讲中形容的 VMA(虚拟内存区域)对应的构造,记录 mmap 创立的虚拟内存范畴的地址、长度、权限、文件等。因为 xv6 内核中没有内存分配器,因而能够申明一个固定大小的 VMA 数组,并依据须要从该数组进行调配。大小为 16 就足够了。
  4. 实现 mmap:在过程的地址空间中查找要在其中映射文件的未应用区域,并将 VMA 增加到过程的映射区域表中。VMA 应蕴含指向要映射的文件的struct file 的指针; mmap 应减少文件的援用计数,以便在敞开文件时构造不会隐没(提醒:请参阅 filedup)。运行 mmaptest:第一个 mmap 应该胜利,但第一次拜访已映射的内存会导致页面谬误并 kill mmaptest
  5. 增加代码,以在拜访已映射区域中导致的页面谬误时,调配一页物理内存,将相干文件的 4096 字节读取到该页面中,并将其映射到用户地址空间。应用 readi 读取文件,它须要一个偏移参数来读取文件(但您必须锁定 / 解锁传递给 readi 的 inode)。不要遗记在页面上正确设置权限。运行 mmaptest; 它应该达到第一个 munmap。
  6. 实现 munmap:找到地址范畴的 VMA 并勾销映射指定的页面(提醒:应用 uvmunmap)。如果 munmap 删除了前一个 mmap 的所有页面,它应该缩小相应struct file 的援用计数。如果已批改未映射的页面并且文件已映射 MAP_SHARED,请将该页面写回该文件。查看filewrite 以取得灵感。
  7. 现实状况下,您的实现只会写回真正被程序修改的 MAP_SHARED 页面。RISC-V PTE 中的脏位(D)批示是否已写入页面。然而,mmaptest 不会查看非脏页面是否没有写回; 因而,您能够在不查看 D 位的状况下从新编写页面。
  8. 批改 exit 以勾销映射过程的映射区域,就像调用 munmap 一样。运行 mmaptest; mmap_test应该通过,但可能不会通过fork_test
  9. 批改 fork 以确保子级与父级具备雷同的映射区域。不要遗记递增 VMA struct file的援用计数。在子级的页面谬误处理程序中,能够调配新的物理页面,而不是与父级共享页面。后者会更酷,但须要更多的工作。运行 mmaptest; 它应该通过 mmap_testfork_test

实现

  1. 增加 mmapmunmap零碎调用:增加零碎调用通用步骤(在用户空间申明零碎调用、增加条目,在内核空间减少零碎调用命令序号以及对应的 sys_mmap()sys_munmap()函数)
  2. proc.hstruct proc中增加映射区域:首先定义映射区域构造体 struct MapArea 及一个过程映射区域的数量:

    #define MAP_AREA_LENGTH 16
    
    struct MapArea {
        uint64 address;  // 映射开始地址
        int length;  // 映射区域长度
        int prot;  
        int flag;  
        int offset;  
        struct file* fp;  
    };

    而后在 struct proc 中增加示意映射区域的数组字段:

    struct proc {
        ...
        struct MapArea mapareas[MAP_AREA_LENGTH];
    };
  3. 实现 sys_mmap() 的性能:

    uint64 sys_mmap(void) {
        uint64 addr;
        int len, prot, flag, fd, offset, i;
        struct file* fp;
        struct proc* p = myproc();
    
        argaddr(0, &addr);
        argint(1, &len);
        argint(2, &prot);
        argint(3, &flag);
        if (argfd(4, &fd, &fp) < 0) return -1;
        argint(5, &offset);
    
        // 判断权限是否抵触
        if (!fp->writable && (prot & PROT_WRITE) && (flag & MAP_SHARED)) {printf("file is not writable!\n");
            return -1;
        }
    
        // 找闲暇 map area
        for (i = 0; i < MAP_AREA_LENGTH; ++i) {if (p->mapareas[i].address == 0) break;
        }
        if (i == MAP_AREA_LENGTH) {printf("no more map area!\n");
            return -1;
        }
    
        // 若未指定地址则须要调配地址
        if (!addr) {addr = PGROUNDUP(p->sz);
            p->sz += PGROUNDUP(len);
        }
    
        // 复制字段
        p->mapareas[i].address = addr;
        p->mapareas[i].length = len;
        p->mapareas[i].prot = prot;
        p->mapareas[i].flag = flag;
        p->mapareas[i].offset = offset;
        p->mapareas[i].fp = fp;
        // 文件援用计数 +1
        filedup(fp);
        return addr;
    }
  4. usertrap() 中辨认页面谬误并将文件内容写入对应地址空间:首先须要在 usertrap() 本来的抉择分支框架中增加页面谬误的分支,在该分支中分配内存并将文件内容写入到指定地位。这里通过 map_fill() 函数实现此性能。

    void usertrap(void) {
        ...
        if(r_scause() == 8){
            // system call
            ...
        } else if (r_scause() == 13 || r_scause() == 15) {
            // 页面谬误,须要分配内存并写入文件内容
            if (map_fill(r_stval()) == 0) {goto error;}
        } else if((which_dev = devintr()) != 0){// ok} else {
    error:
            printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
            printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
            setkilled(p);
        }
        ...
    }

    map_fill()函数实现分配内存并写入文件内容的性能:

    uint64 map_fill(uint64 va) {
        int idx, perm, prot, offset;
        struct inode* ip;
        void* new_mem;
        struct proc* p = myproc();
    
        // 找到哪个 map area
        for (idx = 0; idx < MAP_AREA_LENGTH; ++idx) {if (va >= p->mapareas[idx].address && 
                va < p->mapareas[idx].address + p->mapareas[idx].length) break;
        }
        if (idx == MAP_AREA_LENGTH) return 0;
    
        // 分配内存
        if ((new_mem = kalloc()) == 0) {return 0;}
    
        // 物理虚拟地址建设映射
        prot = p->mapareas[idx].prot;
        perm = PTE_U;
        if (prot & PROT_READ) perm |= PTE_R;
        if (prot & PROT_WRITE) perm |= PTE_W;
        if (prot & PROT_EXEC) perm |= PTE_X;
        if (mappages(p->pagetable, va, PGSIZE, (uint64)new_mem, perm) == -1) {kfree(new_mem);
            return 0;
        }
    
        // 拷贝文件内容  
        ip = p->mapareas[idx].fp->ip;
        if (ip == 0) {printf("ip == 0\n");
            return 0;
        }
        offset = p->mapareas[idx].offset;
        ilock(ip);
        if (p->mapareas[idx].length - offset > PGSIZE) {readi(ip, 1, va, offset, PGSIZE);
        } else {readi(ip, 1, va, offset, p->mapareas[idx].length - offset);
        }
        p->mapareas[idx].offset += PGSIZE;
        iunlock(ip);
        return (uint64)new_mem;
    }
  5. 实现sys_munmap()

    uint64 sys_munmap(void) {
        uint64 addr;
        int length, idx;
        struct proc* p = myproc();
        argaddr(0, &addr);
        argint(1, &length);
    
        // 找到 addr 对应的 map area
        for (idx = 0; idx < MAP_AREA_LENGTH; ++idx) {if (addr >= p->mapareas[idx].address && addr < p->mapareas[idx].address + p->mapareas[idx].length) break;
        }
        if (idx == MAP_AREA_LENGTH) return -1;
    
        // MAP_SHARED 写回文件
        if (p->mapareas[idx].flag & MAP_SHARED) {filewrite(p->mapareas[idx].fp, addr, length);
        }
    
        // 解除映射
        uvmunmap(p->pagetable, addr, PGROUNDUP(length) / PGSIZE, 1);
        
        if (PGROUNDUP(length) >= p->mapareas[idx].length) {
            // 若解除了所有映射,则文件援用计数 -1,并将地址置 0 以标记该区域闲暇
            fileclose(p->mapareas[idx].fp);
            p->mapareas[idx].address = 0;
        } else {
            // 若未解除所有映射,则调整映射区域的范畴(地址及长度)p->mapareas[idx].length -= PGROUNDUP(length);
            p->mapareas[idx].address += PGROUNDUP(length);
        }
    
        return 0;
    }
  6. exit() 中解除映射区域的映射:

    void exit(int status) {struct proc *p = myproc();
    
        if(p == initproc)
            panic("init exiting");
    
        // 解除 map area 的 mmap
        for (int i = 0; i < MAP_AREA_LENGTH; ++i) {if (p->mapareas[i].address) {
                // MAP_SHARED 写回文件
                if (p->mapareas[i].flag & MAP_SHARED) {filewrite(p->mapareas[i].fp, p->mapareas[i].address, p->mapareas[i].length);
                }
                // 解除映射
                uvmunmap(p->pagetable, p->mapareas[i].address, 
                        PGROUNDUP(p->mapareas[i].length) / PGSIZE, 1);
                fileclose(p->mapareas[i].fp);
                p->mapareas[i].address = 0;
            }
        }
        ...
    }
  7. fork() 中复制 map area

    int fork(void) {
        ...
        pid = np->pid;
    
        // 拷贝映射区
        for (i = 0; i < MAP_AREA_LENGTH; ++i) {if (p->mapareas[i].address) {np->mapareas[i] = p->mapareas[i];
                filedup(np->mapareas[i].fp); // 文件援用计数 +1
            }
        }
        ...
    }

问题

  1. panic: uvmunmap: not mapped

    起因:munmap()时,实际上可能存在局部文件内容没有拜访、因此没有触发页面谬误、没有建设映射的状况,这时候是对着没有映射过的内存解除映射,uvmunmap()对这种状况会报错。
    我的解决办法是将 uvmunmap()(*pte & PTE_V) == 0的状况由原来的 panic(...) 改为 continue。碰到没有映射过的内存时,不触发 panic 而是疏忽掉持续运行。
    fork()uvmcopy() 时碰过到相似状况也做相似解决。

后果

播种

  1. mmap()的作用是将(局部)文件内容与一段内存建设映射关系,以减速程序对文件的拜访。
  2. mmap()有两种模式,其中 MAP_SHARED 模式不仅能够读文件,还能够将程序对映射内存的改变写回到文件中(如果文件可写)
正文完
 0