本文简要介绍了操作系统中常见进程间通信方式的基本概念和原理。

进程间通信(IPC)

概念

进程间通信IPCInter-Process Communication),指至少两个进程线程间传送数据或信号的一些技术或方法。

进程是计算机系统分配资源的最小单位(严格说来是线程)。每个进程都有自己的一部分独立的系统资源,彼此是隔离的。为了能使不同的进程互相访问资源并进行协调工作,才有了进程间通信。通常,使用进程间通信的两个应用可以被分为客户端和服务器(见主从式架构),客户端进程请求数据,服务端响应客户端的数据请求。有一些应用本身既是服务器又是客户端,这在分布式计算中,时常可以见到。这些进程可以运行在同一计算机上或网络连接的不同计算机上。

IPC对微内核和nano内核的设计过程非常重要。 微内核减少了内核提供的功能数量。 然后通过IPC与服务器通信获得这些功能,与普通的宏内核相比,IPC的数量大幅增加

ICP的方法

方法提供方(操作系统或其他环境)描述
共享文件(File)多数操作系统存储在磁盘上的记录,或由文件服务器按需合成的记录,可以被多个进程访问。
信号(Signal)多数操作系统从一个进程发送到另一个进程的系统消息,通常不用于传输数据,而是用来提醒进程一个事件已经发生。
套接字 Socket多数操作系统通过网络接口发送到同一台计算机上的不同进程或网络上的另一台计算机的数据。 面向流(TCP;通过套接字写入的数据需要格式化以保留消息边界)或更罕见的面向消息(UDP、SCTP)。
Unix 套接字 (Unix domain socket)多数操作系统类似于 Internet 套接字,但所有通信都发生在内核中。域套接字使用文件系统作为它们的地址空间。进程引用一个域套接字作为一个inode,多个进程可以与一个套接字通信。
消息队列 (Message queue)多数操作系统类似于套接字的数据流,但通常保留消息边界。通常由操作系统实现,它们允许多个进程读取和写入消息队列,而无需相互直接连接。
匿名管道(Anonymous pipe)所有的 POSIX 系统, Windows.使用标准输入和输出的单向数据通道。 写入管道写端的数据由操作系统缓存,直到从管道的读端读取。 通过使用两个“方向”相反的管道,可以实现进程之间的双向通信。
命名管道(Named pipe)所有的 POSIX 系统, Windows.被视为文件的管道。进程不像匿名管道那样使用标准输入和输出,而是从命名管道中写入和读取,就好像它是一个普通文件一样。
信号量所有的 POSIX 系统, Windows.从一个进程发送到另一个进程的系统消息,通常不用于传输数据,而是用于远程命令合作进程。
共享内存(Shared memory)所有的 POSIX 系统, Windows.多个进程可以访问同一个内存块,这为进程之间的通信创建了一个共享缓冲区。
消息传递(Message passing)用于MPI规范,Java RMICORBAMSMQMailSlot以及其他.允许多个程序使用消息队列和/或非操作系统托管通道进行通信。常用于并发模型。
内存映射文件(Memory-mapped file)所有的 POSIX 系统, Windows.映射到 RAM 的文件,可以通过直接更改内存地址而不是输出到流来修改。这与标准文件具有相同的好处。

文件

信号(Signals)

在计算机科学中,信号(英语:Signals)是Unix类Unix以及其他POSIX兼容的操作系统中进程间通讯的一种有限制的方式。它是一种异步的通知机制,用来提醒进程一个事件已经发生。当一个信号发送给一个进程,操作系统中断了进程正常的控制流程,此时,任何非原子操作都将被中断。如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数。

信号类似于中断,不同之处在于中断由处理器调解并由内核处理,而信号由内核调解(可能通过系统调用)并由进程处理。内核可以将中断作为信号传递给导致中断的进程(典型的例子有SIGSEGV、SIGBUS、SIGILL和SIGFPE)。

信号起源于20世纪70年代的贝尔实验室Unix,最近在POSIX标准中有所规定。

嵌入式程序可能会发现信号对于进程间通信很有用,因为信号的计算和内存占用很小。

套接字(Sockets)

伯克利套接字(英语:Internet Berkeley sockets) ,又称为BSD 套接字(BSD sockets)是一种应用程序接口(API),用于网络套接字( socket)与Unix域套接字,包括了一个用C语言写成的应用程序开发库,主要用于实现进程间通讯,在计算机网络通讯方面被广泛使用。

