从内存映射mmap说开去

5,890 阅读8分钟

avatar

关于作者

E-moss,程序员,爱好阅读和撸狗,主要从事iOS开发工作,公众号:知本集。  
主要分享和编写技术方面文章,不定期分享读书笔记,亦可访问“知本集”Git地址:https://github.com/knowtheroot/KnowTheRoot_iOS,欢迎提出问题和讨论。

文章目录

  • 操作系统读写文件流程
  • mmap内存映射
  • mmap的优点
  • mmap的实践应用

什么是内存映射?

所谓内存映射,就是将文件的磁盘扇区映射到进程的虚拟内存空间的过程。

操作系统中的进程

  • 进程就是一个正在运行的应用程序
  • 每一个进程都是独立的,并且每一个进程都在一个独立的、受保护的空间内
  • 在Linux系统中,通常使用fork()方法来开启一个新的进程
  • 在iOS系统中,每一个进程都有自己的内存和磁盘空间,其他的进程是不被允许访问的

一、操作系统读写文件流程

读写操作的流程

1.进程发起一个读文件请求;

2.内核通过查找进程文件符表,定位到内核已打开的文件集上的文件信息,从而找到对应文件的inode;

3.inode在地址空间(address_space)上查找要请求的文件是否已经缓存在内核页的高速缓存中,如果存在,则直接放回该文件的内容;

4.如果文件不存在高速缓存中,则通过inode定位到文件的磁盘地址,将数据从磁盘复制到内核页高速缓存。之后再次范圣琦读页面的过程,将内核高速缓存中的数据发送给用户进程

什么是inode?

全称为index node,既存储文件元信息的区域,中文译名“索引节点”。
例如包含:文件权限、文件拥有者的UID、文件的大小等等。

操作系统读写的特点

1.系统在read/write的时候是很耗时的,例如在读文件的时候,将文件内容从硬盘拷贝到内核空间的一个缓冲区,然后再将这些数据拷贝到用户空间,实际上完成了两次数据拷贝
2.同理,写入操作同样耗时,待写入的buffer在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存,再写回磁盘中(延迟写回),也是需要两次数据拷贝
3.如果两个进程都对磁盘中的一个文件内容进行访问,那么这个内容在物理内存中有三份:进程A的地址空间 + 进程B的地址空间 + 内核页高速缓冲空间;

此时我们找到了文件读取的痛点:两次拷贝导致效率过低

二、mmap内存映射

映射

“映射”这个词,就和数学课上说的“一一映射”是一个意思,就是建立一种一一对应关系,在这里主要是指硬盘上文件 的位置与进程逻辑地址空间 中一块大小相同的区域之间的一一对应

注意:这种对应关系纯属是逻辑上的概念,物理上是不存在的,原因是进程的逻辑地址空间本身就是不存在的。

具体到代码,就是建立并初始化了相关的数据结构(struct address_space),这个过程有系统调用mmap()实现,所以建立内存映射的效率很高。

内存映射过程

1.mmap()会返回一个指针ptr,它指向进程逻辑地址空间中的一个地址,这样以后,进程无需再调用read或write对文件进行读写,而只需要通过ptr就能够操作文件;
2.但是ptr所指向的是一个逻辑地址,要操作其中的数据,必须通过MMU将逻辑地址转换成物理地址;
3.建立内存映射并没有实际拷贝数据,这时,MMU在地址映射表中是无法找到与ptr相对应的物理地址的,也就是MMU失败,将产生一个缺页中断,缺页中断的中断响应函数会在swap中寻找相对应的页面,如果找不到(也就是该文件从来没有被读入内存的情况),则会通过mmap()建立的映射关系,从硬盘上将文件读取到物理内存中;
4.如果在拷贝数据时,发现物理内存不够用,则会通过虚拟内存机制(swap)将暂时不用的物理页面交换到硬盘上;

MMU是Memory Management Unit的缩写,中文名是内存管理单元,它是中央处理器(CPU)中用来管理虚拟存储器、物理存储器的控制线路,同时也负责虚拟地址映射为物理地址,以及提供硬件机制的内存访问授权,多用户多进程操作系统。

mmap内存映射的实现过程,总的来说可以分为三个阶段:
1.进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域;
2.调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系;
3.进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝;

映射过程核心

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

  • 进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常
  • 缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程
  • 调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到主存中。
  • 之后进程即可直接对这片主存进行读或者写的操作。

效率

常规文件操作

之前说过,常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制,由于页缓存处在内核空间,不能被用户进程直接寻址,这样就出现了两次拷贝的过程,这也是常规文件操作的性能限制。

内存映射

使用mmap操作文件中,创建新的虚拟内存区域和建立文件磁盘地址和虚拟内存区域映射这两步,没有任何文件拷贝操作
之后访问数据时发现内存中并无数据而发起的缺页异常过程,可以通过已经建立好的映射关系,只使用一次数据拷贝,就从磁盘中将数据传入内存的用户空间中,供进程使用。

结论

  • 常规文件操作需要从磁盘到页缓存再到用户主存的两次数据拷贝。
  • 而mmap操控文件,只需要从磁盘到用户主存的一次数据拷贝过程。mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同数据不通的繁琐过程。因此mmap效率更高。

mmap的例子

对硬盘上一个名为“mmap_test”的文件进行操作,文件中存有10000个整数,程序两次使用不同的方法将它们读出,加1,再写回硬盘。

gettimeofday( &tv1, NULL );
fd = open( "mmap_test", O_RDWR );
array = mmap( NULL, sizeof(int)*MAX, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0 );
for( i=0; i<MAX; ++i )
 
++array[ i ];
munmap( array, sizeof(int)*MAX );
msync( array, sizeof(int)*MAX, MS_SYNC );
free( array );
close( fd );
gettimeofday( &tv2, NULL );

三、mmap的优点

  • 对文件的读取操作跨过了页缓存,减少了数据的拷贝次数,用内存读写取代I/O读写,提高了文件读取效率。
  • 实现了用户空间和内核空间的高效交互方式。两空间的各自修改操作可以直接反映在映射的区域内,从而被对方空间及时捕捉。
  • 可用于实现高效的大规模数据传输。内存空间不足,是制约大数据操作的一个方面,解决方案往往是借助硬盘空间协助操作,补充内存的不足。但是进一步会造成大量的文件I/O操作,极大影响效率。这个问题可以通过mmap映射很好的解决。换句话说,但凡是需要用磁盘空间代替内存的时候,mmap都可以发挥其功效。

mmap的实践应用——MMKV

以下摘自《微信开发团队guoling的技术分享》

什么是MMKV? MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。 MMKV的实现 内存准备

通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由 iOS 负责将内存回写到文件,不必担心 crash 导致数据丢失。

数据处理

数据序列化方面我们选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。考虑到我们要提供的是通用 kv 组件,key 可以限定是 string 字符串类型,value 则多种多样(int/bool/double等)。要做到通用的话,考虑将 value 通过 protobuf 协议序列化成统一的内存块(buffer),然后就可以将这些 KV 对象序列化到内存中。

关于MMKV的原理之后将会专门新开一篇文章来详解和应用。