当前位置: 首页 > 知识库问答 >
问题:

将文件用于共享内存IPC

傅阳炎
2023-03-14

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

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

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

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

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

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

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

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

以下是我的理解:

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

总之,如果我理解正确的话,虽然映射提供了性能优势,但它似乎并没有提供任何“共享内存”功能,而这些功能仅仅是Linux和页面缓存的特性所没有的。

所以,请让我知道我的理解是错的。

谢谢。

共有3个答案

司马英才
2023-03-14

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

您的评估是正确的,基于MMAP的IO通常比基于文件的IO具有性能优势。特别是,如果代码在文件的艺术点执行大量小IO,那么性能优势将非常显著。

考虑更改第N个字节:使用mmap缓冲区[N]=缓冲区[N] 1,并且使用基于文件的访问,您需要(至少)4次系统调用错误检查:

   seek() + error check
   read() + error check
   update value
   seek() + error check
   write + error check

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

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

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

邓阳伯
2023-03-14

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

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

您可以在没有共享内存的情况下清晰地实现IPC;e、 g.插座上方。因此,如果您不是出于性能原因而这样做,那么根本就没有必要执行共享内存IPC!

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

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

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

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

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

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

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

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

>

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

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

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

    • 在父进程中,使用mmap创建一个段,其中包含“MAP\u ANONYMOUS”和“MAP\u SHARED”
    • 分叉子进程。这些将最终与彼此和父进程共享该段

    然而,为Java进程实现这一点将是。。。具有挑战性的哎呀,Java不支持这个。

    参考:

    • 在mmap系统调用中MAP_ANONYMOUS标志的目的是什么?

  • 皮骏
    2023-03-14

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

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

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

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

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

    当然:您可以安全地使用读写文件在两个进程之间传输数据。但是,您需要定义一个协议,允许读者知道编写者编写了多少数据。如果读者知道作者什么时候写了什么,可能需要进行投票;e、 g.查看文件是否已修改。

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

    >

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

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

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

    参考:

    • 零拷贝I:用户模式透视图

  •  类似资料:
    • 共享内存是两个或多个进程共享的内存。 但是,为什么我们需要共享内存或其他通信方式呢? 重申一下,每个进程都有自己的地址空间,如果任何进程想要将自己的地址空间的某些信息与其他进程进行通信,那么只能通过IPC(进程间通信)技术进行。 我们已经知道,通信可以在相关或不相关的进程之间进行。 通常,使用管道或命名管道来执行相互关联的进程通信。 可以使用命名管道或通过共享内存和消息队列的常用IPC技术执行无关

    • EasySwoole对Swoole table进行了基础的封装。 方法列表 getInstance() 该方法用于获取TableManager管理器实例 add($name,array $columns,$size = 1024) 该方法用于创建一个table get($name):?Table 该方法用于获取已经创建好的table 示例代码 TableManager::getInstance()

    • shmat是shared memory attach的缩写。而attach本意是贴的意思。 如果进程要使用一段共享内存,那么一定要将该共享内存与当前进程建立联系。即经该共享内存挂接(或称映射)到当前进程。 shmdt则是shmat的反操作,用于将共享内存和当前进程分离。在共享内存使用完毕后都要调用该函数。 函数原型 #include <sys/types.h> #include <sys/shm.

    • 共享内存的控制 函数原型 #include <sys/ipc.h> #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf); 参数 shmid 由shmget函数生成,不同的key值对应不同的id值。 cmd 操作字段,包括: 公共的IPC选项(ipc.h中): IPC_RMID //删除 IPC_SET

    • 创建共享内存,通过key返回id。 函数原型 #include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, size_t size, int shmflg); 参数 key 不消多说 size 欲创建的共享内存段的大小 shmflg 共享内存段的创建标识: 公共的IPC选项(在/usr/include/linux/ipc.h中定义)

    • 通过查看shmget()的手动页面,我了解到shmget()调用在内存中分配了#个页面,这些页面可以在进程之间共享。 它是否要创建内核内存页,并将其映射到进程的本地地址空间?还是为该段保留了相同的进程内存页,并将为其他附加进程共享相同的内存页? 调用shmget()时,内核将保留一定数量的段/页。 调用shmat()时,保留的段映射到进程的地址空间/页。 当一个新进程附加到同一段时,前面创建的内核