思路与实现 链接到标题

添加系统调用就不多说了。

整体流程应该是这样的,lab 的提示中,要求我们定义一个 vma 结构体,vma 的定义如下;然后 lab 的提示要求我们声明一个大小为 $16$ 的 vma 数组,并按需要从该数组分配,问题来了,数组在哪里声明呢?考虑到每个进程都有自己的虚拟地址空间,因此,每个进程都有自己的 virtual memory areas,要分配 vma 的时候,应该从每个进程自己的 vma 数组进行分配,于是,我们可以考虑为 struct proc 添加 struct vma areas[NVMA] 字段。

struct vma {
    int fd;
    int rw_flag;
    uint64 start;
    uint64 cur;
    uint len;
    int state;
    int flags;
};

struct proc {
    // 已有的省略不写
    struct vma areas[NVMA];
};

vma 的定义中,start 表示起始地址,$[start, cur)$ 这一段虚拟地址(左闭右开)是已经绑定了 pp 的,pp 的数据与 file 绑定。

那么我们如何实现 sys_mmap 呢?这里可以参照 sbrk,递增 p->sz,然后仿照 allocproc,寻找状态为 UNUSED 的 vma,分配给本次 sys_mmap。注意如果文件本身是 read_only,并且以 MAP_SHARED 模式进行 map,那么 flags 不能为 PROT_WRITE,write only 的情况同理(即文件不可读)。

uint64 sys_mmap(void) {
    uint len, offset;
    int prot, flags, fd;
    struct file *f;
    // 读取参数
    if (argint(1, (int *)&len) < 0 || argint(2, &prot) < 0 || argint(3, &flags) < 0 || argint(5, (int *)&offset) < 0) {
        return -1;
    }
    if (argfd(4, &fd, &f) < 0) {
        return -1;
    }
    struct proc *p = myproc();

    if (!f->writable && (prot & PROT_WRITE) && (flags & MAP_SHARED)) {
        return BADADDR;
    }
    if (!f->readable && (prot & PROT_READ) && (flags & MAP_SHARED)) {
        return BADADDR;
    }
    struct vma *area;
    for (area = p->areas; area < p->areas + NVMA; ++area) {
        if (area->state == UNUSED) {
            // 找到了空闲的 area
            area->state = USED;
            area->rw_flag = prot;
            area->flags = flags;
            area->start = p->sz;
            area->cur = p->sz;
            area->offset = offset;
            p->sz += len;
            area->len = len;
            area->f = f;
            filedup(area->f);
            return area->start;
        }
    }
    return BADADDR;
}

这里,我将 sys_mmap 的实现写在了 sysfile.c 中,这样就可以直接使用 argfd 来获取文件描述符和对应的 file 指针了,如果定义在 sysproc.c 中,那么通过 p->ofile[fd] 也可以获取文件描述符对应的 file 指针。

内核态中,虚拟地址到物理地址是直接映射的!除了 PHYSTOP 之上的部分。

这里我将 page fault 的下的处理封装成了一个函数 mmap_handle,出现 page fault,首先查看出现 page fualt 的虚拟地址是否位于 vma 中,如果是,得到对应的 vma 的指针 area,然后调用 mmap_handle(wrong_addr, area, r_scause)

mmap_handle 的流程如下,首先判断 page fault 是否是我们所预期的,然后申请物理内存,map VP 与 PP(注意 pte_flag 不要漏掉 PTE_U),然后将文件的数据根据偏移量,读到对应的虚拟地址中去。读完之后,将 area->curarea->offset 都增加读取的数据的字节数,同时减少 area->lenarea->len 表示 vma 中还有多少空余空间。

int mmap_handle(uint64 wrong_addr, struct vma *area, uint64 scause) {
    struct proc *p = myproc();
    if (scause == 13 && !(area->rw_flag & PROT_READ)) {
        printf("read a file that can't be read\n");
        return -1;
    }
    if (scause == 15 && !(area->rw_flag & PROT_WRITE)) {
        printf("write a file that is read-only\n");
        return -1;
    }

    char *mem = kalloc();
    if (mem == 0) {
        printf("without free mem\n");
        p->killed = 1;
    }
    memset(mem, 0, PGSIZE);
    uint64 lb = PGROUNDDOWN(area->cur);
    int pte_flag = PTE_U;
    if (area->rw_flag & PROT_READ) {
        pte_flag |= PTE_R;
    }
    if (area->rw_flag & PROT_WRITE) {
        pte_flag |= PTE_W;
    }
    if (area->rw_flag & PROT_EXEC) {
        pte_flag |= PTE_X;
    }
    uint area_pg = PGSIZE - (area->cur - lb);
    printf("area_pg: %p, cur: %p\n", area_pg, area->cur);
    area_pg = area_pg <= area->len ? area_pg : area->len;

    printf("mappages\n");
    if (mappages(p->pagetable, lb, PGSIZE, (uint64)mem, pte_flag) != 0) {
        printf("mappage failed\n");
        kfree(mem);
        uvmdealloc(p->pagetable, lb + PGSIZE, lb);
        return -1;
    }

    ilock(area->f->ip);
    if (readi(area->f->ip, 0, (uint64)mem + area->cur - lb, area->offset, area_pg) < 0) {
        printf("readi failed\n");
        kfree(mem);
        return -1;
    }
    iunlock(area->f->ip);

    area->offset += area_pg;
    area->cur += area_pg;
    area->len -= area_pg;

    return 0;
}

