mmap详解

前言

相信很多读者知道零拷贝技术,而我们知道 mmap 也是零拷贝技术的一种实现。在本文中,我们主要介绍 mmap 的原理。

一、普通读写与mmap对比

在unix/linux平台下读写文件,一般有两种方式。第一种是首先open文件,接着使用read系统调用读取文件的全部或一部分。于是内核将文件的内容从磁盘上读取到内核页高速缓冲(也即pageCache),再从内核高速缓冲读取到用户进程的地址空间。而写的时候,需要将数据从用户进程拷贝到内核高速缓冲,然后在从内核高速缓冲把数据刷到磁盘中,那么完成一次读写就需要在内核和用户空间之间做四次数据拷贝。而且当多个进程同时读取一个文件时,则每一个进程在自己的地址空间都有这个文件的副本,这样也造成了物理内存的浪费。
在这里插入图片描述
可能有人会问,为什么先要把数据拷贝到pageCache,然后再拷贝到用户空间呢,直接拷贝到用户空间不行吗?其实这里考虑的是时间和空间局部性原理,比如当用户进程需要将修改的数据刷盘的时候,可以先刷到pageCache而不用直接刷盘,因为考虑到下一次很快就要修改数据,如果频繁刷盘会影响性能。
考虑到多次拷贝的缺点,
第二种读写方式是使用内存映射的方式。mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。如下图所示:
在这里插入图片描述
由上图可以看出,进程的虚拟地址空间,由多个虚拟内存区域构成。虚拟内存区域是进程的虚拟地址空间中的一个同质区间,即具有同样特性的连续地址范围。上图中所示的text数据段(代码段)、初始数据段、BSS数据段、堆、栈和内存映射,都是一个独立的虚拟内存区域。而为内存映射服务的地址空间处在堆栈之间的空余部分。
linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域,由于每个不同质的虚拟内存区域功能和内部机制都不同,因此一个进程使用多个vm_area_struct结构来分别表示不同类型的虚拟内存区域。各个vm_area_struct结构使用链表或者树形结构链接,方便进程快速访问,如下图所示
在这里插入图片描述
大体的来说,那用户空间如何操作文件?由于进程是跑在虚拟地址空间的,虚拟地址空间空间是通过页表找到真实的物理内存,而我们只需要在页表中填入内核pageCache的地址即可,而无需再把内核中的pageCache拷贝到程序的用户空间中。同时不同的进程将该pageCache的地址填入到自己进程页表中,那么就实现了进程间的通信与数据共享!下面将描述mmap内存映射的实现过程

二、mmap的使用方式

下面我们介绍一下怎么使用 mmap,mmap 函数的原型如下:

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

下面介绍一下 mmap 函数的各个参数作用:
addr:指定映射的虚拟内存地址,可以设置为 NULL,让 Linux 内核自动选择合适的虚拟内存地址。
length:映射的长度。
prot:映射内存的保护模式,可选值如下:
PROT_EXEC:可以被执行。
PROT_READ:可以被读取。
PROT_WRITE:可以被写入。
PROT_NONE:不可访问。
flags:指定映射的类型,常用的可选值如下:
MAP_FIXED:使用指定的起始虚拟内存地址进行映射。
MAP_SHARED:与其它所有映射到这个文件的进程共享映射空间(可实现共享内存)。
MAP_PRIVATE:建立一个写时复制(Copy on Write)的私有映射空间。
MAP_LOCKED:锁定映射区的页面,从而防止页面被交换出内存。

fd:进行映射的文件句柄。
offset:文件偏移量(从文件的何处开始映射)。
介绍完 mmap 函数的原型后,我们现在通过一个简单的例子介绍怎么使用 mmap:

int fd = open(filepath, O_RDWR, 0644); // 打开文件
void *addr = mmap(NULL, 8192, PROT_WRITE, MAP_SHARED, fd, 4096); // 对文件进行映射
在上面例子中,我们先通过 open 函数以可读写的方式打开文件,然后通过 mmap 函数对文件进行映射,映射的方式如下:

addr 参数设置为 NULL,表示让操作系统自动选择合适的虚拟内存地址进行映射。
length 参数设置为 8192 表示映射的区域为 2 个内存页的大小(一个内存页的大小为 4 KB)。
prot 参数设置为 PROT_WRITE 表示映射的内存区为可读写。
flags 参数设置为 MAP_SHARED 表示共享映射区。
fd 参数设置打开的文件句柄。
offset 参数设置为 4096 表示从文件的 4096 处开始映射。
mmap 函数会返回映射后的内存地址,我们可以通过此内存地址对文件进行读写操作。

三、mmap内存映射实现过程

mmap内存映射的实现过程,总的来说可以分为三个阶段:

(一)进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域
1、进程在用户空间调用库函数mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
2、在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址
3、为此虚拟区分配一个vm_area_struct结构,接着对这个结构的各个域进行了初始化
4、将新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树中

(二)调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系

5、为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符,链接到内核“已打开文件集”中该文件的文件结构体(struct file),每个文件结构体维护着和这个已打开文件相关各项信息。
6、通过该文件的文件结构体,链接到file_operations模块,调用内核函数mmap,其原型为:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用户空间库函数。
7、内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址。
8、通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址并没有任何数据关联到主存中。

(三)进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝

注:前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时。

9、进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。
10、缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程。
11、调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到主存中。
12、之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。
注:修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,可以调用msync()来强制同步, 这样所写的内容就能立即保存到文件里了。

总结

本文主要介绍了 mmap 的原理和使用方式,通过本文我们可以知道,使用 mmap 对文件进行读写操作时可以减少内存拷贝的次数,并且可以减少系统调用的次数,从而提高对读写文件操作的效率。

由于内核不会主动同步 mmap 所映射的内存区中的数据,所以在某些特殊的场景下可能会出现数据丢失的情况(如断电)。为了避免数据丢失,在使用 mmap 的时候可以在适当时主动调用 msync 函数来同步映射内存区的数据。

  • 6
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值