理解内存映射的概念

3
我已经在cs.stackexchange.com上提出了这个问题,但决定在这里也发布一下。
我阅读了几篇博客和Stack Exchange上的问题,但我无法理解内存映射文件的真正缺点。我经常看到以下缺点被列出:
  1. 32位地址空间无法内存映射大文件(>4GB)。现在我明白这个道理了。

  2. 我想到的一个缺点是,如果内存映射了太多的文件,这可能会导致可用系统资源(内存)减少 => 可能会导致页面被驱逐 => 可能会出现更多的页面故障。因此,在决定要内存映射哪些文件以及它们的访问模式时需要谨慎考虑。

  3. 内核映射和数据结构的开销 - 根据Linus Torvalds的说法。我不会质疑这个前提,因为我对Linux内核的内部了解不多。 :)

  4. 如果应用程序试图从未加载到页面缓存中的文件部分读取数据,则它(应用程序)将面临一种页故障的惩罚,这反过来意味着操作的I/O延迟增加。

问题 #1:对于标准文件I/O操作,这也是这种情况吗?如果应用程序尝试从尚未缓存的文件部分读取数据,它将导致系统调用,这将导致内核从设备加载相关页面/块。除此之外,页面需要被复制回用户空间缓冲区。

这里的担忧是页错误比一般的系统调用更昂贵吗?这是我对Linus Torvalds在这里说的内容的理解吗?因为页错误是阻塞的,线程没有被CPU调度走,我们浪费了宝贵的时间?还是我在这里漏掉了什么?

  1. 不支持内存映射文件的异步I/O。

问题 #2:支持内存映射文件的异步I/O存在架构限制,还是只是没有人来做它?

问题3: 与此有些相关,但我对this article的理解是,内核可以为标准I/O预读(即使没有fadvise()),但不会为映射到内存的文件预读(除非使用madvice()发出建议)。这个说法准确吗?如果这个说法确实正确,那么为什么标准I/O的系统调用可能更快,而映射到内存的文件几乎总是会导致页面错误?

1个回答

4
问题1:标准文件 I/O 操作也是这样吗?如果应用程序尝试读取尚未缓存的文件部分,将导致系统调用,从而使内核加载相关页/块。此外,页面需要复制回用户空间缓冲区。 你可以将 read 内容读入缓冲区,并且 I/O 设备会将其复制到该位置。还有异步读取或 AIO,其中数据将由内核在后台传输,因为设备提供了它。您可以使用线程和 read 进行相同的操作。对于 mmap 用例,您无法控制或不知道页面是否已映射。与 read 相关的情况更为明确。
ssize_t read(int fd, void *buf, size_t count);

你需要指定一个bufcount。你可以明确地确定数据在程序中的位置。作为程序员,你可能知道该数据不会再被使用。因此,后续对read的调用可以重用上次调用所使用的同一buf。这样做有多个好处;最容易看到的是减少了内存使用(或至少是地址空间和MMU表)。mmap不知道一个页面是否仍将来被访问。mmap不知道页面中只有部分数据是有意义的。因此,read更加明确。
假设你在磁盘上有4096个大小为4095字节的记录。你需要读/查看其中两个随机记录并对它们进行操作。对于read,你可以使用malloc()分配两个大小为4095的缓冲区,也可以使用static char buffer[2][4095]数据。对于mmap(),每个记录平均必须映射8192字节以填充两页或总共16k。访问每个mmap记录时,记录横跨两页。这导致每个记录访问产生两个page faults。此外,内核必须分配四个TLB / MMU页面来保存数据。
另外,如果使用顺序缓冲区进行read,只需要两个页面,并且只需要两个系统调用(read)。此外,如果记录上的计算很复杂,则缓冲区的局部性将使得它比mmap数据更快(CPU缓存命中)。

而且,还要将页面复制回用户空间缓冲区。

这个复制可能没有你想象的那么糟糕。CPU将缓存数据,以便下次访问时无需从主内存重新加载,因为后者可能比L1 CPU缓存慢100倍。
在上述情况下,mmap可能需要两倍以上的时间才能完成read所需的时间。