整体流程如下,首先

对于 sys_munmap,我们先读取参数 addrlen,然后执行 unmap(addr, len);unmap 定义与 proc.c 中,在 defs.h 中声明即可,作业中已经说明了,要进行 munmap 的虚拟地址区间,要么左端点与 area 重合,要么右端点与 area 重合,如果左右端点都重合,说明整个文件都被解除 map 了,并且文件以 MAP_SHARED 方式 mmap 且可写,那么执行 filewrite(area->f, addr, len) 将这段虚拟内存的内容写回到文件中,直接全写回即可(反正作业中一次也不会 map 很多个 page);写回之后,我们要执行 fileclose(area->f) 来递减 f->refcnt

最后调用 uvmunmap,解除 VP 与 PP 的映射关系。

uint64 unmap(uint64 addr, uint len) {
    struct vma *area;
    struct proc *p = myproc();
    for (area = p->areas; area < p->areas + NVMA; ++area) {
        // 左端点与 area 起始位置重合
        if (addr == area->start) {
            area->start -= len;
            break;
        }
        if (addr + len == area->cur + area->len) {
            area->cur -= len;
            area->len += len;
            break;
        }
    }
    if (area == p->areas + NVMA) {
        printf("no matached area\n");
        return -1;
    }
    if ((area->flags & MAP_SHARED) && (area->rw_flag & PROT_WRITE)) {
        // 写回到文件中
        filewrite(area->f, addr, len);
    }
    if (area->start == area->cur) {
        fileclose(area->f);
        area->state = UNUSED;
    }
    uvmunmap(p->pagetable, PGROUNDDOWN(addr), len / PGSIZE, 0);
    return 0;
}

我们还要修改 exitfork。首先修改 exit,在 exit 时,遍历 p->areas,如果当前 area 不是 UNUSED,那么调用 unmap 解除当前 area 的映射。在 fork 中,如果父进程的 area 是 USED,那么我们需要将其中的数据拷贝给子进程。

void exit(int status) {
    // 省略
    // Close all open files.
    for (int fd = 0; fd < NOFILE; fd++) {
        if (p->ofile[fd]) {
            struct file *f = p->ofile[fd];
            fileclose(f);
            p->ofile[fd] = 0;
        }
    }
    struct vma *area;
    for (area = p->areas; area < p->areas + NVMA; ++area) {
        if (area->state == USED) {
            unmap(area->start, area->cur - area->start);
        }
    }

    begin_op();
    iput(p->cwd);
    end_op();
}

int fork(void) {
    // 省略
    // increment reference counts on open file descriptors.
    for (i = 0; i < NOFILE; i++)
        if (p->ofile[i])
            np->ofile[i] = filedup(p->ofile[i]);
    np->cwd = idup(p->cwd);

    // 父进程与子进程具有相同 vma
    struct vma *area = p->areas;
    for (int i = 0; i < NVMA; ++i) {
        if (area[i].state == USED) {
            // np->areas[i].state = p->areas[i].state;
            // np->areas[i].rw_flag = p->areas[i].rw_flag;
            // np->areas[i].flags = p->areas[i].flags;
            // np->areas[i].start = p->areas[i].start;
            // np->areas[i].cur = p->areas[i].cur;
            // np->areas[i].offset = p->areas[i].offset;
            // np->areas[i].len = p->areas[i].len;
            // np->areas[i].f = p->areas[i].f;
            memmove()
            filedup(area->f);
        }
    }
    safestrcpy(np->name, p->name, sizeof(p->name));

    // 省略
}

最后,和 copy-on-write fork 一样,我们需要修改 uvmunmapuvmcopy,如果发现 pte 不是 valid,跳过本次循环即可。

总结 链接到标题

之前看 csapp 的虚拟内存那一章的时候,书里面有提到 virtual memory area 的概念,当时不是很理解为什么需要一个这玩意,做了本次 lab 之后,对 linux 中的 vm_area 也算有所理解,本 lab 中,vm_area 是直接以数组形式存储的,每次都要 $O(n)$ 的时间才能找到对应的 area,而在 Linux 中,vm_area 是以红黑树的形式组织的,找到 vaddr 对应的 area,只需要 $O(\log n)$ 的时间。(当然,这也是因为我们只有 $16$ 个 area,而 Linux 中 area 的数量要多得多)。