Mit6.S081笔记Lab10: mmap 文件内存映射

news/2024/11/19 17:02:11/文章来源:https://www.cnblogs.com/Amroning/p/18555179

课程地址:https://pdos.csail.mit.edu/6.S081/2020/schedule.html
Lab 地址:https://pdos.csail.mit.edu/6.S081/2020/labs/mmap.html
我的代码地址:https://github.com/Amroning/MIT6.S081/tree/mmap
xv6手册:https://pdos.csail.mit.edu/6.S081/2020/xv6/book-riscv-rev1.pdf
相关翻译:https://xv6.dgs.zone/labs/requirements/lab10.html
参考博客:https://blog.miigon.net/posts/s081-lab10-mmap/

学习笔记记录,如有错误恳请各位大佬指正

Lab10: mmap

​ 仿照Linux实现mmap功能,即将文件映射到进程地址空间,如果进程修改了这部分内存,并且内存标记为映射内存的修改应写回文件,那么释放映射前需要把修改写入源文件。这样与文件交互的时候就可以减少磁盘操作。该实验需要用到很多前面实验的知识点

完整题目要求请去顶部链接查看

mmap(hard)

您应该实现足够的mmapmunmap功能,以使mmaptest测试程序正常工作。如果mmaptest不会用到某个mmap的特性,则不需要实现该特性。

​ 进程所使用的内存空间从低地址向高地址生长(sbrk调用),范围是stack到trapframe。为了不和进程使用的内存空间冲突,将mmap使用的地址空间映射到trapframe下面的页,从上往下生长。先定义mmap最后一页的地址:

// kernel/memlayout.h
// MMAP 进程映射文件内存最后一个页(开区间)
#define MMAPEND TRAPFRAME

定义一个vma结构体,表示虚拟内存区域,用来记录mmap创建的虚拟内存地址的范围、长度、权限、文件等。再声明一个vma结构体的数组,当mmap映射操作时,就来这个数组获取vma虚拟内存区域:

// kernel/proc.h
// 定义一个虚拟内存区域结构体,用来记录mmap创建的虚拟内存地址的范围、长度、权限、文件等
struct vma {int valid;              // 该虚拟内存区域是否已被映射uint64 vastart;         // 该虚拟内存区域开始地址uint64 sz;              // 该虚拟内存区域大小struct file* f;         // 该虚拟内存区域映射的文件int prot;               // 该虚拟内存区域权限int flags;              // 标记映射内存的修改是否写回文件uint64 offset;          // 映射文件的起点
};#define NVMA 16             // VMA数组大小// Per-process state
struct proc {struct spinlock lock;......struct vma vmas[NVMA];       // mmap虚拟内存映射地址数组
};

​ 接下来实现mmap的系统调用。这个实验要做的事挺多,最后再添加系统调用的声明。参考Linux的mmap函数,需要在进程的vmas数组中遍历寻找空闲的vma,遍历的过程中也要计算当前正在使用的所有vma的最低地址,这是为了后面添加新的vma。找到空闲的vma后,设置他的地址为刚才找到的最低地址减去sz(因为mmap的地址是从高到低生长)。然后需要调用filedup函数将映射文件的引用数+1

​ 调用mmap函数的时候需要注意文件权限问题。如果文件不可读,vma映射为可读,则mmap失败;如果文件不可写,vma映射为可写,并且开启了回盘标志(MAP_SHARED),则mmap失败。据此写出mmap系统调用函数:

// kernel/sysfile.c
// mmap系统调用实现
uint sys_mmap(void) {uint64 addr, sz, offset;int prot, flag, fd;struct file* f;// 读取传入参数if (argaddr(0, &addr) < 0 || argaddr(1, &sz) < 0 || argint(2, &prot) < 0 || argint(3, &flag) < 0 || argfd(4, &fd, &f) < 0 || argaddr(5, &offset) < 0 || sz == 0)return -1;// 以下情况直接返回-1:if ((!f->readable && ((prot & (PROT_READ))))                                // 源文件不可读,vma映射为可读|| (!f->writable && (prot & PROT_WRITE) && !(flag & MAP_PRIVATE)))      // 源文件不可写 ,vam映射为可写并且设置了将修改写回源文件return -1;sz = PGROUNDUP(sz);struct proc* p = myproc();struct vma* v = 0;uint64 vaend = MMAPEND;// 遍历查询未被使用的vma,并且计算当前已使用的va的最低地址for (int i = 0;i < NVMA;++i) {struct vma* vv = &p->vmas[i];if (vv->valid == 0) {           // 若找到了空闲vma就保存下来if (v == 0) {v = &p->vmas[i];v->valid = 1;}}else if (vv->vastart < vaend) {vaend = PGROUNDDOWN(vv->vastart);}}// 没找到空闲的vmaif (v == 0)panic("mmap: no free vma");// 设置vma属性v->vastart = vaend - sz;v->sz = sz;v->f = f;v->prot = prot;v->flags = flag;v->offset = offset;// 增加源文件引用数filedup(v->f);return v->vastart;
}

