关于 mmap 与 read/write

前几天在看 malloc 实现资料的时候,看到 mmap,发现自己并不是非常理解 mmap 的作用,于是查了一些资料,顺便把以前的知识梳理一下,于是就有了这篇博文。

Linux 下对文件的访问主要有两种方式。一种是 read/write/seek,而另外一种是利用 mmap 系统调用将整个文件映射到内存中。 8 9 89 89对比两种方式的性能,测试结果如图 1、2 所示。

图1:mmap 读写文件测试

图2:mmap 图像处理性能测试 (scale 是测试程序的一个参数)

mmap 的优势

Stackoverflow 上 2 2 2有一个很好的讨论,对比 readmmap。总结一下,通常使用 mmap() 的三种情况最终目的其实都一样:提高效率。

🔥提高 I/O 效率 :传统的 file I/O 中 read 系统调用首先从磁盘拷贝数据到 kernel,然后再把数据从 kernel 拷贝到用户定义的 buffer 中(可能是 heap 也有可能是 stack 或者是全局变量中 6 6 6)。而 mmap 直接由内核操刀,mmap 返回的指针指向映射内存的起始位置,然后可以像操作内存一样操作文件,而且如果是用 read/write 将 buffer 写回 page cache 意味着整个文件都要与磁盘同步(即使这个文件只有个别 page 被修改了),而 mmap 的同步粒度是 page,可以根据 page 数据结构的 dirty 位来决定是否需要与 disk 同步。这是 mmap 比 read 高效的主要原因。对于那种频繁读写同一个文件的程序更是如此。

匿名内存映射 :匿名内存映射有点像 malloc(),其实 Heap 和 BSS 段就可以看成是一个 anonymous mmap 图 4。有些 malloc 的实现中,当要分配较大的内存块时,malloc 会调用 mmap 进行匿名内存映射,此时内存操作区域不是堆区,内存释放后也会直接归还给 OS,不像 heap 中的内存可以再利用。匿名内存不是 POXIS 的标准,但是几乎所有的 OS 实现了这个功能。一个目前功能最好的 malloc 实现 dlmalloc 就是采用这种方式 4 5 6 456 456。dlmalloc 有三种分配方式:

  • (1) 小于 64B 的 exact-size quicklist;
  • (2) 小于 128KB 的 coalesce quicklist;
  • (3) 对于较大请求直接调用 mmap。

共享内存进程通信:相对于管道、消息队列方式,通过内存映射的方式进程通信效率明显更高,它不需要任务数据拷贝。说到共享内存,还有一种 System V 保留下来的内存共享方法就是 shmget 相关系统调用。两者相比 mmap 更加简单易用一些,而 shmget 提供的功能更全面一些。

mmap 的一些限制

当然 mmap 也有一定的限制。

  1. mmap 的对齐方式是 page 为大小的 ,有存在内存内部碎片的可能(调用的时候 length 没有对齐),所以 mmap 不适合小文件
  2. mmap 后的内存大小不能改变。当一个文件被 mmap 后,如果其他程序要改变文件的大小,要特别留意8
  3. mmap 不能处理所有类型的文件,例如 pipes、tty、网络设备文件就不能处理。
  4. mmap 要求进程提供一块连续的虚拟内存空间,对于大文件(1G)的内存映射。有时候会失败。尽管空闲内存大于 1G,但是很有可能找不到连续的 1G 的内存空间。
  5. mmap 对于那种批量写 (write-only) 的情景并没有优势

1 1 1讨论了在传统的数据库中对数据库文件的相关操作"为什么不用 mmap?"。传统数据库对 datafile 的读写大部分是通过 open 系统调用加 O_DIRECT 标志。使用 O_DIRECT 标志可以跳过 kernel 的 page cache 而直接与 block device(如磁盘)打交道,与普通的 read/write 相比少了一层缓存 (page cache),数据库开发者通过实现在用户层的高效缓存来达到提高效率目的。但是这带来很大的复杂性问题,首先使用 O_DIRECT 的话,就必须以页为单位进行 I/O,而且既然放弃 kernel 的提供 page cache 以及相关的缓存策略,那么意味着是想通过 O_DIRECT 提供自己的更好的缓存策略,这个往往是很困难的。在 Linus 看来 12 12 12,"O_DIRECT 是一个非常糟糕的接口,目前只为数据库开发者保留,其他人使用都属于脑残的行为"。数据库开发者想通过直接与 device 打交道(简单说,他们觉得能比 OS 干得更好),来提高 I/O 性能,例如提供更适合数据库的缓存策略(如 LIRS 缓存算法 13 13 13)。例如 Innodb 中通过配置文件的形式提供了两种方式读写数据文件,一种是传统的 read/write 读写,一种是 O_DIRECT 访问。一般情况下 O_DIRECT 的性能要高 11 11 11

处理高并发的方案

在作者看来,用 mmap 管理是一个可行的方案:使用 mmap 可以减少 kernel 与 user space 之间的 context switch ;kernel 提供了 page cache 高效的缓存管理;内存被共享时 kernel 提供了同步功能。除了 mmap,配合 mlock()madvise()msync(),开发者能够更自由的控制缓存策略。像 MongoDB 就是使用 mmap 来读写数据文件的。但是由于使用 madvise 的人不多,kernel 好像并没有利用 madvise 信息,或者效果不是很好 12 12 12。 文中最后提了这种方式如何应对高并发的请求,并提了一些解决方案。

