【I/O】内存映射—— mmap() 函数的使用

这段时间学习 JVM 学得实在是有点累,要记忆的东西太多…在下一篇笔记还没发出来之前先插一脚,研究一下最近经常遇到的一个函数—— mmap() 。

提到 mmap 大家可能会感到陌生,其实 Android 中的 Binder 机制就是基于 mmap 来实现的。这个函数实现了一种内存映射文件的功能。它不仅仅在 Binder 中有应用。诸如腾讯的 xLog、美团的 Logan 等日志库,以及腾讯的 MMKV 库中都大量使用到了 mmap 的功能。如果听到这里,你也对 mmap 函数产生了兴趣的话,就请和我一起从这篇文章开始上手研究 mmap 函数吧。

内存映射

首先,我们需要理解一下内存映射文件的概念,下面是它在 Wiki 中的定义:

内存映射文件(Memory-mapped file),或称“文件映射”、“映射文件”,是一段虚内存逐字节对应于一个文件或类文件的资源,使得应用程序处理映射部分如同访问主内存。

也就是说,它是一种可以将本地的文件逐字节映射到内存中的功能。当成功进行内存映射后,我们操作这块映射的内存时实际上就是在对这个映射的文件进行操作。

image-20190114151416504

内存映射的主要应用是提升 I/O 的性能。将文件从硬盘中读入内存,需要文件系统进行数据拷贝,在这个过程中它会首先将文件内容从硬盘拷贝到内核空间的一个缓冲区,再把这些数据从缓冲区拷贝到用户空间,这个过程经历了两次拷贝。而如果我们通过内存映射的方式,则是直接将硬盘中的内容映射到了用户空间的内存中,不存在用户拷贝。

mmap 及其具体使用

mmap 是一种实现了内存映射功能的函数,它位于 <sys/mman.h> 头文件中。

函数原型及用途

它的函数原型如下:

其中 mmap 函数用于将文件映射到内存,而 munmap 则是取消映射,msync 用于实现磁盘文件内容与共享内存区中的内容一致,即同步操作。一般来说,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用 munmap() 后才执行该操作。

mmap 这个函数主要有下面三个用途:

1、将普通文件映射到内存中,通常在需要对文件进行频繁读写时使用,用内存读写取代I/O读写,获得较高的性能;

2、将特殊文件进行匿名内存映射,为关联进程提供共享内存空间;

3、为无关联的进程间的Posix共享内存(SystemV的共享内存操作是shmget/shmat)

参数

1. addr

想要映射到的内存的起始地址,如果没有则设为 NULL,即由系统随机分配,当映射成功后会返回该地址。

2. length

指要将文件中内容映射到内存中的长度。

3. prot

指映射区域的保护方式,可以由下面的参数进行组合

  • PROT_EXEC 映射区域可被执行
  • PROT_READ 映射区域可被读取
  • PROT_WRITE 映射区域可被写入
  • PROT_NONE 映射区域不能存取

4. flags

影响映射区域的各种特性,调用 mmap() 时必须要指定 MAP_SHARED 或 MAP_PRIVATE。

  • MAP_FIXED:如果start所指地址无法成功建立映射,则放弃映射,不对地址做修正。
  • MAP_SHARED:对映射区域的写入数据会复制回文件内,且允许其他映射该文件的进程共享。
  • MAP_PRIVATE:对映射区域的写入操作会产生一个映射文件的拷贝,对此区域作的任何修改都不会写回原来的文件。
  • MAP_ANONYMOUS:建立匿名映射。此时会忽略参数fd,不涉及文件,且映射区域无法和其他进程共享。
  • MAP_DENYWRITE:只允许对映射区域的写入操作,其他对文件直接写入的操作将被拒绝。
  • MAP_LOCKED:将映射区域锁定,则该区域不会被置换(swap)。

5. fd

要映射到该内存区域的文件描述符,若使用匿名映射则会被设为 -1。

6. offset

文件映射偏移量,必须是分页大小的整数倍。

实例1 映射文件到内存

下面我们用一个实例来测试内存映射,首先我们先建立一个测试用文件。在这里我使用 Mac 建立了一个大小为 1KB 的文本文件 Test.txt。

$ sudo mkfile 1k Test.txt

之后在文本文件的开始加入「 hello 」。

$ vi Test.txt

