本书详细讲述了用于进程间通信(IPC)的四种不同技术:
(1)消息传递(管道、FIFO、Posix和System V消息队列);
(2)同步(互斥锁、条件变量、读写锁、文件和记录锁、Posix和System V信号量);
(3)共享内存区(匿名共享内存区、有名Posix共享内存区、有名System V共享内存区);
(4)过程调用(Solaris门、Sun RPC)。
消息队列和过程调用往往单独使用,也就是说它们通常提供了自己的同步机制。相反,共享内存区通常需要某种由应用程序提供的同步形式才能正确工作。同步技术有时候单独使用,也就是说不涉及其他形式的IPC。
讨论了共16章的细节后,很显然的一个问题是:解决某个特定问题应使用哪种形式的IPC?遗憾的是不存在关于IPC的简单判定。Unix提供的类型如此之多的IPC表明,不存在解决全部(或者甚至于大部分)问题的单一办法。你能做的仅仅是:逐渐熟悉各种IPC形式提供的机制,然后根据特定应用的要求比较它们的特性。
我们首先列出必须考虑的四个前提,因为它们对于你的应用程序相当重要。
(1)连网的(networked)还是非连网的(nonnetworked)。我们假设已作出这个决定,IPC就是用于单台主机上的进程或线程间的。如果应用程序有可能散布到多台主机上,那就考虑使用套接字代替IPC,从而简化以后向连网的应用程序转移的工作。
(2)可移植性(portability)。回想图1-5,几乎所有Unix系统都支持Posix管道、Posix FIFO和Posix记录上锁。到了1998年,大多数Unix系统支持System V IPC(消息、信号量和共享内存区),而支持Posix IPC(消息、信号量和共享内存区)的仅有几个系统。Posix IPC应该出现更多实现,然而(遗憾的是)它在Unix 98中只是个选项。许多Unix系统支持Posix线程(包括互斥锁和条件变量在内),或者应在不久的将来支持它们。有些支持Posix线程的系统不支持互斥锁和条件变量的进程间共享属性。Unix 98需要的读写锁应被Posix所采纳,而且许多版本的Unix已在支持某种类型的读写锁。内存映射I/O得到广泛支持,大多数Unix系统还提供匿名内存映射 (或者使用/dev/zero,或者使用MAP_ANON)。几乎所有Unix系统都可使用Sun RPC,门则是Solaris特有的特性(到目前为止是这样)。
(3)性能(performance)。如果性能是应用程序设计中的一个关键前提,那就在你自己的系统上运行附录A中开发的程序。更好的做法是,把这些程序修改成模拟特定应用的实际环境,再在这样的环境中测量它们的性能。
(4)实时调度(realtime scheduling)。如果你的应用需要这一特性,而且你的系统支持Posix实时调度选项,那就考虑使用Posix的消息传递和同步函数(消息队列、信号量、互斥锁、条件变量)。举例来说,当某个线程挂出一个有多个线程阻塞在其上的信号量时,待解阻塞的线程是以一种适合于所阻塞线程的调度策略和参数的方式选择的。相反,System V信号量不能保证实时调度。
为帮助理解各种类型IPC的一些特性和局限,我们汇总了它们的一些主要差异。
管道和FIFO是字节流,没有消息边界。Posix消息和System V消息则有从发送者向接收者维护的记录边界。(考虑到UNPv1中讲述的网际协议族,TCP是没有记录边界的字节流, UDP则提供具有记录边界的消息。)
当有一个消息放置到一个空队列中时,Posix消息队列可向一个进程发送一个信号,或者启动一个新的线程。System V消息队列不提供类似的通知形式。这两种消息队列都不能直接跟select或poll(UNPv1的第6章)一起使用,不过我们分别在图5-14和6.9节中提供了间接的方法。
管道或FIFO中的数据字节是先进先出的。Posix消息和System V消息具备由发送者赋予的优先级。从一个Posix消息队列读出消息时,首先返回的总是具有最高优先级的消息。从一个System V消息队列读出时,读出者可以请求所想要的任意优先级的消息。
当有一个消息放置到一个Posix或System V消息队列,或者写到一个管道或FIFO时,只有一个副本递交给刚好一个线程。这些IPC形式不存在窥探能力(即类似于套接字API的MSG_PEEK标志,UNPv1的13.7节 [1] ),它们的消息也不能广播或多播到多个接收者(这对于使用UDP协议的套接字程序和XTI程序是可能的,UNPv1第18章和第19章)。
互斥锁、条件变量和读写锁都是无名的,也就是说它们是基于内存的。它们能够很容易地在单个进程内的不同线程间共享。然而只有当它们存放在不同进程间共享的内存区中时,它们才可能为这些进程所共享。而Posix信号量就有两种形式:有名的和基于内存的。有名信号量总能在不同进程间共享(因为它们是用Posix IPC名字标识的),基于内存的信号量也能在不同进程间共享,条件是必须存放在这些进程间共享的内存区中。System V信号量也是有名的,不过所用的是key_t数据类型,它往往是从某个文件的路径名获取的。这些信号量能够很容易地在不同进程间共享。
如果持有某个锁的进程没有释放它就终止,内核就自动释放fcntl记录锁。System V信号量将这一特性作为一个选项提供。互斥锁、条件变量、读写锁和Posix信号量不具备该特性。
每个fcntl锁都与通过其相应描述符访问的文件中的某个字节范围(我们称之为一个“记录”)相关联。读写锁则不与任何类型的记录关联。
Posix共享内存区和System V共享内存区都具有随内核的持续性。它们一直存在到被显式地删除为止,即使当前没有任何进程在使用它们也这样。
Posix共享内存区对象的大小可在其使用期间扩张。System V共享内存区的大小则是在创建时固定下来的。
System V IPC所存在的三种内核限制往往需要系统管理员对它们进行调整,因为它们的默认值通常不能满足现实应用的需要(3.8节)。Posix IPC所存在的三种内核限制则通常根本不需要调整。
有关System V IPC对象的信息(当前大小、属主ID、最后修改时间,等等)可使用三个XXXctl函数的IPC_STAT命令获取,也可执行ipcs命令获取。有关Posix IPC对象的信息则不存在标准的获取方式。如果这些对象是用文件系统中的文件实现的,而且我们知道从Posix IPC名字到路径名的映射关系,那么这些对象的信息可使用stat函数或1s命令获取。但是如果这些对象不是使用文件实现的,那么可能获取不了这样的信息。
在众多的同步技术——互斥锁、条件变量、读写锁、记录锁、Posix信号量和System V信号量——中,可从信号处理程序中调用的函数(图5-10)只有sem_post和fcntl。
在众多的消息传递技术——管道、FIFO、Posix消息队列和System V消息队列——中,可从一个信号处理程序中调用的函数只有read和write(适用于管道和FIFO)。
在所有的消息传递技术中,只有门向服务器准确地提供了客户的标识(15.5节)。在5.4节中我们提到过,另外两种消息传递类型也标识客户:BSD/OS在使用Unix域套接字时提供这种标识(UNPv1的14.8节 [2] ),SVR4则在通过某个管道传递一个描述符时通过同一个管道传递发送者的标识(APUE的15.3.1节)。
[1]. 此处为UNPv1第2版英文原版书节号,第3版为14.3节。——编者注
[2]. 此处为UNPv1第2版英文原版书节号,第3版为15.2节。——编者注