Berkeley套接字(也作BSD套接字应用程序接口)刚开始是4.2BSD Unix操作系统(于1983发布)的一套应用程序接口。然而,由于AT&T的专利保护着UNIX,所以只有在1989年伯克利大学才能自由地发布自己的操作系统和网络库。

Berkeley套接字应用程序接口形成了事实上的网络套接字的标准精髓。 大多数其他的编程语言使用与这套用C语言写成的应用程序接口[1] 类似的接口。 这套应用程序接口也被用于Unix域套接字(Unix domain sockets),后者可以在单机上为进程间通讯(IPC)的接口。

Berkeley套接字接口,一个应用程序接口(API),使用一个Internet套接字的概念,使主机间或者一台计算机上的进程间可以通讯。 它可以在很多不同的输入/输出设备和驱动之上运行,尽管这有赖于操作系统的具体实现。 接口实现用于TCP/IP协议,因此它是维持Internet的基本技术之一。 它是由加利福尼亚的伯克利大学开发,最初用于Unix系统。 如今,所有的现代操作系统都有一些源于Berkeley套接字接口的实现,它已成为连接Internet的标准接口。

套接字接口的接入有三个不同的级别,最基础的也是最有效的就是raw socket级别接入。 很少的应用程序需要在外向通讯控制的这个级别接入,所以raw socket级别是只为了用于开发计算机Internet相关技术的。 最近几年,大多数的操作系统已经实现了对它的全方位支持,包括Windows XP。

TCP 套接字流程图

消息队列

计算机科学中,消息队列(英语:Message queue)是一种进程间通信或同一进程的不同线程间的通信方式,软件贮列用来处理一系列的输入,通常是来自用户。消息队列提供了异步通信协议,每一个贮列中的纪录包含详细说明的资料,包含发生的时间,输入设备的种类,以及特定的输入参数,也就是说:消息的发送者和接收者不需要同时与消息队列交互。消息会保存在队列中,直到接收者取回它。

实现

消息队列常常保存在链表结构中。[2]拥有权限的进程可以向消息队列中写入或读取消息。

优缺点

消息队列本身是异步的,它允许接收者在消息发送很长时间后再取回消息,这和大多数通信协议是不同的。例如WWW中使用的HTTP协议(HTTP/2之前)是同步的,因为客户端在发出请求后必须等待服务器回应。然而,很多情况下我们需要异步的通信协议。比如,一个进程通知另一个进程发生了一个事件,但不需要等待回应。但消息队列的异步特点,也造成了一个缺点,就是接收者必须轮询消息队列,才能收到最近的消息。

信号相比,消息队列能够传递更多的信息。与管道相比,消息队列提供了有格式的数据,这可以减少开发人员的工作量。[2]但消息队列仍然有大小限制。

消息队列除了可以当不同线程进程间的缓冲外,更可以透过消息队列当前消息数量来侦测接收线程进程性能是否有问题。

管道(Pipeline)

类Unix操作系统(以及一些其他借用了这个设计的操作系统,如Windows)中,管道(英语:Pipeline)是一系列将标准输入输出链接起来的进程,其中每一个进程的输出被直接作为下一个进程的输入。 每一个链接都由匿名管道实现[来源请求]。管道中的组成元素也被称作过滤程序

示例

实现

在大多数类UNIX操作系统中,管线上的所有进程同时启动,输入输出流也已经被正确地连接,并且这些进程被调度程序所管理。最为重要的一点就是,所有的UNIX管道和其他管道实现不一样的地方就是缓存的概念:输出进程可能会以每秒5000 byte的速度输出,但是接收进程也许每秒只能接收100 byte,但不会有数据丢失。原因就是管道上游的进程的所有输出都会被放入一个队列中。当下游进程开始接收数据时,操作系统就会将数据从队列传至接收进程,并将传完的数据从队列中移除。当缓存队列空间不足时,上游进程会被终止,直到接收进程读取数据为上游进程腾出空间。在Linux中,缓存队列的大小是65536 byte。

匿名管道

使用C语言在UNIX中使用pipe(2)系统调用时,这个函数会让系统构建一个匿名管道,这样在进程中就打开了两个新的,打开的文件描述符:一个只读端和一个只写端。管道的两端是两个普通的,匿名的文件描述符,这就让其他进程无法连接该管道。 为了避免死锁并利用进程的并行运行的好处,有一个或多个管道的UNIX进程通常会调用fork(2)产生新进程。并且每个子进程在开始读或写管道之前都会关掉不会用到的管道端。或者进程会产生一个子线程并使用管道来让线程进行数据交换。