kernel/fcntl.h中定义好了相关宏,编译器提示未定义标识符可以不用管:

// kernel/fcntl.h
#ifdef LAB_MMAP
#define PROT_NONE       0x0
#define PROT_READ       0x1
#define PROT_WRITE      0x2
#define PROT_EXEC       0x4#define MAP_SHARED      0x01
#define MAP_PRIVATE     0x02
#endif

​ 映射功能使用写时复制机制实现,即只有在访问的时候才进行磁盘操作。具体原理参考Lab5:

// kernel/sysfile.c
// 通过虚拟地址寻到对应的vma
struct vma* findvma(struct proc* p, uint64 va) {for (int i = 0;i < NVMA;++i) {struct vma* vv = &p->vmas[i];// 如果va地址在某一个vma范围内,则返回这个vmaif (vv->valid == 1 && va >= vv->vastart && va < vv->vastart + vv->sz) {return vv;}}return 0;
}// 给虚拟地址分配物理页并建立映射
int vmaalloc(uint64 va) {struct proc* p = myproc();struct vma* v = findvma(p, va);if (v == 0)return 0;// 分配物理地址void* pa = kalloc();if (pa == 0)panic("vmaalloc:kalloc");memset(pa, 0, PGSIZE);// 从磁盘读取文件begin_op();ilock(v->f->ip);readi(v->f->ip, 0, (uint64)pa, v->offset + PGROUNDDOWN(va - v->vastart), PGSIZE);iunlock(v->f->ip);end_op();// 建立映射if (mappages(p->pagetable, va, PGSIZE, (uint64)pa, PTE_R | PTE_W | PTE_U) < 0)panic("vmaalloc: mappages");return 1;
}
// kernel/trap.c
void
usertrap(void)
{......else if ((which_dev = devintr()) != 0) {// ok}else if (r_scause() == 13 || r_scause() == 15){uint64 va = r_stval();        // 读取当前发生页面错误的地址if (vmaalloc(va) == 0)panic("usertrap: wrong va");}......
}

​ 接下来需要实现另一个系统调用munmap,释放所有的vma。如果设置了MAP_SHARED,还需要将修改写回磁盘源文件。

munmap传入的参数为释放映射的地址addr,释放地址的范围大小sz。需要检测释放的区域,取消映射的位置要么在区域起始位置,要么在区域结束位置,要么就是整个区域,但是不能在vma中间“打洞”。页有可能不是完整释放,如果 addr 处于一个页的中间,则那个页的后半部分释放,但是前半部分不释放,此时该页整体不应该被释放:

// kernel/sysfile.c
// 释放vma映射的页
uint64 sys_munmap(void) {uint64 addr, sz;if (argaddr(0, &addr) < 0 || argaddr(1, &sz) < 0 || sz == 0)return -1;struct proc* p = myproc();struct vma* v = findvma(p, addr);if (v == 0)return -1;if (addr > v->vastart && addr + sz < v->vastart + v->sz)        // 释放的区域不能在vma中“打洞”return -1;uint64 addr_alinged = addr;if (addr > v->vastart)addr_alinged = PGROUNDUP(addr);int nunmap = sz - (addr_alinged - addr);            // 计算要释放的字节数if (nunmap < 0)nunmap = 0;vmaunmap(p->pagetable, addr_alinged, nunmap, v);    // 从addr_alinged开始释放nunmap字节数if (addr <= v->vastart && addr + sz > v->vastart) {v->offset += addr + sz - v->vastart;v->vastart = addr + sz;}v->sz -= sz;if (v->sz <= 0) {fileclose(v->f);v->valid = 0;}return 0;
}

vmaunmap函数实现释放映射功能。释放映射之后,需要更新对应vma的offset、vastart、sz字段。如果释放完了vma的sz大小的范围,则应该关闭文件的引用,释放该vma。

vmaunmap函数仿照uvmunmap函数实现, 从传入的参数虚拟地址va开始遍历,查找va + nbytes范围内的每一个页,检查这个页是否被修改过,并且该vma设置了回盘MAP_SHARED,则需要把修改写回磁盘。注意不是每一个页都需要完整的写回,这里需要处理开头页不完整、结尾页不完整以及中间完整页的情况

​ 先加上PTE_D标志位,表示页表被修改过:

// kernel/riscv.h
#define PTE_V (1L << 0) // valid
#define PTE_R (1L << 1)
#define PTE_W (1L << 2)
#define PTE_X (1L << 3)
#define PTE_U (1L << 4) // 1 -> user can access
#define PTE_G (1L << 5) 
#define PTE_A (1L << 6) 
#define PTE_D (1L << 7) // 页表被修改过

​ 实现vmaunmap函数:

// kernel/vm.c
// 添加必要的头文件
#include "fcntl.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "file.h"
#include "proc.h"// 释放mmap映射的页,根据PTE_D和MAP_SHARED判断是否将修改写回磁盘
void vmaunmap(pagetable_t pagetable, uint64 va, uint64 nbytes, struct vma* v) {uint64 a;pte_t* pte;for (a = va;a < va + nbytes;a += PGSIZE) {if ((pte = walk(pagetable, a, 0)) == 0)     // 读取va对应ptecontinue;if (PTE_FLAGS(*pte) == PTE_V)panic("sys_munmap: not a leaf");if (*pte & PTE_V) {uint64 pa = PTE2PA(*pte);if ((*pte & PTE_D) && (v->flags & MAP_SHARED)) {        // 将修改写回磁盘begin_op();ilock(v->f->ip);uint64 aoff = a - v->vastart;                       // 相对于vma的偏移量if (aoff < 0)writei(v->f->ip, 0, pa + (-aoff), v->offset, PGSIZE + aoff);        // 第一页是不满PGSIZE的一个页else if (aoff + PGSIZE > v->sz)writei(v->f->ip, 0, pa, v->offset + aoff, v->sz - aoff);            // 最后一页是不满PGSIZE的一个页elsewritei(v->f->ip, 0, pa, v->offset + aoff, PGSIZE);iunlock(v->f->ip);end_op();}kfree((void*)pa);*pte = 0;}}
}

​ 在proc.c中需要添加对vma的处理

​ 首先是初始化进程时,需要初始化一个进程的vmas数组:

// kernel/proc.c
static struct proc*
allocproc(void)
{......// 初始化时清空vmas数组for (int i = 0;i < NVMA;++i)p->vmas[i].valid = 0;return p;
}

​ 释放进程时,要在释放页表前清空vmas数组:

// kernel/proc.c
static void
freeproc(struct proc *p)
{if(p->trapframe)kfree((void*)p->trapframe);p->trapframe = 0;for (int i = 0;i < NVMA;++i) {                        // 释放页表前把vmas数组清空struct vma* v = &p->vmas[i];vmaunmap(p->pagetable, v->vastart, v->sz, v);}if (p->pagetable)proc_freepagetable(p->pagetable, p->sz);......
}

​ fork创建子进程时,子进程复制父进程的vmas数组,不复制物理页:

// kernel/proc.c
int
fork(void)
{......// 父进程vmas复制到子进程中,实际内存页和pte不会被复制for (int i = 0;i < NVMA;++i) {struct vma* v = &p->vmas[i];if (v->valid) {np->vmas[i] = *v;filedup(v->f);}}safestrcpy(np->name, p->name, sizeof(p->name));pid = np->pid;np->state = RUNNABLE;release(&np->lock);return pid;
}

​ 主体做好了,现在添加系统调用声明

​ user.h:

// mmaptest中调用mmap时需要返回char*,这里可以把返回值设置为void*
void* mmap(void* addr, uint sz, int prot, int flag, int fd, uint offset);
int munmap(void* addr, uint sz);

usys.pl:

entry("mmap");
entry("munmap");

syscall.h:

#define SYS_mmap   22
#define SYS_munmap 23

syscall.c:

extern uint64 sys_mmap(void);
extern uint64 sys_munmap(void);static uint64 (*syscalls[])(void) = {
......
[SYS_mmap]    sys_mmap,
[SYS_munmap]  sys_munmap,
};

​ 此时可以验证实验是否通过

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/836877.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

.net 非阻塞的异步编程 及 线程调度过程

本文主要分为三个部分: 1、语法格式 2、线程调度情况 3、编程注意事项 * 阅读提示 :鼠标悬停在 章节标题 上可见 文章目录异步编程(Task Asynchronous Programming,TAP),一种编程模式(Task-based Asynchronous Pattern)。 TAP 是 .NET 中推荐的异步编程模式,基于 Task…

微软Office 2021 24年11月授权版

概述 Microsoft Office LTSC 2021 专业增强版是微软公司推出的一款专为企业客户设计的办公软件套件。该版本于2024年11月进行了批量许可版更新推送,旨在为企业用户提供更加稳定、高效的办公体验。主要特点LOGO设计趋势强化:新版Office将棱角改为圆角风格,提高了企业的辨识度…

OpenStack制作镜像

Ubuntu镜像的制作采用的是IOS 安装 转qcow2 上传OpenStack使用。 1.环境介绍 主机:Ubuntu 22.04 工具:QEMU+KVM 镜像类型:Ubuntu 22.04 工具:VNC-Client、系统镜像ISO或者IMG格式等,这里使用:ubuntu-22.04.5-live-server-amd64.iso2. 环境准备 root@node3:~/t# cat /proc…

ReNamer Pro 7.5 中文绿色便携专业版-文件重命名工具

前言 我们日常生活和工作中所涉及的文件数量日益增多。无论是图片、音频、视频还是各种文档,这些文件在存储、管理和分享时,都需要有一个清晰、有序的文件命名规则。然而,手动重命名大量文件不仅耗时耗力,而且容易出错,这对于追求效率和准确性的现代生活来说显然是不现实的…

接口测试之fiddler

二、Fiddler 简介 fiddler 是 C# 开发免费web调试工具之一,记录所有客户端和服务端常见的 http 以及 https 请求,可监视设断点,甚至修改输入输出数据,它还包含了一个强大的基于事件脚本的子系统,并且能使用 .net 语言来拓展。 Fiddler也是一款专用的抓包工具,也是一个调试…

DHCP介绍与实现方法

简介:动态主机配置协议(Dynamic Host Configuration Protocol,缩写:DHCP)是 RFC 1541(已被 RFC 2131 取代)定义的标准协议,该协议允许服务器向客户端动态分配 IP 地址和配置信息。 工作原理: DHCP协议支持C/S(客户端/服务器)结构,主要分为两部分: 1、DHCP客户端:…

推荐一个好用的 REST API 测试工具 Apifox

大家好啊!今天给大家安利一个超级好用的 REST API 测试工具 —— Apifox。说实话,作为一个经常和 API 打交道的开发者,以前总是被各种 API 测试和管理的问题困扰。直到遇到了 Apifox,才发现原来 API 测试可以这么舒服! Apifox 是啥? 简单来说,Apifox 就是一个"一站…

Qwen2.5-0.5B-Instruct搭建

模型地址 https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct简介 通义千问新一代开源模型Qwen2.5,旗舰模型Qwen2.5-72B性能超越Llama 405B,再登全球开源大模型王座。Qwen2.5全系列涵盖多个尺寸的大语言模型、多模态模型、数学模型和代码模型,每个尺寸都有基础版本、指令跟…

Pod环境安装(Mac)

原文链接:https://blog.csdn.net/huwan12345/article/details/135088993 背景知识: 安装pods需要依赖 ruby 环境,而安装 ruby 需要借助能够管理不同版本的 ruby工具 rvm,安装 rvm 又需要借助工具 Homebrew,Homebrew是一款Mac OS平台下的软件包管理工具,拥有安装、卸载、更…

CI配置项,IT服务的关键要素

随着现今数字经济的不断发展,逐渐成熟的IT 基础设施已不再是简单的竞争优势,而已成为企业生存和发展的基石。然而,仅仅拥有强大的基础设施是不够的。为了保障 IT 服务的平稳运行和持续交付,企业还需要重点关注 IT 服务的核心构建模块——配置项(Configuration Item,CI)。…

类的组合、继承、模板类、标准库

任务2 GradeCalc.hpp1 #include <iostream>2 #include <vector>3 #include <string>4 #include <algorithm>5 #include <numeric>6 #include <iomanip>7 8 using std::vector;9 using std::string;10 using std::cin;11 using std::cout;1…

cmu15545笔记-查询优化(Query Optimization)

目录概述Heuristics / RulesCost-based SearchSingle relationMutiple relationGenertive / Bottom-UpTransformation / Top-DownNested sub-queriesDecomposing QueriesExpression/Queries RewritingStatistics 概述 数据库系统的执行流程:从优化器到磁盘所设计的步骤:查询优…