使用文件进行共享内存IPC

11
在我的应用程序中,有一个进程将数据写入文件,然后在响应请求时,将其中的一部分数据通过网络发送给请求进程。这个问题的基础是看看当两个进程恰好在同一主机上时,我们是否可以加快通信速度。(在我的情况下,进程是Java,但我认为这个讨论可以更广泛地应用。)
目前有一些项目使用Java的FileChannel.map()返回的MappedByteBuffers作为在同一主机上的JVM之间具有共享内存IPC的方法 (参见Chronicle Queue,Aeron IPC等)。
加速同一主机通信的一种方法是使我的应用程序使用其中一种技术为同一主机通信提供请求-响应路径,可以与现有的写入数据文件机制结合使用,也可以提供一个统一的通信和写入文件的方式。
另一种方法是允许请求进程直接访问数据文件。
如果两个进程同时打开同一个文件,从我的理解来看,它们之间的“通信”实际上是通过“共享内存”进行的。在这两种方法中,我倾向于第二种方法——假设它是正确的——因为它更容易实现,并且似乎比每个请求复制/传输数据的效率更高(假设我们没有替换写入文件的现有机制)。
基本上,我想要了解当两个进程有访问同一文件的权限时,特别是Java (1.8)和Linux (3.10)时,究竟会发生什么。
需要注意的是,这个问题并不涉及是否使用MappedByteBuffer的性能影响——使用映射缓冲区和减少复制和系统调用的开销相比读写文件会降低开销,但这可能需要对应用程序进行重大更改。
以下是我的理解:
当Linux从磁盘加载文件时,它将该文件的内容复制到内存中的页面。该内存区域称为页面缓存。据我所知,无论使用哪种Java方法(FileInputStream.read(),RandomAccessFile.read(),FileChannel.read(),FileChannel.map())或本地方法读取文件(通过“free”监视“cache”值),它都会这样做。
如果另一个进程尝试加载相同的文件(而该文件仍驻留在缓存中),内核将检测到此情况,并且不需要重新加载文件。如果页面缓存已满,则页面将被逐出-脏页面被写回到磁盘中。(如果显式刷新到磁盘,则还会将页面写回,并定期使用内核线程。)
将具有(大)文件的缓存是显着的性能提升,远远超过基于我们用于打开/读取该文件的Java方法的差异。
如果使用mmap系统调用(C)或通过FileChannel.map()(Java)加载文件,则文件的页面(在缓存中)直接加载到进程地址空间中。使用其他方法打开文件,文件加载到未在进程地址空间中的页面中,然后读/写该文件的各种方法将一些字节从/到这些页面复制到进程地址空间中的缓冲区中。避免该复制显然具有性能优势,但我的问题并不涉及性能。
因此,总之,如果我理解正确-虽然映射提供了性能优势,但似乎它并没有提供我们从Linux和页面缓存的性质中获得的任何“共享内存”功能。
请告诉我我的理解有哪些错误。
谢谢。

你在Linux相关的问题板块可能会比在Java问题板块更容易获得帮助。 - Deadron
@Deadron - 已采纳建议。我添加了Linux标签。谢谢。 - dan.m was user2321368
相关链接:https://dev59.com/C18e5IYBdhLWcg3wwMr7 - Erich Kitzmueller
3个回答

4
我的问题是,在Java(1.8)和Linux(3.10)上,实现共享内存IPC是否真的需要MappedByteBuffers,或者对于任何共同文件的访问是否提供相同的功能?
答:这取决于您希望实现共享内存IPC的原因。如果您不是出于性能原因而这样做,则可以明确地实现IPC而无需使用共享内存。使用Java经典io或nio API通过文件进行访问不提供共享内存功能或性能。与常规文件I/O或套接字I/O相比,使用共享内存IPC的主要区别在于前者需要应用程序显式调用读取和写入系统调用以发送和接收消息,并且还涉及内核复制数据。此外,如果有多个线程,则需要在每个线程对之间建立单独的“通道”或使用某些方法将多个“会话”多路复用到共享通道上。后者可能导致共享通道成为并发瓶颈。请注意,这些开销与Linux页面缓存不相关。相比之下,使用映射的共享内存实现IPC时,没有读取和写入系统调用,也没有额外的复制步骤。每个“通道”只需使用映射缓冲区的不同区域即可。一个进程中的线程将数据写入共享内存,第二个进程几乎立即看到该数据。但是,这些进程需要同步并实现内存屏障,以确保读者不会看到过时的数据。但是,这两者都可以在没有系统调用的情况下实现。总之,使用映射的共享内存IPC >>比<< 使用传统文件或套接字更快,这就是人们这样做的原因。您还隐含地问道,是否可以在没有映射的内存映射文件的情况下实现共享内存IPC。
  • 一种实际的方法是为存储在仅存在于内存中的文件系统(例如Linux中的 "tmpfs")中的文件创建一个内存映射文件。

    从技术上讲,这仍然是一个内存映射文件。但是,您不会产生将数据刷新到磁盘的开销,并且可以避免私有IPC数据最终出现在磁盘上的潜在安全问题。

  • 可以理论上通过执行以下操作之一来实现两个进程之间的共享段:

    • 在父进程中,使用mmap创建具有MAP_ANONYMOUS | MAP_SHARED的段。
    • 派生子进程。 这些进程将最终相互共享该段,以及与父进程。

    然而,在Java进程中实现这一点将是...具有挑战性的。 据我所知,Java不支持此功能。

参考资料:


我不确定你是否理解了我的问题,可能是因为它写得不好。我尝试通过重新编写来使其更清晰。本质上,我正在尝试理解当两个进程同时打开同一个文件时会发生什么,并且是否可以利用这一点安全高效地在两个进程之间进行通信。 - dan.m was user2321368

3
基本上,我试图理解当两个进程同时打开同一个文件时会发生什么以及是否可以安全且高效地使用它来在两个进程之间进行通信。
如果使用常规文件进行读取和写入操作(即未将其映射到内存),则两个进程不共享任何内存。
与该文件相关联的Java Buffer对象中的用户空间内存在地址空间中不共享。
当进行write系统调用时,数据从一个进程的地址空间中的页面复制到内核空间中的页面中。(这些可能是页面缓存。具体情况取决于操作系统。)
进行read系统调用时,数据从内核空间中的页面复制到读取进程的地址空间中的页面中。
必须以这种方式执行。如果操作系统在读者和写者进程缓冲区背后共享页面,则这将是一个安全/信息泄漏漏洞:
读者将能够查看作者尚未通过write(...)编写的地址空间中的数据,而这些数据可能永远不会被写入。
作者将能够查看读者(假设)写入其读取缓冲区的数据。
无法通过内存保护的巧妙使用来解决此问题,因为内存保护的粒度为页面,而read(...)和write(...)的粒度最少为单个字节。
当然:您可以安全地使用读写文件在两个进程之间传输数据。但是,您需要定义一个允许读者知道作者编写了多少数据的协议。而读者知道何时作者已经写入某些内容可能需要轮询;例如,查看文件是否已被修改。
如果只考虑通信“通道”中的数据复制,则:
使用内存映射文件,您需要将数据(序列化)从应用程序堆对象复制到映射缓冲区,然后再次(反序列化)从映射缓冲区复制到应用程序堆对象中。
对于普通文件,还有两个额外的副本:1)从写入进程(非映射)缓冲区到内核空间页面(例如,在页面缓存中),2)从内核空间页面到读取进程(非映射)缓冲区。
下面的文章解释了常规读/写和内存映射中发生的情况。(它是在复制文件和“零拷贝”的上下文中,但您可以忽略它。)
参考:

这些可能是页面缓存中的页面。这是特定于操作系统的。同意,我的问题是关于最近的Linux(我在问题中指定了v 3.10)。这就是我困惑的地方,如果操作系统通过页面缓存执行所有读/写操作,那么这些进程不会共享该内存(在缓存中)吗? - dan.m was user2321368
修正了这个问题。添加了为什么readwrite必须复制的解释。 - Stephen C
最后一个问题 - 如果一个文件被缓存在内存中的页面中,并且两个进程可以访问同一页,而它不是“只读”的 - 那不就是两个进程之间共享内存的定义吗?你的评论中“页面缓存中的页面不是共享的”和“通过...不同的进程访问更快...因为数据正在被缓存”之间存在矛盾,或者我可能没有理解其中的一些微妙之处。 - dan.m was user2321368
没有任何矛盾。页面缓存通过在内存中保存数据来提高性能,因此您无需从磁盘读取它。共享内存IPC通过进程共享页面来提高性能。这两种加速机制在概念上是正交的。 - Stephen C
1
这将是个问题。我在我的回答中甚至描述了它...并指出了这种方法的缺点。我使用“共享内存”的方式与Linux文档等中使用的方式相同。也许你对这个术语有不同的解释,但我强烈建议你坚持标准的意义/定义...如果你想理解文档并进行有意义的讨论的话。通过传统的文件I/O或套接字I/O来执行IPC并不是共享内存...根据标准术语。 - Stephen C
显示剩余12条评论

0
值得一提的三个点:性能、并发更改和内存利用率。
您对基于MMAP的评估是正确的,通常会比基于文件的IO提供性能优势。特别是,如果代码在文件的任意点执行大量小的IO,则性能优势显着。
考虑更改第N个字节:使用mmap buffer [N] = buffer [N] + 1 ,而使用基于文件的访问则需要(至少)4个系统调用+错误检查:
   seek() + error check
   read() + error check
   update value
   seek() + error check
   write + error check

确实,实际的IO(到磁盘)数量很可能是相同的。

第二点值得注意的是并发访问。使用基于文件的IO时,你必须担心潜在的并发访问问题。你需要在读取之前发出显式锁定,并在写入之后解锁,以防止两个进程同时错误地访问该值。使用共享内存,原子操作可以消除额外的锁定需求。

第三点是实际内存使用情况。对于共享对象大小显著的情况,使用共享内存可以允许大量进程访问数据而无需分配额外的内存。如果系统受到内存限制,或者需要提供实时性能的系统,这可能是访问数据的唯一方式。


我不确定你是否理解了我的问题,可能是因为它写得不好。我尝试通过重新编写来使其更清晰。本质上,我正在尝试理解当两个进程同时打开同一个文件时会发生什么,并且是否可以利用这一点安全高效地在两个进程之间进行通信。 - dan.m was user2321368

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