基于二进制重排的冷启动优化非常热门,其中涉及到了mmap相关知识。mmap是什么? 它的原理? 使用以及何时该使用。
一、mmap基础介绍
mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的内存地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对应关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上。对相关文件的操作不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映到用户空间,从而可以实现不同进程间的文件共享。
在iOS中我们可以使用的mmap应该仅是将文件映射进入内存中。他的原理就是通过虚拟内存技术,将部分进程内的虚拟内存地址,与本地磁盘中的一个文件进行映射关联。通过mmap映射文件后,对我们程序员来说,对文件的读写就是操作一段内存的读写。系统会自动将修改的内容同步到本地文件中。
我们通过代码来举个例子,看看mmap映射文件后的效果:
1 | - (void)testRead |
在上述例子中,我们映射了1个1GB大小的文件,每次读取其中的1KB,然后使用读取的数据做一些事情。
mmap函数为我们返回了一个void *类型的指针,这里我们可以将其看做是一个data数组,其完全与我们平时通过malloc函数分配在内存中的数组一模一样,对其的操作也可以通过memcpy等一系列内存操作相关函数进行。
你可能注意到,上述代码映射了1G大小的文件进入内存中。在iOS这样内存受限的系统上,这种操作会导致我们的App内存占用过高被系统强行杀掉吗?答案是不会的。因为mmap仅会将文件与内存建立映射关系,并不会一次性将文件所有内容加载到物理内存中。如果使用XCode查看此时App的内存使用情况,你会发现通过mmap加载1G文件并不会造成太多的内存占用(实际上可能仅占用kb级别的内存,这与虚拟内存页加载机制有关,后面我们会详细讲述该机制)。
二、背后原理
在上一节mmap基础介绍中我们有提到mmap可以加载大文件进入内存而不占用过多的实际物理内存,那mmap是如何做到这一点的呢?我们来探究一下他的基本原理。
1.虚拟内存
前文中我们有提及到一个虚拟内存的概念,这个概念是mmap的基础。什么是虚拟内存呢?
系统为我们每个iOS的App(进程)都创建了一套连续的内存地址,但这些内存地址并不是真实的物理内存地址。我们访问这些地址时系统会翻译成对应的物理内存(或其他存储位置)的地址。这样我们在开发App时不用关心这些内存地址是怎么存储的,只要拿来用就可以了。系统会帮助我们去真实的物理存储中读写我们需要的内容。
2.mmap映射内存
基于虚拟内存的技术,对于mmap来说,其实就是系统帮我们虚拟出了一套连续的内存地址,而这些内存实际对应的存储是磁盘上的某个文件。当我们访问这部分内存,需要进行加载实际内容时,系统会通过缺页中断进行内存页的加载。所以,我们加载了1G大小的文件,并没有占用实际的物理内存,在内存占用上也就不会占用太多的内存了
3.mmap读写内存
我们通过调用mmap函数,系统为我们返回了一个void *类型的内存地址。这部分内存就是由mmap映射出来的虚拟内存地址。我们可以对这部分内存内容进行读写操作。下面针对读和写2个操作分别介绍一下系统是如何加载文件内容和写入内容到文件的:
- 读:
对于读操作来说,系统首先会判断当前所需要读取的内容是否已经从文件加载到物理内存中。如果已经加载,则会直接返回物理内存中的内容。如果未加载,则会触发缺页中断,系统会以内存页大小的单位进行文件读取(进行文件IO操作),并将内存页进行缓存。以便减少整体IO操作次数。
- 写:
对于写操作来说,系统会将写入内容的内存页标记为脏页(dirty),并在合适的时机将脏页批量写入到映射的文件中(IO)。
三、MMAP函数使用教程
1.映射文件
mmap函数在系统的<sys/mman.h>头文件中声明:
1 | void *mmap(void *start, size_t length, int prot, int flags,int fd, off_t offset) |
参数含义为:
- start:映射区的开始地址(从哪个地址开始映射内存),如果传入NULL,则系统会自动选择合适的地址开始进行映射(通常情况下传入NULL)
- length:映射的长度
- prot:映射内存保护方式,且不能与open文件打开方式冲突,可以通过|操作符指定多个类型。在iOS中通常选择PROT_READ、PROT_WRITE
- flags:映射与其他进程的共享方式,iOS中通常使用MAP_SHARED
- fd:使用open函数获取的文件句柄,指定对那个文件进行操作
- offset:从文件的何处开始进行映射
返回值为void *类型的指针,为映射成功后的内存地址。如果返回NULL表示映射失败,可以通过errno宏获取到对应的错误码。
简单的代码例子为:
1 | - (void)testRead |
在映射成功后,可以对返回的指针进行常规的读写操作,跟操作分配在内存中的内容一模一样。
2.解除映射
munmap函数用于对已映射文件的内存进行解除,函数声明为int munmap(void * addr, size_t len),参数含义为:
addr:使用mmap获取到的映射内存地址
len:映射的长度
返回值为int,0代表成功,-1代表失败。使用errno获取对应的失败信息。
3.同步磁盘数据
通常来说,对映射内存的更改不会同步写入到磁盘文件,而是有系统决定一个合适时机进行写入。使用msync函数可以立即将更改同步到磁盘。其函数声明为:int msync(void *addr, size_t len, int flags),参数含义为:
addr、len:与mmap、munmap类似,分别为映射内存地址和指定的长度
flags:可选MS_ASYNC、MS_SYNC、MS_INVALIDATE,使用标志MS_ASYNC函数会计划一次同步,但其立即返回,
四、应用场景
前文我们分析了mmap的基本原理与性能后,我们来看看对于iOS开发来说mmap应该在什么场景下进行应用
- mmap有个很大的优势在于映射文件不占用物理内存空间,因此可以用来读取大文件
- 对于读写效率上与常规的read/write比较,适用于需要随机读写的场景,及写入文件的场景
- 对于需要长时间持有某个文件或者需要与其他进程共享某个文件,及进程间通讯需要传递大量数据时
如果不是特别需要或者性能上确实有巨大提升,否则还是老实的用常规文件读写吧。如果要使用mmap,那么进行充分的测试,否则可能有你意想不到的问题。
- 本文作者: Grx
- 本文链接: https://ruixiaoguo.github.io/Grx.github.io/Grx.github.io/2021/08/11/iOS MMAP初识/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!