lab10 mmap

image-20230829232154575

task


mmap的定义如下

void *mmap(void *addr, size_t length, int prot, int flags,
           int fd, off_t offset);
  1. 在这个lab中,addr永远为0,即内核决定用哪个虚拟地址去映射这个文件,mmap返回这个虚拟地址,或者0xffffffffffffffff 表示失败

  2. length代表映射的字节数量,不一定要是文件的长度

  3. prot决定了这个内存,可读,可写或者可执行

    具体的宏为PROT_READ、PROT_WRITE

  4. flags如果是MAP_SHARED,意味着对内存的修改要写回到文件

    MAP_PRIVATE意味着不用写回文件

  5. fd是文件的描述符,你可以假设offset是0


munmap的定义如下

munmap(addr, length)
  1. munmap应该移出这些地址范围内的映射
  2. 如果进程修改了内存,并且映射方式是MAP_SHARED,那么修改应该被写回文件
  3. munmap可能只覆盖了mmap的一部分区间,可以是开始,可以是末尾,也可能是整个,但不会是中间位置

hints

  1. 首先在Makefile中添加_mmaptest,并且增加mmap和munmap系统调用

    kernel/fcntl.h为你定义了PROT_READ等参数

  2. lazy地对待页表,类似于lazy lab。

    即mmap并不会直接分配物理内存和读文件,在usertrap中处理页错误时再真正的分配内存

  3. 你需要记录每个进程通过mmap映射了什么

    你可以定义一个和VMA相关的数据结构,去记录地址,长度,权限,文件等

    你可以定义一个固定长度的VMA数组,16就足够了

  4. 实现mmap

    1. 在进程的地址空间找一块未使用的区域去映射文件

    2. 并且增加一个VMA到进程映射区域的表中

      1. VMA应该包含一个指向struct file的指针。mmap应该增加这个文件的引用,这一部分可以参考filedup
    3. 这时候运行mmaptest,可以发现第一个成功了,但是后面的还是失败了

  5. 增加代码

    1. 在页错误发生在mapped区域时,分配一个物理页面,从相关的文件中读取4096个字节,并且将其映射到用户地址空间
    2. 用readi读取文件,它会使用到一个offset参数,同时你需要将inode结点给lock和unlock
    3. 不要忘记设置这一页的权限位
    4. 运行mmaptest,这时候会运行到munmap了
  6. 实现munmap

    1. 找到这个地址范围内的VMA,unmap指定的页面,使用uvmunmap
    2. 如果munmap删除了mmap分配的所有区域,那它应该减少文件的引用次数
    3. 如果一个页面是MAP_SHARED,并且被修改了,那么应该写回文件(学习filewrite)。写回时不需要管pte的dirty位
  7. 修改exit函数,使其能够在进程用过mmap的情况下,将没有被munmap都处理掉,至此mmap_test可能可以通过了

  8. 修改fork保证孩子也有和父节点一样的映射区域,不要忘记了去给VMA文件增加引用数。在页错误发生时,可以分配一个新的物理页面,而不是和父进程共享一个。

思路

基础设置

首先需要修改Makefile并且添加两个系统调用,这个就比较简单了

增加数据结构

我们应该按照hints提示的,创造一个VMA的结构体,并且在进程的proc的结构体中存储一个VMA的数组。这里的思路就比较简单暴力了,就只维护一个数组,需要用的时候就遍历数组去找就行了。

struct vma {
    uint64 addr;
    int length;
    int prot;
    int flag;
    int fd;
    struct file *file;
    int valid;
};

struct proc {
    struct vma vmas[16];
    //.......
}

sys_mmap

开始写mmap函数

  1. 这玩意的作用就是只申请一个虚拟地址空间,但是不直接分配物理地址。在申请完之后,就将相关的东西存在proc的VMA数组中去
  2. 有几个情况需要特判
    1. 如果文件不可读,那么就不能用MAP_SHARED,因为这种模式在之后会写入到磁盘
    2. 地址不够了,即p->sz>MAXVA
  3. 注意点
    1. 只分配虚拟内存,不增加物理内存,就是通过只增加p->sz而不真正的映射实现的。包括页表中都没有相关的记录
    2. argfd取出文件指针
    3. 记得用filedup增加文件的引用计数
