ChatGPT解决这个技术问题 Extra ChatGPT

什么时候应该使用 mmap 进行文件访问?

POSIX 环境至少提供两种访问文件的方式。有标准的系统调用 open()read()write() 和朋友,但也有使用 mmap() 将文件映射到虚拟内存的选项。

什么时候最好使用其中一种?包括两个接口的优点是什么?

另请参阅其中一个答案中引用的 Linus Torvalds 的 mmap() vs. reading blocksthis post

p
patryk.beza

如果您有多个进程以只读方式从同一个文件中访问数据,那么 mmap 非常有用,这在我编写的那种服务器系统中很常见。 mmap 允许所有这些进程共享相同的物理内存页面,从而节省大量内存。

mmap 还允许操作系统优化分页操作。例如,考虑两个程序;程序 A1MB 文件读入使用 malloc 创建的缓冲区,程序 B mmaps 将 1MB 文件读入内存。如果操作系统必须将 A 的部分内存换出,它必须先将缓冲区的内容写入交换,然后才能重用内存。在 B 的情况下,任何未修改的 mmap 页面都可以立即重复使用,因为操作系统知道如何从它们原来的 mmap 文件中恢复它们。 (操作系统可以通过最初将可写的 mmap 页面标记为只读并捕获 seg faults 来检测哪些页面未修改,类似于 Copy on Write 策略)。

mmapinter process communication 也很有用。您可以在需要通信的进程中将文件 mmap 作为读/写,然后在 mmap'd 区域中使用同步原语(这就是 MAP_HASSEMAPHORE 标志的用途)。

如果您需要在 32 位机器上处理非常大的文件,mmap 可能会有些尴尬。这是因为 mmap 必须在进程的地址空间中找到一个连续的地址块,该地址空间大到足以容纳被映射文件的整个范围。如果您的地址空间变得碎片化,这可能会成为一个问题,您可能有 2 GB 的可用地址空间,但没有一个单独的范围可以适合 1 GB 的文件映射。在这种情况下,您可能必须将文件映射成比您想要的更小的块。

使用 mmap 替代读/写的另一个潜在尴尬是您必须在页面大小的偏移量上开始映射。如果您只想在偏移量 X 处获取一些数据,则需要修正该偏移量,使其与 mmap 兼容。

最后,读/写是您可以处理某些类型文件的唯一方法。 mmap 不能用于 pipesttys 之类的东西。


您可以对正在增长的文件使用 mmap() 吗?还是在分配 mmap() 内存/文件时大小是固定的?
当您进行 mmap 调用时,您必须指定一个大小。所以如果你想做一些类似尾部操作的事情,它不是很合适。
Afaik MAP_HASSEMAPHORE 特定于 BSD。
@JonathanLeffler 当然,您可以对正在增长的文件使用 mmap() ,但是当文件达到您最初分配的空间限制时,您必须使用新大小再次调用 mmap() 。 LevelDB 的 PosixMmapFile 给你一个很好的例子。但它从 1.15 开始停止使用 mmap。您可以从 Github 获取旧版本
mmap 在需要多次处理文件的情况下也很有用:分配虚拟内存页面的成本只支付一次。
B
Ben Combee

我发现 mmap() 不是优势的一个领域是读取小文件(低于 16K)时。与仅执行一次 read() 系统调用相比,读取整个文件的页面错误开销非常高。这是因为内核有时可以在您的时间片中完全满足读取,这意味着您的代码不会切换。由于页面错误,似乎更有可能安排另一个程序,从而使文件操作具有更高的延迟。


+1 我可以确认。对于小文件,malloc 一块内存并将 1 read 放入其中会更快。这允许处理内存映射的相同代码处理 malloc'ed 。
这就是说,你的理由是不对的。调度程序与差异无关。不同之处在于对页表的写访问,它是内核的全局结构,持有哪些进程持有哪些内存页面及其访问权限。此操作可能非常昂贵(它可以使缓存行无效,它可以通过 TLB,表是全局的,因此必须防止并发访问等)。您需要一定大小的映射,以便 read 访问的开销高于虚拟内存操作的开销。
@PatrickSchlüter 好的,我知道在 mmap() 开始时存在开销,其中涉及修改页表。假设我们将文件的 16K 映射到内存。对于 4K 的页面大小,mmap 必须更新页表中的 4 个条目。但是使用 read 复制到 16K 的缓冲区也涉及更新 4 个页表条目,更不用说它需要将 16K 复制到用户地址空间。那么您能否详细说明页表上的操作差异,以及mmap 的成本如何更高?
mmap 会重新加载一些页面,因此读取较小的文件不一定会出现页面错误
P
Patrick Schlüter

