将文件用于共享内存 IPC

Using files for for shared memory IPC

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

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

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

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

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

本质上,我想了解当两个进程访问同一个文件并使用它进行通信时到底发生了什么,特别是 Java (1.8) 和 Linux (3.10) .

据我了解,如果两个进程同时打开同一个文件,那么它们之间的 "communication" 基本上是通过 "shared memory".

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

这是我的理解:

  1. 当Linux 从磁盘加载文件时,它会将该文件的内容复制到内存中的页面。该内存区域称为页面缓存。据我所知,无论使用哪种 Java 方法(FileInputStream.read()、RandomAccessFile.read()、FileChannel.read()、FileChannel.map( )) 或本机方法用于读取文件(用 "free" 观察并监视 "cache" 值)。
  2. 如果另一个进程试图加载同一个文件(当它仍然驻留在缓存中时),内核会检测到这一点并且不需要重新加载该文件。如果页面缓存已满,页面将被逐出 - 脏页面被写回磁盘。 (如果有显式刷新到磁盘,并且周期性地使用内核线程,页面也会被写回)。
  3. 缓存中已经有一个(大)文件可以显着提高性能,远远超过我们对 open/read 该文件使用的 Java 方法的差异。
  4. 如果使用 mmap 系统调用 (C) 或通过 FileChannel.map() (Java) 加载文件,基本上文件的页面(在缓存中)直接加载到进程中' 地址 space。使用其他方法打开文件,文件被加载到不在进程地址 space 中的页面,然后各种方法到 read/write 该文件复制一些字节 from/to 这些页面到进程地址 space 中的缓冲区。避免该副本有明显的性能优势,但我的问题与性能无关。

所以总而言之,如果我理解正确 - 虽然映射提供了性能优势,但它似乎并没有提供任何我们尚未从 "shared memory" 的性质中获得的功能 Linux 和页面缓存。

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

谢谢。

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

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

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

My question is, on Java (1.8) and Linux (3.10), are MappedByteBuffers really necessary for implementing shared memory IPC, or would any access to a common file provide the same functionality?

这取决于为什么你想实现共享内存IPC。

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

因此性能必须是任何讨论的基础。

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

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

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

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

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

在清理过程中,使用内存映射文件的共享内存 IPC >><< 比使用传统文件或套接字更快,这就是人们这样做的原因。


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

  • 一种实用的方法是为内存文件系统中的文件创建一个内存映射文件;例如Linux 中的 "tmpfs"。

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

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

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

    但是,为 Java 流程实施该流程将……具有挑战性。据我所知,Java 不支持这个。

参考:

Essentially, I'm trying to understand what happens when two processes have the same file open at the same time, and if one could use this to safely and performantly offer communication between to processes.

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

  • User-space 与文件关联的 Java Buffer 对象中的内存未跨地址 space 共享。
  • 当进行 write 系统调用时,数据从一个进程地址 space 中的页面 复制 到内核 space 中的页面。 (这些 可能 是页面缓存中的页面。那是 OS 特定的。)
  • 当进行 read 系统调用时,数据从内核 space 中的页面 复制 到读取进程地址 space 中的页面.

必须这样做。如果操作系统共享与 reader 关联的页面,并且在背后写入进程缓冲区,那么这将是一个安全/信息泄漏漏洞:

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

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

如果仅从通信中的数据复制的角度来看"channel"

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

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

下面的文章解释了传统的读/写和内存映射是怎么回事。 (它是在复制文件和"zero-copy"的上下文中,但你可以忽略它。)

参考: