将文件用于共享内存 IPC

dan*_*368 6 java linux ipc memory-mapped-files mappedbytebuffer

在我的应用程序中,有一个进程将数据写入文件,然后响应接收到的请求,将通过网络向请求进程发送(一些)该数据。这个问题的基础是看当两个进程碰巧在同一台主机上时,我们是否可以加快通信速度。(就我而言,进程是 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 和页面缓存的性质中获得的“共享内存”功能。

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

谢谢。

Ste*_*n C 3

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

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

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

必须这样做。如果与读取器和写入器关联的操作系统共享页面在其背后处理缓冲区,那么这将是一个安全/信息泄漏漏洞:

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

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

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

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

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

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

参考: