共享内存或mmap - Linux C/C++进程间通信

18

这个上下文涉及到进程间通信,其中一个进程(“服务器”)需要向运行在同一台机器上的多个监听进程(“客户端”)发送固定大小的结构体。

我非常熟练地使用Socket编程进行这项工作。为了使服务器和客户端之间的通信更快,减少复制的数量,我想尝试使用共享内存(shm)或mmaps。

操作系统是RHEL 64位。

由于我是新手,请建议我应该使用哪个。 如果有人能为我指点一本书或在线资源来学习相同的内容,我将不胜感激。

谢谢回答。我想补充说,服务器(市场数据服务器)通常会接收组播数据,这将导致它每秒“发送”约200,000个结构体到“客户端”,其中每个结构体大约100字节。 shm_open/mmap实现仅针对大块数据还是对大量小结构体也有优势?

4个回答

24
我会使用 mmapshm_open 结合起来将共享内存映射到进程的虚拟地址空间中。这相对比较直接和干净:
  • 你要用一些符号名称来标识你的共享内存段,比如"/myRegion"
  • 使用 shm_open 在该区域上打开一个文件描述符
  • 使用 ftruncate 扩大内存片段到所需大小
  • 使用 mmap 将其映射到你的地址空间中
shmat 和其他接口(至少从历史上来看)可能存在限制你可以映射的最大内存量的缺点。
然后,所有的 POSIX 线程同步工具 (pthread_mutex_t, pthread_cond_t, sem_t, pthread_rwlock_t, ...) 都有初始化接口,允许你在进程共享环境中使用它们。所有现代的 Linux 发行版都支持这种方式。
相对于使用套接字,是否应该选择这种方式呢?就性能而言,这可能会有一些差异,因为你不需要复制数据。但我认为主要点是,一旦你初始化了你的段,这个概念上会更加简单。要访问一个项目,你只需要在共享锁上取得锁、读取数据,然后再解锁即可。
正如 @R 所建议的,如果你有多个读者,则使用 pthread_rwlock_t 可能是最好的锁结构。

3
如果你只在一个端读取数据,互斥锁是错误的同步原语。应该使用读写锁。 - R.. GitHub STOP HELPING ICE

9
我曾经使用共享内存段实现了一个IPC库;这使我避免了复制数据的过程(不需要将数据从发送者内存复制到内核空间,再从内核空间复制到接收者内存,而是可以直接从发送者复制到接收者内存)。
然而,结果并不如我预期的那样好:实际上共享内存段是一个非常昂贵的过程,因为重新映射TLB条目和其他一切都非常昂贵。有关更多详细信息,请参见此邮件(虽然我不是那些人之一,但在开发我的库时遇到了这样的邮件)。
只有对于真正大的消息(比如几MB以上),结果才会好;如果你正在使用小缓冲区,则Unix套接字是你可以找到的最优化的东西,除非你愿意编写内核模块。

7
除了已经提出的建议之外,我想提供另一种方法:IPv6节点/接口本地组播,即仅限于环回接口的组播。 http://www.iana.org/assignments/ipv6-multicast-addresses/ipv6-multicast-addresses.xml#ipv6-multicast-addresses-1 起初这可能看起来很繁重,但大多数操作系统在零复制架构中实现了环回套接字。传递给“send”的buf参数映射到页面,并标记为写时复制,因此如果发送程序覆盖其中的数据或释放内容,则会保留其中的内容。
您应该使用强大的数据结构而不是传递原始结构。Netstrings http://cr.yp.to/proto/netstrings.txt和BSON http://bsonspec.org/ 是不错的选择。

@HumbleDebugger:RHEL只是另一个Linux发行版,Linux是那些内核之一,它在套接字缓冲区上实现了零拷贝。很抱歉这么晚才回答,因为您的原始评论没有出现在我的通知中,我直到今天才看到它,当我的答案再次得到赞时。 - datenwolf
2
做过无数次后,对我来说在新项目中使用套接字进行IPC就像甘道夫进入摩利亚矿山的保留一样。你只是无法摆脱你会遇到巴尔罗格的感觉。如果您经常写入页面,则COW很重,因为除了复制之外,您还需要TLB无效,并且正如Linus所说,“您完全处于那个糟糕的类别中”。结构体+共享内存=易于使用和最佳性能,套接字+序列化=复杂且较慢。我不知道为什么有这么多人选择后者。 - Eloff
1
@Eloff:因为在IPC中,健壮性和完整性确实很重要,而易于性能通常意味着脆弱性,这是您想在IPC中避免的。是的,SHM有应用程序,也有需要原始性能的情况。但是,如果您希望两个进程进行通信,而又不希望它们互相干扰(考虑沙盒化的工作进程),那么良好通道的套接字为您提供了一个清晰的入口路径,以便新数据到达。 - datenwolf
1
当然可以,但你最终会得到更多的代码。一个简单的共享内存解决方案和一个简单的锁定方案更容易理解,也不容易出现错误。但这只是我的观点,你的观点显然不同。 - Eloff
@Eloff: 没有“简单”的锁定方案。每个锁定方案都是一个复杂的问题。而且,如果没有明确定义和分离上下文的接口,严重的竞争条件会出现在每个角落。我已经走过这条路太多次了。除非你在争取最后一个时钟周期,或者受到严重的I/O限制,否则应该避免使用共享内存。此外,实现一个经过适当设计的零拷贝共享内存解决方案将需要更多的代码。 - datenwolf
显示剩余2条评论

2
选择 POSIX 的 shm_open/mmap 接口和旧的 System V 的 shmop 接口之间并没有太大区别,因为在初始化系统调用之后,你最终得到的是同样的情况:一个在不同进程之间共享的内存区域。如果你的系统支持,我建议使用 shm_open/mmap,因为这是一个设计更好的接口。
然后,您可以将共享内存区域用作所有进程都可以书写其数据的公共黑板。难点在于同步访问该区域的进程。在这里,我建议避免自己构建同步方案,因为这可能非常困难且容易出错。相反,使用现有的基于 socket 的实现来同步进程之间的访问,并仅使用共享内存来传输大量数据。即使使用此方案,您仍需要一个中央进程来协调缓冲区的分配,因此只有在需要传输非常大量的数据时才值得使用此方案。或者,使用同步库,例如 Boost.Interprocess

如果您使用shm_openmmap,则不会产生任何副作用。 - Jens Gustedt
请明确表达您所指的POSIX共享内存(shm_open)。还有一种遗留的SysV共享内存,使用起来很困难... - R.. GitHub STOP HELPING ICE

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