【问题标题】:Using files for for shared memory IPC将文件用于共享内存 IPC
【发布时间】:2020-09-09 17:30:43
【问题描述】:

在我的应用程序中,有一个进程将数据写入文件,然后响应接收请求,将通过网络将(部分)数据发送到请求进程。这个问题的基础是看当两个进程碰巧在同一个主机上时,我们是否可以加快通信速度。 (在我的例子中,流程是 Java,但我认为这个讨论可以更广泛地应用。)

有一些项目使用 Java 的 FileChannel.map() 返回的 MappedByteBuffers 作为在同一主机上的 JVM 之间共享内存 IPC 的一种方式(请参阅 Chronicle Queue、Aeron IPC 等)。

加速同一主机通信的一种方法是让我的应用程序使用其中一种技术来为同一主机通信提供请求-响应路径,或者结合现有的写入数据文件的机制,或者通过提供统一的通信和写入文件的方式。

另一种方法是允许请求进程直接访问数据文件。

我倾向于支持第二种方法 - 假设它是正确的 - 因为它更容易实现,并且似乎比为每个请求复制/传输数据副本更有效(假设我们没有替换现有的写入文件的机制)。

基本上,我想了解当两个进程可以访问同一个文件并使用它进行通信时究竟会发生什么,特别是 Java (1.8) 和 Linux (3.10)。

根据我的理解,如果两个进程同时打开同一个文件,那么它们之间的“通信”本质上将是通过“共享内存”。

请注意,此问题与是否使用 MappedByteBuffer 的性能影响无关 - 与读取和写入文件相比,使用映射缓冲区以及减少复制和系统调用似乎很有可能会减少开销,但这可能需要对应用程序进行重大更改。

这是我的理解:

  1. 当 Linux 从磁盘加载文件时,它会将该文件的内容复制到内存中的页面。该内存区域称为页面缓存。据我所知,无论使用哪种 Java 方法(FileInputStream.read()、RandomAccessFile.read()、FileChannel.read()、FileChannel.map())或本机方法来读取文件,它都会执行此操作(观察“免费”并监控“缓存”值)。
  2. 如果另一个进程尝试加载相同的文件(当它仍然驻留在缓存中时),内核会检测到这一点并且不需要重新加载文件。如果页面缓存已满,页面将被逐出——脏页面被写回磁盘。 (如果有明确的磁盘刷新,页面也会被写回,并且会定期使用内核线程)。
  3. 在缓存中已经有一个(大)文件可以显着提升性能,这比我们使用哪些 Java 方法打开/读取该文件的差异要大得多。
  4. 如果使用 mmap 系统调用 (C) 或通过 FileChannel.map() (Java) 加载文件,则基本上文件的页面(在缓存中)被直接加载到进程的地址空间中。使用其他方法打开文件,将文件加载到不在进程地址空间中的页面中,然后读取/写入该文件的各种方法将一些字节从/到这些页面复制到进程地址空间中的缓冲区中.避免该副本有明显的性能优势,但我的问题与性能无关。

总之,如果我理解正确的话——虽然映射提供了性能优势,但它似乎没有提供任何“共享内存”功能,而这些功能我们还没有从 Linux 和页面的性质中获得缓存。

所以,请让我知道我的理解在哪里。

谢谢。

【问题讨论】:

标签: java linux ipc memory-mapped-files mappedbytebuffer


【解决方案1】:

基本上,我试图了解当两个进程同时打开同一个文件时会发生什么,以及是否可以使用它来安全、高效地提供进程之间的通信。

如果您使用readwrite 操作使用常规文件(即不进行内存映射),那么这两个进程不共享任何内存。

  • 与文件关联的 Java Buffer 对象中的用户空间内存不跨地址空间共享。
  • 当进行write 系统调用时,数据会从一个进程地址空间中的页面复制到内核空间中的页面。 (这些可能是页面缓存中的页面。这是特定于操作系统的。)
  • 当进行read 系统调用时,数据会从内核空间中的页面复制到读取进程地址空间中的页面。

必须这样做。如果操作系统在后台共享与读写器相关的页面,那么这将是一个安全/信息泄漏漏洞:

  • 读者可以通过write(...)看到作者地址空间中尚未写入的数据,而且可能永远不会。
  • 写入器将能够看到读取器(假设)写入其读取缓冲区的数据。
  • 巧妙地使用内存保护是不可能解决问题的,因为内存保护的粒度是一个页面,而read(...)write(...) 的粒度只有一个字节。

当然:您可以安全地使用读写文件在两个进程之间传输数据。但是你需要定义一个协议,让读者知道作者写了多少数据。并且读者知道什么时候作者写了一些东西可能需要轮询;例如查看文件是否被修改。

如果您仅从通信“通道”中的数据复制来看这一点

  • 使用内存映射文件,您可以将数据从应用程序堆对象复制(序列化)到映射缓冲区,然后再从映射缓冲区复制(反序列化)到应用程序堆对象。

  • 对于普通文件,有两个额外的副本:1) 从写入进程(非映射)缓冲区到内核空间页面(例如在页面缓存中),2) 从内核空间页面到读取进程(非映射)缓冲区。

下面的文章解释了常规读/写和内存映射的情况。 (它是在复制文件和“零复制”的上下文中,但您可以忽略它。)

参考:

【讨论】:

  • “这些可能是页面缓存中的页面。这是特定于操作系统的”同意,我的问题是关于最近的 Linux(我在问题中指定了 v 3.10)。这就是我感到困惑的地方,如果操作系统都通过页面缓存进行读/写,这些进程不会共享该内存(在缓存中)吗?
  • 不。这不是它的工作原理。查看参考中的图表。 (哦......我看到我没有完成这个答案中的关键句子。)
  • (已修复。添加了为什么 readwrite 必须复制的解释。)
  • 我了解我的读取缓冲区和您的写入缓冲区是私有的(尽管我的问题自成立以来就已经明确,但如果不是,很抱歉),但不是缓存共享?我想你说的是,但请确认。如果它们是共享的,这不是“共享内存 IPC”还是为什么不,或者我们现在只是处理语义?
  • 会的。我什至在我的回答中描述了这一点......并指出了这种方法的缺点。我在 Linux 文档等中使用“共享内存”的方式。很可能你对该术语有不同的解释,但我强烈建议你坚持标准的含义/定义......如果您想了解文档并就此进行有意义的讨论。通过执行常规文件 I/O 或套接字 I/O 执行 IPC 不是共享内存……根据标准术语。
【解决方案2】:

我的问题是,在 Java (1.8) 和 Linux (3.10) 上,MappedByteBuffers 真的是实现共享内存 IPC 所必需的吗,或者对公共文件的任何访问都可以提供相同的功能吗?

这取决于为什么您要实现共享内存 IPC。

不用共享内存也可以清楚地实现IPC;例如在插座上。所以,如果你出于性能原因不做的话,根本就没有必要做共享内存IPC!

因此,性能必须是任何讨论的根源。

通过 Java 经典 io 或 nio API 使用文件访问不提供共享内存功能或性能。

常规文件 I/O 或套接字 I/O 与共享内存 IPC 的主要区别在于前者要求应用程序显式地进行 readwrite 系统调用来发送和接收消息。这需要额外的系统调用,并且需要内核复制数据。此外,如果有多个线程,您要么需要每个线程对之间的单独“通道”,要么需要在共享通道上多路复用多个“对话”。后者可能导致共享通道成为并发瓶颈。

请注意,这些开销与 Linux 页面缓存正交

相比之下,使用共享内存实现 IPC,没有 readwrite 系统调用,也没有额外的复制步骤。每个“通道”可以简单地使用映射缓冲区的单独区域。一个进程中的线程将数据写入共享内存,并且几乎立即对第二个进程可见。

需要注意的是,进程需要 1) 同步,以及 2) 实现内存屏障以确保读取器不会看到陈旧的数据。但是这些都可以在没有系统调用的情况下实现。

在清洗过程中,使用内存映射文件的共享内存 IPC >>


您还隐含地询问是否可以在没有内存映射文件的情况下实现共享内存 IPC。

  • 一种实用的方法是为存在于仅内存文件系统中的文件创建内存映射文件;例如Linux 中的“tmpfs”。

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

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

    • 在父进程中,使用mmap创建一个带有MAP_ANONYMOUS | MAP_SHARED的segment。
    • 分叉子进程。这些最终将全部与彼此和父进程共享段。

    但是,为 Java 进程实现这一点将是……具有挑战性的。 AFAIK,Java 不支持这个。

参考:

【讨论】:

  • 我不确定你是否理解我的问题,大概是因为它写得不好。我试图通过重写使其更清楚。本质上,我试图了解当两个进程同时打开同一个文件时会发生什么,以及是否可以使用它来安全、高效地提供进程之间的通信。
【解决方案3】:

值得一提的三点:性能、并发更改和内存利用率。

您的评估是正确的,即基于 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,您必须担心潜在的并发访问。您将需要发出显式锁定(在读取之前)和解锁(在写入之后),以防止两个进程同时错误地访问该值。使用共享内存,原子操作可以消除对额外锁的需求。

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

【讨论】:

  • 我不确定你是否理解我的问题,大概是因为它写得不好。我试图通过重写使其更清楚。本质上,我试图了解当两个进程同时打开同一个文件时会发生什么,以及是否可以使用它来安全、高效地提供进程之间的通信。
猜你喜欢
  • 2014-10-08
  • 1970-01-01
  • 2012-12-11
  • 1970-01-01
  • 2011-07-23
  • 1970-01-01
  • 2014-04-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多