惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The Hacker News
The Hacker News
F
Full Disclosure
Cloudbric
Cloudbric
Blog — PlanetScale
Blog — PlanetScale
W
WeLiveSecurity
N
News and Events Feed by Topic
T
Troy Hunt's Blog
V2EX - 技术
V2EX - 技术
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
B
Blog
GbyAI
GbyAI
C
Check Point Blog
B
Blog RSS Feed
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Recorded Future
Recorded Future
The Last Watchdog
The Last Watchdog
N
News and Events Feed by Topic
T
The Blog of Author Tim Ferriss
O
OpenAI News
V
V2EX
人人都是产品经理
人人都是产品经理
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
IT之家
IT之家
WordPress大学
WordPress大学
www.infosecurity-magazine.com
www.infosecurity-magazine.com
S
Security @ Cisco Blogs
C
Cisco Blogs
Security Latest
Security Latest
S
Security Affairs
V
Visual Studio Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Hacker News - Newest:
Hacker News - Newest: "LLM"
博客园 - 司徒正美
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Microsoft Azure Blog
Microsoft Azure Blog
Last Week in AI
Last Week in AI
AWS News Blog
AWS News Blog
雷峰网
雷峰网
Apple Machine Learning Research
Apple Machine Learning Research
PCI Perspectives
PCI Perspectives
博客园_首页
U
Unit 42
Google DeepMind News
Google DeepMind News
Hugging Face - Blog
Hugging Face - Blog
Project Zero
Project Zero
Cisco Talos Blog
Cisco Talos Blog
The Register - Security
The Register - Security
N
Netflix TechBlog - Medium
L
LINUX DO - 热门话题
H
Hacker News: Front Page

VnYzm的博客

游戏玩后感:ReLief:献给亲爱的你 我的周边(谷子)分享 游戏玩后感:Kanon 简谱:致真实的你 《Rust中常见的有关生命周期的误解》学习笔记 简谱:StarMap 简谱:かく咲きたらばいと恋ひめやも 简谱:东风 简谱:无法诉说的思念 简谱:Girlish 游戏玩后感:时钟机关的Layline 简谱:风之琶音 简谱:星空的记忆 简谱:因为遇见了你 简谱:月童 番茄简谱脚本转调器 游戏玩后感:青空下的约定:Refine 游戏玩后感:在这苍穹展翅 书籍读后感:控制论与科学方法论 游戏玩后感:恋爱表达式 游戏玩后感:樱之诗 MLIR-tutorial学习笔记 游戏玩后感:潜伏之赤途 游戏玩后感:纯爱咖啡厅:帕露菲重制版 游戏玩后感:智以泪聚 游戏玩后感:初雪樱 游戏玩后感:告别回忆:从今以后 游戏玩后感:梦灯花 游戏玩后感:金辉恋曲四重奏 游戏玩后感:五彩斑斓的世界 昇腾310P使用记录 游戏玩后感:AIR 游戏玩后感:弹丸论破 游戏玩后感:流景之海的艾佩莉亚 Xilinx_HLS上板过程记录 游戏玩后感:告别回忆2 游戏玩后感:恋爱绮谭 Faiss和Rapidsai_Raft使用记录 游戏玩后感:近月少女的礼仪 游戏玩后感:樱色之云,绯色之恋 游戏玩后感:幸运草的约定 游戏玩后感:星之梦、候鸟和丸子与银河龙 游戏玩后感:白色相簿2 Windows上使用VTune分析PyTorchExtension调用的Cpp程序 SpinalHDL上板过程记录 游戏玩后感:仰望夜空的星辰 最简单的算卦方法之一:梅花易数法 游戏玩后感:苍之彼方的四重奏 krkr引擎解包工具介绍 自定义CUDA实现PyTorch算子的四种简单方法 游戏玩后感:星空的记忆 游戏玩后感:9nine 游戏玩后感:AtriMyDearMoments 游戏玩后感:极限脱出 游戏玩后感:魔女的夜宴 SSH实现多跳代理 动漫观后感:向山进发 flv重封装H264、AAC流 动漫观后感:夏日重现 CSP模板 游戏玩后感:海沙风云 动漫观后感:灵能百分百 游戏玩后感:交响乐之雨 游戏玩后感:爱上火车LastRun 游戏玩后感:LittleBustersEX 游戏玩后感:SummerPockets 游戏玩后感:逆转裁判 Ultra96V2开发板简单使用 SpinalWorkshop实验笔记(三) SpinalWorkshop实验笔记(二) SpinalWorkshop实验笔记(一) PYNQ开发板上使用USB声卡+OSS兼容层播放音频 TestOS移植K210开发板 rCore-Tutorial-Book-v3学习笔记(七) 动漫观后感:凉宫春日的忧郁 rCore-Tutorial-Book-v3学习笔记(♭七) rCore-Tutorial-Book-v3学习笔记(六) rCore-Tutorial-Book-v3学习笔记(五) rCore-Tutorial-Book-v3学习笔记(四) rCore-Tutorial-Book-v3学习笔记(三) rCore-Tutorial-Book-v3学习笔记(二) rCore-Tutorial-Book-v3学习笔记(一) 游戏玩后感:RewritePlus MIT-6.S081-2020实验(xv6-riscv64)十一:net MIT-6.S081-2020实验(xv6-riscv64)九:fs MIT-6.S081-2020实验(xv6-riscv64)八:lock MIT-6.S081-2020实验(xv6-riscv64)七:thread MIT-6.S081-2020实验(xv6-riscv64)六:cow MIT-6.S081-2020实验(xv6-riscv64)五:lazy MIT-6.S081-2020实验(xv6-riscv64)四:traps MIT-6.S081-2020实验(xv6-riscv64)三:pgtbl MIT-6.S081-2020实验(xv6-riscv64)二:syscall 动漫观后感:吹响吧上低音号 MIT-6.S081-2020实验(xv6-riscv64)一:util 快速生成网络mp4视频缩略图技术 用plantuml画图示例 QQ缩略图和大图不同实现 Python制作字符图片 动漫观后感:命运石之门 使用OpenCV对图片进行特征点检测和匹配
MIT-6.S081-2020实验(xv6-riscv64)十:mmap
VnYzm · 2021-01-13 · via VnYzm的博客

实验文档

概述

这次实验要求实现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参数支持,代码量就更大了,甚至可以发展成完整的硬盘虚拟内存技术……