之后通过 C++ 编写如下的代码:

这样,当我们在内存中对 mapped 进行修改,实际上也是在对 打开的文件进行修改。

当我输入 Test.txt 打开该文件后,在 mapped 后添加上了 「world!」。

程序运行结束后,我们打开这个文件,会发现,内容被成功添加到文件中

helloworld!

实例2 匿名映射共享数据

下面这个例子使用 mmap 的匿名映射建立了一块共享内存,使得 fork 的子进程与父进程可以通过这块内存进行通信。

运行结果如下:

child got a message: from ur father
parent got a message: from ur son

可以看到,通过 mmap 我们成功地在两个进程之间映射了一块共享内存,父子进程可通过这个共享内存进行通信。

使用中的一些细节

在使用 mmap 时,我们需要注意一些细节:

  1. mmap 映射区的大小必须是物理页大小整数倍(32位系统中通常是4k字节)。因为 Linux 采用的是页式管理机制,即内存的最小粒度是页,而进程虚拟地址空间和内存的映射也是以页为单位。为了匹配内存的操作, mmap 从磁盘到虚拟地址空间的映射也必须是页。
  2. 映射建立之后,即使文件关闭,映射依然存在。因为映射的是磁盘的地址,不是文件本身,和文件句柄无关。同时,可用于进程间通信的有效地址空间不完全受限于被映射文件的大小限制,因为是按页映射。

分析美团 Logan 库 mmap 部分

Logan 是美团开源的一款日志工具,它在将日志存储于本地的过程中就使用了 mmap,下面让我们对它进行分析。

为什么要使用 mmap

在了解它是如何使用 mmap 之前,我们先考虑一个问题,为什么 Logan 日志库要用到 mmap 呢?

我们考虑一下实现日志的几种方式:

第一种方式是用户每进行一次操作,就对数据做一次保存。这样的好处是能保证日志的数据能安全保存,没有丢失的风险。但缺点也很明显,频繁的 I/O 操作会对我们的 App 流畅性造成影响

第二种方式是先将日志缓存到内存中,到达一定量再保存到本地。这种方式虽然解决了频繁 I/O 的问题,但是如果应用在保存日志之前就 Crash 了,那么这些还未保存的日志就这样丢失了。这显然是不合理的,无法保证日志的完整性。

于是,xLog 和 Logan 这两个库采用了同一种方式—— mmap,在不影响性能的前提下又保证了日志的完整性。

只需要把日志文件映射到内存中,那么对内存进行的操作就会被同步到本地日志中,极大的提高了效率。

着手分析

Logan 的 mmap 部分位于Meituan-Dianping/Logan/blob/master/Logan/Clogan/文件夹下,分别是 mmap_util.h 及 mmap_util.c 两个文件。首先我们看到 mmap_util.h 这一文件。

里面主要是一些常量的定义,接着我们看到 mmap_util.c

在这里我梳理一下它的逻辑:

  1. 首先读入文件后,判断它是否小于 LOGAN_MMAP_LENGTH(150K),小于 150K 则用 0 将文件补齐到 150K。
  2. 之后对文件进行了一次保护性的检测,保证映射的文件达到 150K 的要求。
  3. 当前面的检查都完成后,则开始使用 mmap 函数将文件映射到内存 p_map 中。
  4. 之后再次进行了一次检查,在 mmap 文件仍然存在的情况下,将映射的内存的地址赋值给 buffer 指针。
  5. 如果前面的判断出现不满足的,则会走内存 Buffer,创建一块 150K 的内存缓存区。
  6. 如果内存缓存区都创建不了,则代表失败,返回的 back 为 LOGAN_MMAP_FAIL。

总结一下,整体的逻辑就是:如果 mmap 文件缓存目录初始化失败,会延用内存作为缓存的方式继续保存文件。


参考资料:

认真分析mmap:是什么 为什么 怎么用

mmap (一种内存映射文件的方法)

linux内存管理——mmap函数详解

评 论 区

    1. 小南瓜

      (2019-01-22 00:56)


      你的mmap写的简单易懂,特地留言支持一下。感谢教程。

    1. N0tExpectErr0r

      (2019-01-23 15:28)


      谢谢你的肯定…我也是站在巨人的肩膀上看问题才能写出来这篇博客

发表评论

%d 博主赞过: