如果您有多个进程以只读方式从同一个文件中访问数据,那么 mmap
非常有用,这在我编写的那种服务器系统中很常见。 mmap
允许所有这些进程共享相同的物理内存页面,从而节省大量内存。
mmap
还允许操作系统优化分页操作。例如,考虑两个程序;程序 A
将 1MB
文件读入使用 malloc
创建的缓冲区,程序 B mmaps
将 1MB 文件读入内存。如果操作系统必须将 A
的部分内存换出,它必须先将缓冲区的内容写入交换,然后才能重用内存。在 B
的情况下,任何未修改的 mmap
页面都可以立即重复使用,因为操作系统知道如何从它们原来的 mmap
文件中恢复它们。 (操作系统可以通过最初将可写的 mmap
页面标记为只读并捕获 seg faults 来检测哪些页面未修改,类似于 Copy on Write 策略)。
mmap
对 inter process communication 也很有用。您可以在需要通信的进程中将文件 mmap
作为读/写,然后在 mmap'd
区域中使用同步原语(这就是 MAP_HASSEMAPHORE
标志的用途)。
如果您需要在 32 位机器上处理非常大的文件,mmap
可能会有些尴尬。这是因为 mmap
必须在进程的地址空间中找到一个连续的地址块,该地址空间大到足以容纳被映射文件的整个范围。如果您的地址空间变得碎片化,这可能会成为一个问题,您可能有 2 GB 的可用地址空间,但没有一个单独的范围可以适合 1 GB 的文件映射。在这种情况下,您可能必须将文件映射成比您想要的更小的块。
使用 mmap
替代读/写的另一个潜在尴尬是您必须在页面大小的偏移量上开始映射。如果您只想在偏移量 X
处获取一些数据,则需要修正该偏移量,使其与 mmap
兼容。
最后,读/写是您可以处理某些类型文件的唯一方法。 mmap
不能用于 pipes 和 ttys 之类的东西。
我发现 mmap() 不是优势的一个领域是读取小文件(低于 16K)时。与仅执行一次 read() 系统调用相比,读取整个文件的页面错误开销非常高。这是因为内核有时可以在您的时间片中完全满足读取,这意味着您的代码不会切换。由于页面错误,似乎更有可能安排另一个程序,从而使文件操作具有更高的延迟。
malloc
一块内存并将 1 read
放入其中会更快。这允许处理内存映射的相同代码处理 malloc'ed 。
read
访问的开销高于虚拟内存操作的开销。
mmap
必须更新页表中的 4 个条目。但是使用 read
复制到 16K 的缓冲区也涉及更新 4 个页表条目,更不用说它需要将 16K 复制到用户地址空间。那么您能否详细说明页表上的操作差异,以及mmap
的成本如何更高?
当您对大文件进行随机访问时,mmap
具有优势。另一个优点是您可以通过内存操作(memcpy、指针算法)访问它,而无需担心缓冲。当结构大于缓冲区时,使用缓冲区时,普通 I/O 有时会非常困难。要处理的代码通常很难正确处理,mmap 通常更容易。这就是说,使用 mmap
时存在某些陷阱。正如人们已经提到的那样,mmap
的设置成本非常高,因此仅对于给定的大小(因机器而异)才值得使用。
对于对文件的纯顺序访问,它也不总是更好的解决方案,尽管对 madvise
的适当调用可以缓解问题。
您必须小心您的架构(SPARC、安腾)的对齐限制,通过读/写 IO,缓冲区通常会正确对齐,并且在取消引用强制转换的指针时不会陷入陷阱。
您还必须注意不要在地图之外访问。如果您在地图上使用字符串函数,并且文件末尾不包含 \0,则很容易发生这种情况。大多数情况下,当您的文件大小不是页面大小的倍数时,它将起作用,因为最后一页填充为 0(映射区域的大小始终为页面大小的倍数)。
除了其他不错的答案,Google 专家罗伯特·洛夫 (Robert Love) 引用了 Linux system programming 的一句话:
mmap() 的优点 通过 mmap() 操作文件与标准的 read() 和 write() 系统调用相比有一些优点。其中包括: 读取和写入内存映射文件避免了在使用 read() 或 write() 系统调用时发生的无关复制,其中必须将数据复制到用户空间缓冲区或从用户空间缓冲区复制数据。除了任何潜在的页面错误之外,读取和写入内存映射文件不会产生任何系统调用或上下文切换开销。就像访问内存一样简单。当多个进程将同一个对象映射到内存中时,数据在所有进程之间共享。只读和共享可写映射完全共享;私有可写映射共享它们的尚未 COW(写时复制)页面。寻找映射涉及微不足道的指针操作。不需要 lseek() 系统调用。由于这些原因,mmap() 是许多应用程序的明智选择。 mmap() 的缺点 使用 mmap() 时需要牢记以下几点: 内存映射的大小始终为整数页。因此,后备文件的大小与整数页数之间的差异被“浪费”为松弛空间。对于小文件,可能会浪费很大比例的映射。例如,对于 4 KB 页面,7 字节映射会浪费 4,089 字节。内存映射必须适合进程的地址空间。对于 32 位地址空间,大量不同大小的映射会导致地址空间碎片化,从而难以找到大的空闲连续区域。当然,这个问题在 64 位地址空间中不太明显。在内核中创建和维护内存映射和相关数据结构是有开销的。这种开销通常可以通过消除上一节中提到的双重副本来消除,特别是对于较大且经常访问的文件。由于这些原因,当映射文件很大时(因此任何浪费的空间只占总映射的一小部分),或者当映射文件的总大小可以被页面大小(因此不会浪费空间)。
mmap
。使用 mmap,至少您可以避免将数据复制到本地缓冲区。 mmap,实际上只提供对文件缓存的访问。
与传统 IO 相比,内存映射具有巨大的速度优势。它让操作系统在内存映射文件中的页面被触摸时从源文件中读取数据。这通过创建故障页面来工作,操作系统检测到这些页面,然后操作系统自动从文件中加载相应的数据。
这与分页机制的工作方式相同,通常通过读取系统页面边界和大小(通常为 4K)上的数据来针对高速 I/O 进行优化——大多数文件系统缓存都针对该大小进行了优化。
pread
进行的。在 Solaris 9 Sparc (V890) 上,预访问比 mmap 中的 memcpy
慢 2 到 3 倍。但是你是对的,顺序访问不一定更快。
尚未列出的一个优势是 mmap()
能够将只读映射保留为 干净 页面。如果在进程的地址空间中分配一个缓冲区,然后使用 read()
从文件中填充缓冲区,则与该缓冲区对应的内存页面现在是脏,因为它们已被写入。
内核不能从 RAM 中删除脏页。如果有交换空间,则可以将它们调出进行交换。但这很昂贵,并且在某些系统上,例如只有闪存的小型嵌入式设备,根本没有交换。在这种情况下,缓冲区将一直停留在 RAM 中,直到进程退出,或者可能通过 madvise()
将其归还。
未写入 mmap()
页是干净的。如果内核需要 RAM,它可以简单地删除它们并使用页面所在的 RAM。如果具有映射的进程再次访问它,则会导致页面错误,内核从它们最初来自的文件重新加载页面.与他们最初的填充方式相同。
这不需要多个使用映射文件的进程即可成为优势。
read()
时,数据最终放入的页面与它们可能来自的文件无关。所以它们不能被写出,除了交换空间。如果一个文件是 mmap()ed
,并且映射是可写的(而不是只读的),并且可以写入,那么它取决于映射是 MAP_SHARED
还是 MAP_PRIVATE
。可以/必须将共享映射写入文件,但不能将私有映射写入文件。
不定期副业成功案例分享
MAP_HASSEMAPHORE
特定于 BSD。