Coroutine,用户级的协程的好处就是比 thread 的开销更小,但是有一个很大的问题,一旦一个协程调用系统调用阻塞时(如等待 I/O),协程所属的线程就会阻塞,也就意味着其他协程也要跟着阻塞。这里有几种解决方案:

  1. 如果能够容忍阻塞对性能的影响,就不做处理。
  2. 为阻塞的协程新建一个内核线程专门等待系统调用完成,这样其他协程就可以继续, Goroutine 采用的就是这种机制。
  3. 使用 NonblockingIO,文中提到了 epoll+eventfd 14 14 14Epoll 是 linux 下的一种多路复用技术,功能与 selectpoll 一样,eventfd 则与 pipe 有点像,它通过创建的事件对象来读写一个 64 位的整数计数器,线程之间通过协商好事件对应的数值来协调通信。
  4. 对于 O_DIRECT 访问 Disk,使用异步 IO,当然也可一配合 epoll 使用。

1 1 1文中的评论给出了一些不同看法:

  1. 频繁的使用 mmap 会很容易耗尽内存的资源 ,特别对于 32 位的机器。作者提出可以使用 cgroups 15 15 15来控制资源的使用。
  2. mmap 并不适合那种 write-only 的场景,或者说这个时候没什么性能优势,而 database 的 commit log 就属于这一类型。
  3. mmap 不能提供一些灵活的控制缓存的需求,例如控制不同缓存块的写入顺序等等。

图3:Linux 虚拟内存空间(截图自 CSAPP 第二版)

图4:Linux 虚拟内存空间对应的重要数据结构 16 16 16

Page Cache

上文的讨论中,涉及到一个很重要的概念------Page Cache,简单概括,就是磁盘的数据以页式缓存的形式保存在内存中。可以从两个方面去理解 Page 和 Cache。

  • Cache:缓存存在的最主要目的为了解决设备读写速度不均横的问题。例如内存可以理解为 Disk 等设备的缓存,CPU Cache 可以理解为内存的缓存。图 5 给出了传统计算机各个部件的速度,非常的直观。简单说一个好的缓存设计可以大大提高系统 IO 的性能。
  • Page:Page 是 Disk block 在内存中的缓存结构,类似的 Cache Line 是内存数据在 CPU Cache 中的缓存单元。一般 Page 大小是可配置的,一般是 Disk block 的倍数,但是一个 Page 对应的多个 block 在 Disk 中不一定是连续的。

一个高效的缓存,必然会涉及到几个问题。

缓存替换算法:最有名莫过于 LRU (Least Recently Used) 算法,但是 LRU 在批量读写大文件的时候,会清空当前缓存,而如果读取的大文件只是读取一次,那么意味着之前缓存的数据又要从磁盘重新读取,为了避免这种情况 Kernel 使用两条链表,一条 Hot 链表,存的是被访问一次以上的数据,而另一个链表存放第一次读取的 Page,两个链表都使用 LRU 算法。

数据写回侧策略:主要两种:write through 和 write back。Kernel 为了效率使用 write back。后台使用 flush 线程将 page cache 与磁盘等设备同步。有三种情况会触发这个同步线程:

  1. 内存空闲(未与磁盘同步)页表数低于系统设定阈值;
  2. dirty 的页在内存中存在超过系统设置的时间;
  3. 用户调用 sync()fsync() 系统调用。Linux Kernel 中 flush 的线程数目等于系统磁盘(持久化设备)的个数,其实同步线程有一个演化的过程,从 bdflush 和 kupdated 配合使用到动态个数的 pdflush 线程再到现在的与外部设备等个数的 flush 线程。具体可参考 17 17 17

如何高效判断数据已在缓存中 :Kernel 中使用 Radix Tree 进行索引,在 2.6 版本以前使用全局的 Hash Table 效率较低。现在是每个文件都会有一个 radix tree。提供一个文件的偏移值(偏移值对应 radix tree 的 key),可以在常数(与偏移值的位数有关)时间内找到对应的 page 项,如果没有,则先分配一个,再返回,并且每个 page 项表明了是否 dirty 等信息。Radix Tree 是 Trie 的压缩版本,Trie 是一个节点一个字符,Radix Tree 允许多个字符。

有关 Page Cache 的更细节的东西,可以参考《Linux kernel development》 17 17 17第13 章。

图5:一个典型 X86 架构各个部件的速度 16 16 16

Reference

相关推荐
wb04307201几秒前
阿明的二次创业——从阿明用 AI 开第二家店,看 AI 原生创业的四阶段方法论
大数据·人工智能·架构
天南散修6 分钟前
MT7916驱动中802.11转换为802.3
linux·网络·驱动开发·wifi·802.11
Coder-magician39 分钟前
《代码随想录》刷题打卡day15:二叉树part05
数据结构·c++·算法
CriticalThinking41 分钟前
在xshell中使用ssh隧道访问远程服务
linux·网络·ssh
AI 小老六1 小时前
Google AX 控制面拆解:分布式 Agent 如何把断点恢复、审计策略和执行调度收进同一条链路
人工智能·分布式·后端·ai·架构·ai编程
Irissgwe1 小时前
算法的时间复杂度和空间复杂度
数据结构·c++·算法·c·时间复杂度·空间复杂度
随意起个昵称1 小时前
区间dp-基础题目3(永别)
c++·算法
爱装代码的小瓶子1 小时前
安工大题目分类(含解析和翻译)
linux·网络·c
有点。1 小时前
C++贪心算法二(练习题)
c++·算法·贪心算法
硅农深芯1 小时前
解读AUTOSAR:定义现代汽车电子的标准化架构
架构·汽车·autosar