前言:IPC部分写了前言,彰显一下对此部分的重视。这部分的知识总结取自《UNIX环境高级编程的》第14、15章,《UNIX网络编程 卷二:进程间通信》,以及部分博客。APUE书中对IPC的介绍模糊不清,且只讲了XSI IPC的实现(虽然和POSIX IPC大同小异);好在作者W. Richard Stevens在他的另一本书UNPv2中进行了详细论述。
在接下来的几篇博文中,首先会介绍IPC基础,接着介绍管道和FIFO、三种IPC(消息队列、信号量、共享内存),最后通过生产者消费者问题(从易到难的5种)来对IPC同步机制进行详细论述。
————————————————————————————————————————————————————
IPC,Interprocess Communication,进程间通信,描述的是不同进程之间message passing(消息传递)与synchronization(同步)机制。
1. 历史
UNIX在相关方面的演变历史,可以帮助我们更清晰的认识其各方面的特征,在message passing方面,按时间如下:
- pipe(管道),用于具有父子关系的进程间的消息传递;
- named pipe,即FIFO,后来被引入,解决了unrelated进程间的传递问题;
- message queue(消息队列),IPC三剑客之一,被加入标准,用在同一主机上不同进程间的消息传递;
- RPC(Remote Procedure Call),远程计算机间的函数调用,如java RMI。
类似的,synchronization的历史演变也挺有意思:
- recording locking(记录锁),或者叫byte-range locking可能更合适,它用于锁定文件中的一定区域(也可以是整个文件);
- semaphore(信号量),IPC三剑客之二;
- shared memory(共享内存),IPC三剑客之三,IPC三剑客被一同纳入标准;
- mutex & condition variable(互斥锁和条件变量),POSIX线程标准定义的两种同步形式,也能提供不同进程间的同步;
- read-write lock(读写锁)。
这样,关于UNIX在消息传递和同步这两方面,我们就扯出了两条线:
- message passing:pipe --> FIFO --> 消息队列 --> RPC
- synchronization: 记录锁 --> 信号量 --> 共享内存 --> mutex&cond_var --> 读写锁
当然还有像Door(门)、Socket(套接字)等IPC类型都没提及;由于一些相似性,把消息队列、信号量和共享内存划到一起,我姑且把它们叫做“IPC三剑客”吧!
2. 标准
在上面的历史演变中,有一个词叫“标准”,那么有哪些标准呢?
在UNIX的发展中,主要有下面标准:
- POSIX(Portable Operating System Interface),可移植操作系统接口,我们对它已经很熟悉了;
- Open Group(由X/Open公司和OSF开放软件基金会合并),XSI(X/Open System Interface)自然对应上了;
所以,针对IPC标准来说,自然分出了 POSIX IPC 和 XSI IPC(UNP书中是 System V IPC),由于这两种标准目前都在使用着,所以,我们的阐述不会拘泥于具体接口函数。关于POSIX的具体阐述,请参考以下两篇:
【什么是XSI兼容系统:http://fengliqiang.blogspot.com/2012/06/xsi.html】
【POSIX标准和XSI扩展:http://blog.csdn.net/mydo/article/details/8606385】
3. POSIX IPC 与 XSI IPC
我们通过下面两张表格,来看这两种标准在IPC头文件与函数接口上的不同:
首先,POSIX IPC
我们看,POSIX IPC的头文件是:<mqueue.h>, <semaphore.h>, <sys/mman.h>;
XSI IPC,
XSI IPC的头文件是<sys/msg.h>, <sys/sem.h>, <sys/shm.h>。
关于具体的实现,两种标准还是大同小异的。那么接下来,我们需要看的是这些IPC类型的一些特点,这些特点对于理解它们的作用很重要。
4. IPC的一些特点
(1)Persistence of IPC Objects(IPC对象的持续性)
什么是persistence?意思就是,有些IPC对象会随着进程的终止的消失;有些会随着系统的关闭而消失;有些会存在于文件系统中,除非你显式地删除它,否则它会一直存在。在下图中,我们可以看到三种类型的IPC object persistence:
下面,我们对照着各种IPC类型来说明上述三种persistence:
- process-persistent:比如pipe和FIFO,进程创建它们的时候开始使用,一旦进程终止,它们也将消失;
- kernel-persistent:除非关闭系统或显式删除,这些IPC类型不会随着进程的终止而消失。所以,像消息队列、信号量和共享内存,可以预先初始化它们,一旦它们被创建,就在那里了,然后,不同进程可以使用它们而不必非要在进程中创建;
- filesystem-persistent:有时候在POSIX标准中,也可以把“IPC三剑客”设为此类型;
像以前提到的线程同步的各种锁,以及socket都是process-persistent的。其实最后说起来,好像也就“IPC三剑客”是kernel-persistent(及以上)的。
(2)Name Space
name(或identifier)很重要,因为,如果两个unrelated的进程要通信的话,必须通过name找到同一个IPC对象,才能进行下一步的通信。当然,对于父子进程的pipe通信来说,两个进程互相知道共有pipe的存在,所以就不需要pipe的name了。
那么,什么叫name space呢?对于一种给定的IPC类型(比如说共享内存),它所有可能name的集合就是name space(这个name space与C中的namespace好像有点区别)。
- FIFO、“IPC三剑客”、记录锁、socket当然都有name space了;
- pipe,mutex,cond_var等等都没有name space;
注意:POSIX标准中有个memory-based semaphore是没有name的,这个细节我们不予讨论。
(3)UNIX进程间共享信息的三种方式
- 左,共享文件信息,进程读写必须调用read、write、lseek等穿越kernel来进行,当然同步是必须的;
- 中,共享kernel中的信息,比如pipe、消息队列、信号量(严格说应该是XSI下的,见UNPv2 图1-1),每次操作需进行一次system call;
- 右,典型的共享内存,直接通过指针访问内存,不需进入kernel的system call;
6. IPC的现状
随着时代的发展,有些IPC类型用的很少了,有些始终在使用着,下面给出的是APUE上的建议:
- 关于pipe和FIFO,这两种基本技术仍有效地用于大量应用程序中;
- 尽可能避免使用消息队列以及信号量,替代它们的是,全双工管道和记录锁,因为它们的使用更简单;
- 共享存储仍有应用价值。