uint64
sys_mmap(void) {
    uint64 addr;
    struct file *file;
    int length, prot, flags, offest;
    uint64 erro = 0xffffffffffffffff;

    // 取出参数
    if (argaddr(0, &addr) < 0 || argint(1, &length) < 0
        || argint(2, &prot) < 0 || argint(3, &flags) < 0
        || argfd(4, 0, &file) < 0 || argint(5, &offest) < 0) {
        return erro;
    }
    // 权限不对
    if (file->writable == 0 && flags == MAP_SHARED && (prot & PROT_WRITE)) {
        return erro;
    }
    // 地址不够了
    struct proc *p = myproc();
    if (p->sz + length > MAXVA) {
        return erro;
    }
    // 分配地址,找出一个空闲的vma
    addr = p->sz;
    p->sz += length;
    for (int i = 0; i < 16; i++) {
        if (p->vmas[i].valid == 0) {
            p->vmas[i].valid = 1;
            p->vmas[i].addr = addr;
            p->vmas[i].flag = flags;
            p->vmas[i].length = length;
            p->vmas[i].prot = prot;
            p->vmas[i].file = file;
            filedup(file);
            return addr;
        }
    }

    return erro;
}

usertrap

修改usertrap函数,使得vma相关的地址能够被正确处理

  1. 首先应该通过r_scause捕获异常
  2. 判断地址的合法性
    1. 压根不在vma管辖范围内
      1. 不能超出当前进程的虚拟地址范围,即p->sz
      2. 不能低于栈区
      3. 在进程的vma数组中找不到对应的地址
    2. 权限合法性
      1. 通过r_scause可以知道当前是读还是写操作,通过文件的类型,可以确定是否有这个权限
  3. kalloc分配一个物理页面
  4. readi将文件的内容读入物理页面,其中偏移部分通过当前地址和vma记录的addr做差值得到,因为一读就是一个页面,因此还要将这个偏移向PGSIZE舍入
  5. 将映射关系通过uvmmap写入到页表
else if (r_scause() == 13 || r_scause() == 15) {
        // #ifdef LAB_MMAP
        if (mmap_handler(r_stval(), r_scause()) == -1) {
            p->killed = -1;
        }
        // #endif
    } else if ((which_dev = devintr()) != 0) {
    
    
int mmap_handler(uint64 va, uint64 r_cause) {
    struct proc *p = myproc();
    // 地址不合法
    if (va >= p->sz || va < p->trapframe->sp) {
        return -1;
    }
    // 是否和vma有关系
    struct vma *vma = 0;
    for (int i = 0; i < 16; i++) {
        if (p->vmas[i].valid == 1 && (va >= p->vmas[i].addr && va < p->vmas[i].addr + p->vmas[i].length)) {
            vma = &p->vmas[i];
            break;
        }
    }
    // 和vma没关系
    if (vma == 0) {
        return -1;
    }
    // 和vma是有关的
    // 看看权限是否正确
    struct file *file = vma->file;
    // 不能读,但是你读了
    if (!file->readable && r_cause == 13) {
        return -1;
    }
    // 不能写,但是你写了
    if (!file->writable && r_cause == 15) {
        return -1;
    }
    // 构建pte的标志位
    int pte_flag = PTE_U;
    if (vma->prot & PROT_READ) {
        pte_flag |= PTE_R;
    }
    if (vma->prot & PROT_WRITE) {
        pte_flag |= PTE_W;
    }
    if (vma->prot & PROT_EXEC) {
        pte_flag |= PTE_X;
    }
    // 先分配一个物理页面
    uint64 pa = (uint64)kalloc();
    if (pa == 0) {
        return -1;
    }
    memset((void *)pa, 0, PGSIZE);
    // 成功分配物理页面,从文件中读
    struct inode *ip = file->ip;
    ilock(ip);
    // 读取失败
    if (readi(ip, 0, pa, PGROUNDDOWN(va - vma->addr), PGSIZE) == 0) {
        iunlock(ip);
        kfree((void *)pa);
        return -1;
    }
    iunlock(ip);
    // 加入映射
    if (mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, pa, pte_flag) != 0) {
        kfree((void *)pa);
        return -1;
    }

    return 0;
}

