05-IPC工具比较
43.4 IPC工具比较
在需要使用IPC时会发现有很多选择,读者在一开始可能会对这些选择感到迷惑。在后面介绍各个IPC工具的章节中将会把每个工具与其他类似的工具进行比较。下面介绍在确定选择何种IPC工具时通常需要考虑的事项。
IPC对象标识和打开对象的句柄
要访问一个IPC对象,进程必须要通过某种方式来标识出该对象,一旦将对象“打开”之后,进程必须要使用某种句柄来引用该打开着的对象。表43-1对各种类型的IPC工具的属性进行了总结。
| 工 具 类 型 | 用于识别对象的名称 | 用于在程序中引用对象的句柄 | | :----- | :----- | :----- | :----- | :----- | | 管道 | FIFO | 没有名称 | 路径名 | 文件描述符 | 文件描述符 | | UNIX domain socket | Internet domain socket | 路径名 | IP地址+端口号 | 文件描述符 | 文件描述符 | | System V消息队列 | System V信号量 | System V共享内存 | System V IPC键 | System V IPC键 | System V IPC键 | System V IPC标识符 | System V IPC标识符 | System V IPC标识符 | | POSIX消息队列 | POSIX命名信号量 | POSIX无名信号量 | POSIX共享内存 | POSIX IPC路径名 | POSIX IPC路径名 | 没有名称 | POSIX IPC路径名 | mqd_t (消息队列描述符) | sem_t (信号量指针) | sem_t (信号量指针) | 文件描述符 | | 匿名映射 | 内存映射文件 | 没有名称 | 路径名 | 无 | 文件描述符 | | flock()文件锁 | fcntl()文件锁 | 路径名 | 路径名 | 文件描述符 | 文件描述符 |
功能
各种IPC工具在功能上是存在差异的,因此在确定使用何种工具时需要考虑这些差异。下面首先对数据传输工具盒共享内存之间的差异进行总结。
- 数据传输工具提供了读取和写入操作,传输的数据只供一个读者进程消耗。内核会自动处理读者和写者之间的流控以及同步(这样当读者试图从当前为空的工具中读取数据时将会阻塞)。在很多应用程序设计中,这个模型都表现得很好。
- 其他应用程序设计则更适合采用共享内存的方式。一个进程通过共享内存能够使数据对共享同一内存区域的所有进程可见。通信“操作”是比较简单的——进程可以像访问自己的虚拟地址空间中的内存那样访问共享内存中的数据。另一个方面,同步处理(可能还会有流控)会增加共享内存设计的复杂性。在需要维护共享状态(如共享数据结构)的应用程序中,这个模型表现得很好。
关于各种数据传输工具,下面几点是值得注意的。
- 一些数据传输工具以字节流的形式传输数据(管道、FIFO以及流socket),另一些则是面向消息的(消息队列和数据报socket)。到底选择何种方法则需要依赖于应用程序。(应用程序也可以在一个字节流工具上应用面向消息的模型,这可以通过使用分隔字符、固定长度的消息,或对整条消息长度进行编码的消息头来实现,具体可参考44.8节)。
- 与其他数据传输工具相比,System V和POSIX消息队列特有的一个特性是它们能够给消息赋一个数值类型或优先级,这样递送消息的顺序就可以与发送消息的顺序不同了。
- 管道、FIFO以及socket是使用文件描述符来实现的。这些IPC工具都支持第63章中介绍的一组I/O模型:I/O多路复用(select()和poll()系统调用)、信号驱动的I/O、以及Linux特有的epoll API。这些技术的主要优势在于它们允许应用程序同时监控多个文件描述符以判断是否可以在某些文件描述符上执行I/O操作。与之相比,System V消息队列没有使用文件描述符,因此并不支持这些技术。
在Linux上,POSIX消息队列也是使用文件描述符来实现的,因此也支持上面介绍的各种I/O技术。但SUSv3并没有规定这种行为,因此在大多数实现上并不支持这些技术。
- POSIX消息队列提供了一个通知工具,当一条消息进入了一个之前为空的队列中时可以使用它来向进程发送信号或实例化一个新线程。
- UNIX domain socket提供了一个特性允许在进程间传递文件描述符。这样一个进程就能够打开一个文件并使之对另一个本来无法访问该文件的进程可用,在61.13.3节中将会对此特性进行简要介绍。
- UDP(Internet domain datagram)socket允许一个发送者向多个接收者广播或组播一条消息,在61.12节中将会对此特性进行简要介绍。
关于进程同步工具,下面几点是值得注意的。
- 使用fcntl()加上的记录锁由加锁的进程拥有。内核使用这种所有权属性来检测死锁(两个或多个进程持有的锁会阻塞对方后续的加锁请求的场景)。如果发生了死锁,那么内核会拒绝其中一个进程的加锁请求,因此会在fcntl()调用中返回一个错误标示出死锁的发生。System V和POSIX信号量并没有所有权属性,因此内核不会为信号量进行死锁检测。
- 当使用fcntl()获得记录锁的进程终止之后会自动释放该记录锁。System V信号量提供了一个类似的特性,即“撤销”特性,但这个特性仅在部分场景中可靠(参见47.8节)。POSIX信号量并没有提供类似的特性。
网络通信
在图43-1中给出所有IPC方法中,只有socket允许进程通过网络来通信。socket一般用于两个域中:一个是UNIX domain,它允许位于同一系统上的进程进行通信;另一个是Internet domain,它允许位于通过TCP/IP网络进行连接的不同主机上的进程进行通信。通常,将一个使用UNIX domain socket进行通信的程序转换成一个使用Internet domain socket进行通信的程序只需要做出微小的改动,这样只需要对使用UNIX domain socket的应用程序做较小的改动就可以将它应用于网络场景。
可移植性
现代UNIX实现支持图43-1中的大部分IPC工具,但POSIX IPC工具(消息队列、信号量以及共享内存)的普及程度远远不如System V IPC,特别是在较早的UNIX系统上。(只有版本为2.6.x的Linux内核系列才提供了一个POSIX消息队列的实现以及对POSIX信号量的完全支持。)因此,从可移植性的角度来看,System V IPC要优于POSIX IPC。
System V IPC设计问题
System V IPC工具被设计成独立于传统的UNIX I/O模型,其结果是其中一些特性使得它的编程接口的用法更加复杂。相应的POSIX IPC工具被设计用来解决这些问题,特别是下面几点需要注意。
- System V IPC工具是无连接的,它们没有提供引用一个打开的IPC对象的句柄(类似于文件描述符)的概念。在后面的章节中有时候会将“打开”一个System V IPC对象,但这仅仅是描述进程获取一个引用该对象的句柄的简便方式。内核不会记录进程已经“打开”了该对象(与其他IPC对象不同)。这意味着内核无法维护当前使用该对象的进程的引用计数,其结果是应用程序需要使用额外的代码来知道何时可以安全地删除一个对象。
- System V IPC工具的编程接口与传统的UNIX I/O模型是不一致的(它们使用整数键值和IPC标识符,而不是路径名和文件描述符),并且这个编程接口也过于复杂了。这一点在System V信号量上表现得特别明显(参见47.11节和53.5节)。
相反,内核会为POSIX IPC对象记录打开的引用数,这样就简化了何时删除对象的决策。此外,POSIX IPC提供的接口更加简单并且与传统的UNIX模型也更加一致。
可访问性
表43-2中的第二列总结了各种IPC工具的一个重要特性:权限模型控制着哪些进程能够访问对象。下面介绍各种模型的细节信息。
- 对于一些IPC工具(如FIFO和socket),对象名位于文件系统中,可访问性是根据相关的文件权限掩码(指定了所有者、组和其他用户的权限)来确定的(参见15.4节)。虽然System V IPC对象并不位于文件系统中,但每个对象拥有一个相关的权限掩码,其语义与文件的权限掩码类似。
- 一些IPC工具(管道、匿名内存映射)被标记成只允许相关进程访问。这里“相关”指通过fork()关联的。为了使两个进程能够访问同一个对象,其中一个必须要创建该对象,然后调用fork()。而fork()调用的结果就是子进程会继承引用该对象的一个句柄,这样两个进程就能够共享对象了。
- POSIX的未命名信号量的可访问性是通过包含该信号量的共享内存区域的可访问性来确定的。
- 为了给一个文件加锁,进程必须要拥有一个引用该文件的文件描述符(即在实践中它必须要拥有打开文件的权限)。
- 对Internet domain socket的访问(即连接或发送数据报)没有限制。如果有需要的话,必须要在应用程序中实现访问控制。
| 工 具 类 型 | 可 访 问 性 | 持 久 性 | | :----- | :----- | :----- | :----- | :----- | | 管道 | FIFO | 仅允许相关进程 | 权限掩码 | 进程 | 进程 | | UNIX domain socket | Internet domain socket | 权限掩码 | 任意进程 | 进程 | 进程 | | System V消息队列 | System V信号量 | System V共享内存 | 权限掩码 | 权限掩码 | 权限掩码 | 内核 | 内核 | 内核 | | POSIX消息队列 | POSIX命名信号量 | POSIX无名信号量 | POSIX共享内存 | 权限掩码 | 权限掩码 | 相应内存的权限 | 权限掩码 | 内核 | 内核 | 依情况而定 | 内核 | | 匿名映射 | 内存映射文件 | 仅允许相关进程 | 权限掩码 | 进程 | 文件系统 | | flock()文件锁 | fcntl()文件锁 | 文件的open()操作 | 文件的open()操作 | 进程 | 进程 |
持久性
术语持久性是指一个IPC工具的生命周期。(参见表43-2中的第三列。)持久性有三种。
- 进程持久性:只要存在一个进程持有进程持久的IPC对象,那么该对象的生命周期就不会终止。如果所有进程都关闭了对象,那么与该对象的所有内核资源都会被释放,所有未读取的数据会被销毁。管道、FIFO以及socket是进程持久的IPC工具。
FIFO的数据持久性与其名称的持久性是不同的。FIFO在文件系统中拥有一个名称,当所有引用FIFO的文件描述符都被关闭之后该名称也是持久的。
- 内核持久性:只有当显式地删除内核持久的IPC对象或系统关闭时,该对象才会销毁。这种对象的生命周期与是否有进程打开该对象无关。这意味着一个进程可以创建一个对象,向其中写入数据,然后关闭该对象(或终止)。在后面某个时刻,另一个进程可以打开该对象,然后从中读取数据。具备内核持久性的工具包括System V IPC和POSIX IPC。在后面章节中用来描述这些工具的示例程序中将会使用这个属性:对于每种工具都实现一个单独的程序,在程序中创建一个对象,然后删除该对象,并执行通信或同步操作。
- 文件系统持久性:具备文件系统持久性的IPC对象会在系统重启的时候保持其中的信息,这种对象一直存在直至被显式地删除。唯一一种具备文件系统持久性的IPC对象是基于内存映射文件的共享内存。
性能
在一些场景中,不同IPC工具的性能可能存在显著的差异。但在后面的章节中一般不会对它们的性能进行比较,其原因如下。
- 在应用程序的整体性能中,IPC工具的性能的影响因素可能不是很大,并且确定选择何种IPC工具可能并不仅仅需要考虑其性能因素。
- 各种IPC工具在不同UNIX实现或Linux的不同内核中的性能可能是不同的。
- 最重要的是,IPC工具的性能可能会受到使用方式和环境的影响。相关的因素包括每个IPC操作交换的数据单元的大小、IPC工具中未读数据量可能很大、每个数据单元的交换是否需要进行进程上下文切换、以及系统上的其他负载。
如果IPC性能是至关紧要的,并且不存在应用程序在与目标系统匹配的环境中运行的性能基准,那么最好编写一个抽象软件层来向应用程序隐藏IPC工具的细节,然后在抽象层下使用不同的IPC工具来测试性能。