当您对大文件进行随机访问时,mmap 具有优势。另一个优点是您可以通过内存操作(memcpy、指针算法)访问它,而无需担心缓冲。当结构大于缓冲区时,使用缓冲区时,普通 I/O 有时会非常困难。要处理的代码通常很难正确处理,mmap 通常更容易。这就是说,使用 mmap 时存在某些陷阱。正如人们已经提到的那样,mmap 的设置成本非常高,因此仅对于给定的大小(因机器而异)才值得使用。

对于对文件的纯顺序访问,它也不总是更好的解决方案,尽管对 madvise 的适当调用可以缓解问题。

您必须小心您的架构(SPARC、安腾)的对齐限制,通过读/写 IO,缓冲区通常会正确对齐,并且在取消引用强制转换的指针时不会陷入陷阱。

您还必须注意不要在地图之外访问。如果您在地图上使用字符串函数,并且文件末尾不包含 \0,则很容易发生这种情况。大多数情况下,当您的文件大小不是页面大小的倍数时,它将起作用,因为最后一页填充为 0(映射区域的大小始终为页面大小的倍数)。


T
Toby Speight

除了其他不错的答案,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,至少您可以避免将数据复制到本地缓冲区。 mmap,实际上只提供对文件缓存的访问。
映射开销也存在于文件 I/O 中,因为无论如何内核都会将文件映射到其文件缓存中。 mmap 开销仅在于将映射页面的访问权限授予请求进程。在所有情况下都完成了页面构建和磁盘读取。
A
AndyG

与传统 IO 相比,内存映射具有巨大的速度优势。它让操作系统在内存映射文件中的页面被触摸时从源文件中读取数据。这通过创建故障页面来工作,操作系统检测到这些页面,然后操作系统自动从文件中加载相应的数据。

这与分页机制的工作方式相同,通常通过读取系统页面边界和大小(通常为 4K)上的数据来针对高速 I/O 进行优化——大多数文件系统缓存都针对该大小进行了优化。


请注意,mmap() 并不总是比 read() 快。对于顺序读取, mmap() 不会给您带来任何可衡量的优势 - 这是基于经验和理论证据。如果您不相信我,请编写您自己的测试。
我可以给出来自我们项目的数字,一种短语数据库的文本索引。索引有几个千兆字节大,键保存在三叉树中。索引仍在与读取访问并行增长,映射部分之外的访问是通过 pread 进行的。在 Solaris 9 Sparc (V890) 上,预访问比 mmap 中的 memcpy 慢 2 到 3 倍。但是你是对的,顺序访问不一定更快。
只是有点挑剔。它不像分页机制那样工作,它是分页机制。映射文件是将内存区域分配给文件而不是匿名交换文件。
T
TrentP

尚未列出的一个优势是 mmap() 能够将只读映射保留为 干净 页面。如果在进程的地址空间中分配一个缓冲区,然后使用 read() 从文件中填充缓冲区,则与该缓冲区对应的内存页面现在是,因为它们已被写入。

内核不能从 RAM 中删除脏页。如果有交换空间,则可以将它们调出进行交换。但这很昂贵,并且在某些系统上,例如只有闪存的小型嵌入式设备,根本没有交换。在这种情况下,缓冲区将一直停留在 RAM 中,直到进程退出,或者可能通过 madvise() 将其归还。

未写入 mmap() 页是干净的。如果内核需要 RAM,它可以简单地删除它们并使用页面所在的 RAM。如果具有映射的进程再次访问它,则会导致页面错误,内核从它们最初来自的文件重新加载页面.与他们最初的填充方式相同。

这不需要多个使用映射文件的进程即可成为优势。


内核不能通过首先将其内容写入底层文件来删除“脏”mmap'd页面吗?
使用 read() 时,数据最终放入的页面与它们可能来自的文件无关。所以它们不能被写出,除了交换空间。如果一个文件是 mmap()ed,并且映射是可写的(而不是只读的),并且可以写入,那么它取决于映射是 MAP_SHARED 还是 MAP_PRIVATE。可以/必须将共享映射写入文件,但不能将私有映射写入文件。
这与已接受答案的第二段所描述的优势相同