munmap

lab对munmap的情况做了简化,只会从头开始unmap,因此在将数据写回磁盘时,直接调用filewrite函数,它在内部会自动调用file的偏移

munmap就是取消某部分虚拟地址的mmap

  1. 在进程的vma数组找到对应的vma
  2. 更新vma中的addr和length,如果当前length=0,说明全部被写入了,那么就通过fileclose关闭这个文件,并且将这个vma的valid修改为0。这个关闭的操作最好放到最后,因为我们可能在第3步还要写这个文件
  3. 如果需要写入,则通过filewrite函数写入磁盘,这个函数的参数很简单,第一个是文件的指针,第二个是起始的虚拟地址,第三个是length
  4. 将进程的页表给uvmunmap掉,因为lab的仁慈,这里的参数传递也很简单,addr全都是PGSIZE的倍数
uint64
sys_munmap(void) {
    uint64 addr;
    int length;
    if (argaddr(0, &addr) < 0 || argint(1, &length) < 0) {
        return -1;
    }
    // 先找到对应vma
    struct proc *p = myproc();
    struct vma *vma = 0;
    for (int i = 0; i < 16; i++) {
        if (p->vmas[i].valid == 1 && (addr >= p->vmas[i].addr && addr < p->vmas[i].addr + p->vmas[i].length)) {
            vma = &p->vmas[i];
            // 头部
            if (vma->addr == addr) {
                vma->addr += length;
                vma->length -= length;
                // 尾部
            } else if (addr + length == vma->addr + vma->length) {
                vma->length -= length;
            }
            break;
        }
    }
    if (vma == 0) {
        return -1;
    }
    // 如果是shared,需要先写回磁盘
    if (vma->flag == MAP_SHARED) {
        filewrite(vma->file, addr, length);
    }
    // 修改页表
    uvmunmap(p->pagetable, addr, length / PGSIZE, 1);
    //如果map区域为0
    if (vma->length == 0) {
        fileclose(vma->file);
        vma->valid = 0;
    }

    return 0;
}

这里有几个小补丁要打,分别是uvmunmapuvmcopy,它们在查找页表的时候,如果发现pte无效,会panic,这里直接忽略,因为可能是mmap还没有分配物理地址的区域

exit

这个函数是用于进程死亡时,就所有的mmap区域都给删掉

  1. 遍历进程的所有vma
  2. 如果vma有效
    1. 如果需要写入磁盘,那就写入磁盘
    2. 更新页表
    3. 关闭文件
    4. 设置vma无效
    // 将所有的映射区取消
    for (int i = 0; i < 16; i++) {
        if (p->vmas[i].valid) {
            struct vma *vma = &p->vmas[i];
            if (vma->flag == MAP_SHARED) {
                filewrite(vma->file, vma->addr, vma->length);
            }
            fileclose(vma->file);
            vma->valid = 0;
            uvmunmap(p->pagetable, vma->addr, vma->length / PGSIZE, 1);
        }
    }

fork

这个函数主要用于子进程将父进程的mmap区域都给拷贝过来,要不然子进程一旦访问还没有分配物理地址的mmap区域,在usertrap里就不能正确处理它了

    // 将父进程的mmap也拷贝给它
    for (int i = 0; i < 16; i++) {
        struct vma *vma = &p->vmas[i];
        if (vma->valid) {
            memmove(&np->vmas[i], vma, sizeof(struct vma));
            filedup(vma->file);
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值