问题 #1:标准文件 I/O 操作不也是这种情况吗?如果应用程序尝试读取尚未缓存的文件的一部分,它将导致系统调用导致内核从设备加载相关的页面/块。最重要的是,需要将页面复制回用户空间缓冲区。
您对缓冲区执行read,I/O 设备会将其复制到那里。还有 async 读取或 AIO,其中数据将在设备提供时由内核在后台传输。你可以用线程和read 做同样的事情。对于mmap 情况,您无法控制或不知道页面是否已映射。 read 的情况更明确。这是从,
ssize_t read(int fd, void *buf, size_t count);
您指定buf 和count。您可以明确地将数据放置在程序中所需的位置。作为程序员,你可能知道数据不会被再次使用。随后对read 的调用可以重用上次调用中的相同buf。这有很多好处;最容易看到的是更少的内存使用(或至少是地址空间和 MMU 表)。 mmap 将不知道将来是否仍要访问页面。 mmap 不知道只有页面中的一些数据是感兴趣的。因此,read 更加明确。
假设您在磁盘上有 4096 条大小为 4095 字节的记录。您需要阅读/查看两条随机记录并对它们执行操作。对于read,可以用malloc()分配两个4095缓冲区或者使用static char buffer[2][4095]数据。 mmap() 必须为每条记录平均映射 8192 字节以填充两页或总共 16k。访问每个mmap 记录时,该记录跨越两页。这会导致每个记录访问出现两个页面错误。此外,内核必须分配四个 TLB/MMU 页来保存数据。
或者,如果read 到顺序缓冲区,则只需要两个页面,只需要两个系统调用 (read)。此外,如果对记录的计算量很大,则缓冲区的局部性将使其比mmap 数据快得多(CPU 缓存命中)。
最重要的是,页面需要被复制回用户空间缓冲区。
这个副本可能没有你想象的那么糟糕。 CPU 将缓存数据,这样下次访问就不必从主内存重新加载,速度可能比 L1 CPU 缓存慢 100 倍。
在上述情况下,mmap 的占用时间是 read 的两倍。
这里是否担心页面错误通常比系统调用更昂贵 - 我对 Linus Torvalds 在这里所说的内容的解释?是不是因为页面错误阻塞=>线程没有从CPU调度=>我们在浪费宝贵的时间?还是我在这里遗漏了什么?
我认为重点是您无法控制 mmap。您mmap 该文件不知道是否有任何部分在内存中。如果您只是随机访问该文件,那么它将继续从磁盘读回它,并且您可能会在不知不觉中根据访问模式而颠簸。如果访问是纯顺序的,那么乍一看可能并不好。但是,通过重新读取一个新的chunk到同一个用户缓冲区,可以更好地利用CPU的L1/L2 CPU缓存和TLB;既适用于您的进程,也适用于系统中的其他进程。如果您将所有块读取到唯一的缓冲区并按顺序处理,那么它们将大致相同(请参阅下面的注释)。
问题 #2:支持内存映射文件的异步 I/O 是否存在架构限制,或者只是没有人能够做到这一点?
mmap 已经类似于 AIO,但它的大小固定为 4k。即,完整的mmap 文件不需要在内存中即可开始对其进行操作。从功能上讲,它们是获得相似效果的不同机制。它们在架构上是不同的。
问题 #3:相关性模糊,但我对本文的解释是内核可以预读标准 I/O(即使没有 fadvise())但不能预读内存映射文件(除非发出madvice()) 的咨询。这是准确的吗?如果这句话是真的,这就是为什么标准 I/O 的系统调用可能更快,而不是内存映射文件几乎总是会导致页面错误?
read 的不良编程可能与mmap 一样糟糕。 mmap 可以使用madvise。它与使mmap 工作所必须发生的所有Linux MM 的东西更相关。这完全取决于您的用例;根据访问模式,两者都可以更好地工作。我认为 Linus 只是说两者都不是灵丹妙药。
例如,如果您将read 分配到一个比系统内存更多的缓冲区,并且您使用与mmap 执行相同处理的交换,那么您的情况会更糟。您可能有一个没有交换的系统,mmap 用于随机读取访问会很好,并且允许您管理比实际内存更大的文件。设置使用read 执行此操作将需要更多代码,这通常意味着更多错误,或者如果您很天真,您只会收到 OOM 终止消息。注意但是,如果访问是顺序的, read 没有那么多代码,它可能会比 mmap 更快。
额外的read 好处
对于某些人来说,read 提供 sockets 和 pipes 的使用。此外,char 设备(例如 ttyS0)仅适用于 read。如果您编写从命令行获取文件名的命令行程序,这可能会很有帮助。如果你用mmap 构建,可能很难支持这些文件。