进程间通信(Inter-Process Communication)是指两个进程间传递数据的方法。在Linux下常见的有管道、FIFO、POSIX定义的共享内存,信号量和消息队列、System V定义的共享内存,信号量和消息队列,以及套接字。
POSIX IPC是由IEEE定义的API,而System V IPC是由AT&T定义的。它们都提供了相似的设施,基本概念是相同的,但接口不同,除此之外还有一些其他的差异。
这篇文章没有介绍信号量,因为它主要用于进程或线程间的同步。由于大部分图都是通过mermaid语法自动生成的,所以看起来有一些奇怪。
一个管道可用于两个进程间的单向通信。一个管道有一个读端和一个写端,从写端写入的数据可以从读端读取到。
通过函数pipe()
创建一个管道,它返回两个文件描述符fd[0]和fd[1]
,一个表示读端,一个表示写端。之后进程就可以通过read()
和write()
从管道中读取和写入数据。
通常管道只用于具有关系的进程间通信,例如进程调用pipe()
创建管道,然后通过fork()
创建子进程,之后父进程和子进程之间就可以通过pipe()
返回的文件描述符通信,如下图所示:
上图表示父进程向子进程发送数据,子进程向父进程发送数据是类似的,但通常一个管道只允许一个方向的通信。
FIFO也叫做命名管道,它是一种文件类型,在文件系统中有一个文件名,但这个文件没有实际内容,它仅作为一个引用点,以便进程可以通过文件名访问管道。
FIFO可以同时被多个进程打开进行读写,它可用于不相关的进程间通信。
通过函数mkfifo()
创建一个FIFO。通过函数open
打开一个FIFO,之后进程就可以通过read()
和write()
从FIFO中读取和写入数据。
管道和FIFO的主要差异在于它们的创建和打开方式,之后的IO操作基本相似。实际上,对于每个打开FIFO文件的进程,内核都将维护一个管道对象。
POSIX共享内存接口通过将一个文件映射到进程存储空间的一块缓冲区上来实现进程间通信,从缓冲区中读数据就相当于读文件中的相应字节,向缓冲区中写数据时相应字节就被写入到文件。
void *mmap(void *addr, size_t len, int prot, int flag, int fd, off_t off );
通过函数mmap()
将一个文件映射到进一个进程的存储区:
一般来说,当一个进程将数据放入共享存储区时,在操作完成之前,另一个进程不应该读写该共享存储区,因此,进程间应该同步访问共享存储区,我们可以通过信号量或记录锁来实现同步。
因为数据不需要在通信的进程间复制,因此这是最快的一种IPC。
System V IPC提供了消息队列、信号量集和共享内存段。它们之间有一些相似的地方:
struct msqid_ds
(消息队列)和struct shmid_ds
(共享内存段),都通过一个标识符(ID)来引用。标识符(ID)是IPC对象的内部名。因此,这类IPC对象在创建时通常将传递一个KEY,然后返回一个ID。多个进程间可以通过相同的KEY来得到标识IPC对象的ID,所以它们可用于不相关的进程间通信。
消息队列是消息的链表,存储在内核中。消息队列可用于进程间的双向通信。一个消息是一个结构体,这个结构体的第一个字段的数据类型为long
,它表示消息的类型,第二个字段一般为消息的数据。一个消息链表大致如下图所示:
通过函数msgget()
创建或得到一个消息队列(如果它已经存在)。通过函数msgsnd()
将消息放到消息队列中。通过函数msgrcv()
从消息队列取得消息,因为消息具有类型,因此取消息时可以不取队头的消息,而是指定某个类型的消息。
System V共享内存在概念上类似于POSIX共享内存。通过函数shmget()
创建或引用一个现有的共享内存端,通过函数shmat()
将共享内存段连接到进程的地址空间中。
它们在概念上是相似的,但是System V共享内存段并不关联到一个实际的文件。
因为这类的IPC通常将KEY传给一个函数得到一个ID,这个ID标识了IPC对象。因为它们不使用文件描述符,所以不能直接对它们使用IO多路复用函数(select()
和pool()
等),为此需要做一些额外的工作。
前面的进程间通信机制允许同一台计算机上的进程间相互通信,而通过网络套接字还可用于不同计算机上的进程间通信。
字节流套接字是面向连接的、有序的、可靠的,它没有消息边界,即一次写可以对应一次读,也可以对应多次读;多次写可以对应一次读,也可以对应多次读。
在网络编程中,TCP套接字是常见的字节流套接字,客户和服务器程序所使用的套接字接口大致如下:
可以看到客户端和服务器在通信前需要建立连接。
数据报套接字是无连接的、不可靠的、且不保证数据有序,它有消息边界,即一次写对应一次读。
在网络编程中,UDP套接字是常见的数据报套接字,因为数据报套接字是无连接的,所以不区分客户和服务器。但我们假设有这种关系,那么客户和服务器程序所使用的套接字接口大致如下:
可以看到在逻辑上客户端和服务器不需要建立连接。
UNIX域套接字和网络套接字类似,它们都使用共同的套接字接口,只是它们使用的套接字地址结构不同,但无论是UNIX域数据报套接字还是UNIX域字节流套接字都是可靠的。
UNIX域套接字只用于同一台计算机上的进程间通信,并且不执行协议处理,因此当用于同一台计算机上的进程间通信时,UNIX域套接字的速度会更快。
下表是几种IPC的比较:
IPC类型 | 无连接 | 可靠的 | 支持消息类型 | 存在消息边界 | 进程不相关 |
---|---|---|---|---|---|
管道 | 否 | 是 | 否 | 否 | 否 |
FIFO | 否 | 是 | 否 | 否 | 是 |
消息队列 | 否 | 是 | 是 | 是 | 是 |
UNIX域数据报套接字 | 是 | 是 | 否 | 是 | 是 |
UNIX域字节流套接字 | 否 | 是 | 否 | 否 | 是 |
无连接是指进程之间是否有逻辑连接。由于是在同一台计算机上通信,所以它们都是可靠的。只有管道需要两个进程相关。
管道和FIFO是最简单的IPC。当发送端发送数据时,接收端被唤醒;当管道中的缓冲已满时,发送端将被阻塞;当管道中没有数据时,接收端将被阻塞。我们只需要写很少的代码,它们就能工作的很好。
在进程间传递较大的数据时,共享内存会比其他IPC更快,因为发送端将数据写入共享内存后,其他进程可以直接访问共享内存,而不需要将数据从共享内存中拷贝出来。但使用共享内存要处理进程间的同步问题。
消息队列是上面几种IPC中唯一一个支持消息类型的IPC,这使得某个进程可以只读取它感兴趣的数据。
网络套接字可用于不同计算机上的进程间通信,所以还需要处理额外的网络协议。而UNIX域套接字不需要处理网络协议,速度会更快,且与网络套接字使用相同的套接字接口。
《UNIX环境高级编程》、《UNIX网络编程》
pipe(7)、fifo(7)、sysvipc(7)、shm_overview(7)、unix(7)