一、传统的读写文件
一般来说,修改一个文件的内容需要如下3个步骤:
- 把文件内容读入到内存中。
- 修改内存中的内容。
- 把内存的数据写入到文件中。
过程如图 1 所示:
如果使用代码来实现上面的过程,代码如下:
read(fd, buf, 1024); // 读取文件的内容到buf
... // 修改buf的内容
write(fd, buf, 1024); // 把buf的内容写入到文件
复制代码
从图 1 中可以看出,页缓存(page cache)
是读写文件时的中间层,内核使用 页缓存
与文件的数据块关联起来。所以应用程序读写文件时,实际操作的是 页缓存
。
二、使用 mmap 读写文件
从传统读写文件的过程中,我们可以发现有个地方可以优化:如果可以直接在用户空间读写 页缓存
,那么就可以免去将 页缓存
的数据复制到用户空间缓冲区的过程。
那么,有没有这样的技术能实现上面所说的方式呢?答案是肯定的,就是 mmap
。
使用 mmap
系统调用可以将用户空间的虚拟内存地址与文件进行映射(绑定),对映射后的虚拟内存地址进行读写操作就如同对文件进行读写操作一样。原理如图 2 所示:
前面我们介绍过,读写文件都需要经过 页缓存
,所以 mmap
映射的正是文件的 页缓存
,而非磁盘中的文件本身。由于 mmap
映射的是文件的 页缓存
,所以就涉及到同步的问题,即 页缓存
会在什么时候把数据同步到磁盘。
Linux 内核并不会主动把 mmap
映射的 页缓存
同步到磁盘,而是需要用户主动触发。同步 mmap
映射的内存到磁盘有 4 个时机:
- 调用
msync
函数主动进行数据同步(主动)。 - 调用
munmap
函数对文件进行解除映射关系时(主动)。 - 进程退出时(被动)。
- 系统关机时(被动)。
三、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
函数会返回映射后的内存地址,我们可以通过此内存地址对文件进行读写操作。我们通过图 3 展示上面例子在内核中的结构:
四、总结
本文主要介绍了 mmap
的原理和使用方式,通过本文我们可以知道,使用 mmap
对文件进行读写操作时可以减少内存拷贝的次数,并且可以减少系统调用的次数,从而提高对读写文件操作的效率。
由于内核不会主动同步 mmap
所映射的内存区中的数据,所以在某些特殊的场景下可能会出现数据丢失的情况(如断电)。为了避免数据丢失,在使用 mmap
的时候可以在适当时主动调用 msync
函数来同步映射内存区的数据。
(About IOS)
一、mmap 基本概念
mmap
:文件映射,用于将文件或设备映射到虚拟地址空间中,以使用户可以像操作内存地址一样操作文件或设备。
二、mmap 的属性分类:
-
根据 mmap 是否基于文件:
- 基于文件的映射:
- 将硬盘上的文件映射到进程的虚拟地址空间中的一段空间,开发者可以像读写内存一样直接读写硬盘上的文件。
- 不基于文件的映射(
MAP_ANONYMOUS
)- 在虚拟地址空间中开辟一段空间,类似
malloc
- 在虚拟地址空间中开辟一段空间,类似
- 基于文件的映射:
-
根据对 mmap 数据的修改是否立即对别的进程可见:
-
MAP_SHARED
:- 基于文件:其他 map 了同一文件的进程可以看到本进程对文件的修改
- 不基于文件:写进程需要调用 msync 才能让别的进程看到修改
-
MAP_PRIVATE
:修改只有本进程可见
-
三、mmap 的主要特点及其使用场景:
-
用 mmap 提高文件读写效率
mmap 可以使开发者像操作连续内存一样读写一个文件。且默认使用操作系统的分页功能,按需懒加载相关的页。硬盘上的文件数据(实际上是
Page cache
中的数据)以页为单位映射到页表项,读写效率较高。 -
用 mmap 提高写文件的可靠性
在 iOS 系统中,写文件有很多种不同的方式,如 fopen/fwrite,open/write,mmap 等。不论采用哪种方式,在调用完写操作之后,写入的数据并不会直接固化到硬盘的文件中,而是会由操作系统或标准库进行缓存(以提高I/O效率)。使用 mmap 的方式,如果写完后用户进程崩溃,操作系统缓存的数据会由系统保证将它正确地 flush 到硬盘上,而类似情况下 write/fwrite 写的的数据则可能会丢失(详见客户端开发基础知识——写文件避“坑”指南)。
-
用 mmap 来解决 App 内存限额问题(
OOM
)在 iOS 系统中,如果应用程序进程占用的内存过多超过预定限额,就会发生 OOM(Out Of Memory),这种情况下系统会杀死进程,造成用户视角的闪退。应用程序所能使用的内存限额由多种因素决定,主要分以下两种:
- 操作系统人为设置的限额
- iOS 系统会根据不同的机型、系统、应用程序类型做不同的限制
- 像第三方输入法(
Keyboard Extension
)进程,最新的 iOS 系统限定其最多只能使用 80M 的内存,超过此限额则就会被操作系统杀死
- 设备内存的物理限额
- 设备的物理内存大小是一定的,比如 iPhone 6 的内存为 1G,也即运行在手机上的所有进程(包括用户进程和操作系统)一共最多只有 1G 的物理内存可用。
- 因为 iOS 没有 swap 交换区,操作系统在物理内存资源无法满足需要时只能通过杀死用户进程来释放资源
- 系统会优先杀死后台进程,如果还是不能满足需求,才会杀死前台进程
设备上运行的各个进程在其所占用的物理内存中所存放的数据按其是否可以安全地丢弃可以分为两种:
- 可安全丢弃的(即丢掉后还可以从文件中完整无误地读回来)
- 不可丢弃的(即本数据在操作系统中并未明确与某文件有直接映射关系)
其中,可安全丢弃的数据,即使它目前占用了物理内存,因为它随时可以被操作系统按需从文件中读回来,所以操作系统可以随时根据需要把这些数据从物理内存中清除出去。所以,可以认为这种数据是不占据物理内存配额的。
而不可丢弃的数据,这种数据只存在于物理内存中,没有文件与它绑定。因此它所在的物理内存会一直被占着,直到应用程序显式通知操作系统释放这些内存(比如调用 free 方法),任何提前释放都可能会导致数据丢失(data loss)。
还有一种较特殊的情况是:compressed memory,它是指操作系统可能会在一定条件下选择将某些 dirty memory 进行压缩,以减少它们占用的物理内存大小,在进程需要访问它们的时候先做解压再进行访问。这种类型的内存也是不可丢弃的,它会一直占用压缩后大小的物理内存。
如果使用了基于文件的 MAP_SHARED 类型的 mmap ,应用程序在相应的虚拟内存地址上的读写操作都直接映射到文件块上,其所用物理内存在操作系统看来都是可以安全丢弃的,完全不占用物理内存配额(页表项所占的除外)。
- 操作系统人为设置的限额