前几天在看 malloc
实现资料的时候,看到 mmap
,发现自己并不是非常理解 mmap 的作用,于是查了一些资料,顺便把以前的知识梳理一下,于是就有了这篇博文。
Linux 下对文件的访问主要有两种方式。一种是 read/write/seek,而另外一种是利用 mmap 系统调用将整个文件映射到内存中。 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 8 ] [ 9 ] [8][9] </math>[8][9]对比两种方式的性能,测试结果如图 1、2 所示。
图1:mmap 读写文件测试
图2:mmap 图像处理性能测试 (scale 是测试程序的一个参数)
mmap 的优势
Stackoverflow 上 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 2 ] [2] </math>[2]有一个很好的讨论,对比 read
和 mmap
。总结一下,通常使用 mmap() 的三种情况最终目的其实都一样:提高效率。
🔥提高 I/O 效率 :传统的 file I/O 中 read 系统调用首先从磁盘拷贝数据到 kernel,然后再把数据从 kernel 拷贝到用户定义的 buffer 中(可能是 heap 也有可能是 stack 或者是全局变量中 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 6 ] [6] </math>[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 就是采用这种方式 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 4 ] [ 5 ] [ 6 ] [4][5][6] </math>[4][5][6]。dlmalloc 有三种分配方式:
- (1) 小于 64B 的 exact-size quicklist;
- (2) 小于 128KB 的 coalesce quicklist;
- (3) 对于较大请求直接调用 mmap。
共享内存进程通信:相对于管道、消息队列方式,通过内存映射的方式进程通信效率明显更高,它不需要任务数据拷贝。说到共享内存,还有一种 System V 保留下来的内存共享方法就是 shmget 相关系统调用。两者相比 mmap 更加简单易用一些,而 shmget 提供的功能更全面一些。
mmap 的一些限制
当然 mmap
也有一定的限制。
- mmap 的对齐方式是 page 为大小的 ,有存在内存内部碎片的可能(调用的时候 length 没有对齐),所以 mmap 不适合小文件。
- mmap 后的内存大小不能改变。当一个文件被 mmap 后,如果其他程序要改变文件的大小,要特别留意[8]。
- mmap 不能处理所有类型的文件,例如 pipes、tty、网络设备文件就不能处理。
- mmap 要求进程提供一块连续的虚拟内存空间,对于大文件(1G)的内存映射。有时候会失败。尽管空闲内存大于 1G,但是很有可能找不到连续的 1G 的内存空间。
- mmap 对于那种批量写 (write-only) 的情景并没有优势。
<math xmlns="http://www.w3.org/1998/Math/MathML"> [ 1 ] [1] </math>[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 看来 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 12 ] [12] </math>[12],"O_DIRECT 是一个非常糟糕的接口,目前只为数据库开发者保留,其他人使用都属于脑残的行为"。数据库开发者想通过直接与 device 打交道(简单说,他们觉得能比 OS 干得更好),来提高 I/O 性能,例如提供更适合数据库的缓存策略(如 LIRS 缓存算法 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 13 ] [13] </math>[13])。例如 Innodb 中通过配置文件的形式提供了两种方式读写数据文件,一种是传统的 read/write 读写,一种是 O_DIRECT 访问。一般情况下 O_DIRECT 的性能要高 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 11 ] [11] </math>[11]。
处理高并发的方案
在作者看来,用 mmap 管理是一个可行的方案:使用 mmap 可以减少 kernel 与 user space 之间的 context switch ;kernel 提供了 page cache 高效的缓存管理;内存被共享时 kernel 提供了同步功能。除了 mmap,配合 mlock()
、madvise()
、msync()
,开发者能够更自由的控制缓存策略。像 MongoDB 就是使用 mmap 来读写数据文件的。但是由于使用 madvise 的人不多,kernel 好像并没有利用 madvise 信息,或者效果不是很好 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 12 ] [12] </math>[12]。 文中最后提了这种方式如何应对高并发的请求,并提了一些解决方案。
Coroutine,用户级的协程的好处就是比 thread 的开销更小,但是有一个很大的问题,一旦一个协程调用系统调用阻塞时(如等待 I/O),协程所属的线程就会阻塞,也就意味着其他协程也要跟着阻塞。这里有几种解决方案:
- 如果能够容忍阻塞对性能的影响,就不做处理。
- 为阻塞的协程新建一个内核线程专门等待系统调用完成,这样其他协程就可以继续, Goroutine 采用的就是这种机制。
- 使用 NonblockingIO,文中提到了 epoll+eventfd <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 14 ] [14] </math>[14]。
Epoll
是 linux 下的一种多路复用技术,功能与select
,poll
一样,eventfd 则与 pipe 有点像,它通过创建的事件对象来读写一个 64 位的整数计数器,线程之间通过协商好事件对应的数值来协调通信。 - 对于 O_DIRECT 访问 Disk,使用异步 IO,当然也可一配合 epoll 使用。
<math xmlns="http://www.w3.org/1998/Math/MathML"> [ 1 ] [1] </math>[1]文中的评论给出了一些不同看法:
- 频繁的使用 mmap 会很容易耗尽内存的资源 ,特别对于 32 位的机器。作者提出可以使用 cgroups <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 15 ] [15] </math>[15]来控制资源的使用。
- mmap 并不适合那种 write-only 的场景,或者说这个时候没什么性能优势,而 database 的 commit log 就属于这一类型。
- mmap 不能提供一些灵活的控制缓存的需求,例如控制不同缓存块的写入顺序等等。
图3:Linux 虚拟内存空间(截图自 CSAPP 第二版)
图4:Linux 虚拟内存空间对应的重要数据结构 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 16 ] [16] </math>[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 与磁盘等设备同步。有三种情况会触发这个同步线程:
- 内存空闲(未与磁盘同步)页表数低于系统设定阈值;
- dirty 的页在内存中存在超过系统设置的时间;
- 用户调用
sync()
,fsync()
系统调用。Linux Kernel 中 flush 的线程数目等于系统磁盘(持久化设备)的个数,其实同步线程有一个演化的过程,从 bdflush 和 kupdated 配合使用到动态个数的 pdflush 线程再到现在的与外部设备等个数的 flush 线程。具体可参考 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 17 ] [17] </math>[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》 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 17 ] [17] </math>[17]第13 章。
图5:一个典型 X86 架构各个部件的速度 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 16 ] [16] </math>[16]
Reference
- [1] Why not mmap
- [2] When should I use mmap for file access
- [3] Wikipedia of mmap
- [4] quora: what is a good implementation of malloc
- [5] dlmalloc-A Memory Allocator by Doug Lea
- [6] A handout of memory allocation
- [7] 我的笔记:OS Thread
- [8] Advanced Programming-Unix Environment 2ed
- [9] read VS mmap
- [10] 《大规模分布式存储系统------原理解析与构架实践》 杨传辉
- [11] Direct I/O in InnoDB (O_DIRECT)
- [12] Linux Kernel Maillist 上Linus关于O_DIRECT的讨论
- [13] LIRS caching algorithm
- [14] A example usage of epoll with eventfd
- [15] Wikipedia of cgroups
- [16] How the kernel manages your memory
- [17] Robert Love《Linux Kernel Development》第三版