这里是否存在的问题是,page faults比一般的系统调用更昂贵 - 这是我对Linus Torvalds在这里说的理解?是因为page faults是阻塞的 => 线程没有被调度到CPU上 => 我们浪费了宝贵的时间吗?或者我漏掉了什么?

我认为主要问题是使用 mmap 时你无法控制。你将文件映射到内存中,无法确定任何部分是否在内存中。如果你随机访问文件,则它会不停地从磁盘读取,根据访问模式而定,可能会出现抖动,而你并不知情。如果访问纯粹是顺序的,则乍一看似乎并没有更好。但是,通过向同一用户缓冲区重新读取新的 ,CPU 的 L1/L2 缓存和 TLB 将更好地利用;对于您的进程和系统中的其他进程都是如此。如果你将所有块都读取到唯一的缓冲区并进行顺序处理,那么它们将大致相同(见下面的注释)。

问题 #2:支持内存映射文件的异步 I/O 存在架构限制,还是只是没有人去做它呢?

mmap 已经类似于 AIO,但其固定大小为 4k。也就是说,完整的 mmap 文件不需要全部在内存中才能开始操作。从功能上讲,它们是以不同的方式实现相似效果的不同机制。它们在体系结构上是不同的。

问题 #3:有点相关,但我对本文的理解是内核可以提前读取标准 I/O(即使没有使用 fadvise()),但不会为内存映射文件提前读取(除非用 madvice() 进行指示)。这是准确的吗?如果这个说法确实正确,那么标准 I/O 的系统调用可能更快,而内存映射文件几乎总是会导致页错误,是否是因为这个原因?

read 的差编程可能与 mmap 一样糟糕。mmap 可以使用 madvise。它更多地涉及到必须发生的所有 Linux MM 事务,以使 mmap 工作。这完全取决于你的用例;具体情况取决于访问模式哪个更好。我认为 Linus 只是在说两者都不是万能的解决方法。
例如,如果你使用 read 读取的缓冲区比系统中的内存还要大,并且你使用了和 mmap 相同的交换机制,则情况会更糟。如果你的系统没有交换空间,那么 mmap 对于随机读取访问将很好,并允许你管理比实际内存更大的文件。使用 read 进行此操作需要更多的代码,这通常意味着会有更多的 Bug,或者如果你太天真,你将只会得到一个 OOM 杀死消息。 但是,如果访问是顺序的,read 的代码不需要那么多,它可能比 mmap 更快。

read 的其他优势

对于一些情况,read 提供了使用套接字管道的功能。此外,像ttyS0这样的字符设备只能使用 read。如果您编写一个从命令行获取文件名的命令行程序,则这将非常有益。如果使用 mmap 进行结构化,可能难以支持这些文件。


我的异步 I/O 定义是:“应用程序请求内核从偏移量 f 加载/存储 x 字节,并在加载完成时通知我(应用程序)(或)提供监视机制以跟踪加载/存储进度。”我很难想象为什么 mmap 文件与读/写不同。在这两种机制中,内核只需更新页面缓存(内存页面)即可完成。后备存储的自然页面大小与此有何关系?这不是由设备驱动程序来抽象处理吗? - skittish
如果我异步读取并想要加载<4KB,那么内核不会从后备存储中加载4KB吗?因为这是内核的操作单位(假设页面大小为4KB)。 - skittish
这里有另一个值得查看的好页面:https://github.com/angrave/SystemProgramming/wiki/File-System,-Part-6:-Memory-mapped-files-and-Shared-memory。请查看“内存映射文件的优点”和“读写和mmap之间的区别”。 - artless noise
我的意图并不是将它们中的一个单独拿出来作为万灵药。重新阅读了您提供的答案和页面后,我认为mmap与read的最坏情况性能可能并没有那么不同。也许是平均情况下,mmap有可能比read更差,这就是我所忽略的吗?这让我想起了页面上提到的mmap可能会生成次要页错误(即使页面存在于物理内存中,也会加载TLB条目)。这些可能也会发生在read中,但是无法访问超出提供的缓冲区大小的随机偏移量。 - skittish
我试图理解并想出一些心理模型,以便在使用mmap时与使用read相比具有优势。 - skittish
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接