命名管道 (Named pipe)

命名管道是计算机进程间的一种先进先出通信机制。是类Unix系统传统管道的扩展。传统管道属于匿名管道,其生存期不超过创建管道的进程的生存期。但命名管道的生存期可以与操作系统运行期一样长。

信号量(Semaphore)

信号量(英语:semaphore)又称为信号标,是一个同步对象,用于保持在0至指定最大值之间的一个计数值。当线程完成一次对该semaphore对象的等待(wait)时,该计数值减一;当线程完成一次对semaphore对象的释放(release)时,计数值加一。当计数值为0,则线程等待该semaphore对象不再能成功直至该semaphore对象变成signaled状态。semaphore对象的计数值大于0,为signaled状态;计数值等于0,为nonsignaled状态.

信号量的概念是由荷兰计算机科学家艾兹赫尔·戴克斯特拉(Edsger W. Dijkstra)发明的,广泛的应用于不同的操作系统中。在系统中,给予每一个进程一个信号量,代表每个进程目前的状态,未得到控制权的进程会在特定地方被强迫停下来,等待可以继续进行的信号到来。如果信号量是一个任意的整数,通常被称为计数信号量(Counting semaphore),或一般信号量(general semaphore);如果信号量只有二进制的0或1,称为二进制信号量(binary semaphore)。

共享内存(Shared memory)

共享内存指在多处理器计算机系统中,可以被不同中央处理器访问的大容量内存。由于多个CPU需要快速访问存储器,这样就要对存储器进行缓存。由于其他处理器可能也要存取,任一缓存数据更新后,共享内存就需要立即更新,否则不同处理器可能用到不同的数据(参见缓存一致内存一致)。

共享内存的类似方案有分布内存分布共享内存,用以解决同类问题。

消息传递(Message passing)

计算机科学中,消息传递(英语:Message passing)是一种通信的形式,在并发计算并行计算面向对象编程进程间通信中使用。在这种模式中,行程或对象以发送及接收消息的方式来达成同步

不同于传统程序设计通过名字直接调用(invoking)一个进程、子例程或者函数,消息传递直接发送消息给一个进程,依赖进程或基础框架来调用实际执行的代码。 可分为同步方式与异步方式。

消息传递是一种通信范型,在这种模型中,由一个传信者,将消息(messages)送给一个,或多个收信者。消息的形式,根据操作系统与编程语言的支持,而有所不同,常见的有方法(method),信号(signals)与数据包(data packets)。

内存映射文件(Memory-mapped file)

内存映射文件(Memory-mapped file),或称“文件映射”、“映射文件”,是一段虚内存逐字节对应于一个文件或类文件的资源,使得应用程序处理映射部分如同访问主内存

优缺点

主要用处是增加I/O性能,特别是用于大文件。对于小文件,内存映射文件会导致碎片空间浪费,[1]因为内存映射总是要对齐页边界,这起码是4 KiB。因而一个5 KiB文件将会映射占用8 KiB内存,浪费了3 KiB内存。访问内存映射文件比直接文件读写要快几个数量级。

内存映射文件可以只加载一部分内容到用户的逻辑内存空间。这对非常大的文件特别有用。

使用内存映射文件可以避免颠簸:把相当大的文件直接加载到内存时,由于可用内存不足,使得一边读取文件内存,同时把部分已经加载的文件从内存写入硬盘虚存文件中。

内存映射文件由操作系统的内存管理程序负责,因此绕过了硬盘虚存的分页文件(page file)。[2]

内存映射文件需要在进程的占用一块很大的连续逻辑地址空间。对于Intel的IA-32的4 GiB逻辑地址空间,可用的连续地址空间远远小于2---3 GiB。

相关联的文件的I/O错误(如可拔出驱动器或光驱被弹出,磁盘满时写操作等)的内存映射文件会向应用程序报告SIGSEGV/SIGBUS信号(POSIX环境)或EXECUTE_IN_PAGE_ERROR结构化异常(Windows环境)。通常的内存操作是无需考虑这些异常的。

有内存管理单元(MMU)才支持内存映射文件。

参考文献

Q.E.D.