什么是进程间通信
进程是系统分配资源的最小单位。由于虚拟内存的实现,每个进程都有独立的虚拟地址空间,当进程真正使用这块空间时,系统再将虚拟地址与物理地址进行映射,实现真正的系统分配。
这样,每个进程都有自己独立的系统资源,彼此之间是相互隔离的。而有时需要不同的进程互相访问资源并协调工作,才有了进程间通信。
进程间通信原理
每个进程的虚拟地址是相互独立的,首先看看虚拟地址空间的内存是如何分配的。
对于32位操作系统来说,虚拟地址空间的大小为2的32次方即4G。其中高1G为内核空间,低3G为用户空间。
用户空间
进程本身产生的数据基本存储到了用户空间中。常说的进程空间相互独立,说的就是用户空间。用户空间比较特殊的就是共享内存区,也就是图中的共享库的存储器映射区域,这部分空间是可以实现多个进程共同访问的。
内核空间
可以看到,内核空间中物理存储器、内核代码和数据,对于每个进程都是一致的。换句话说,这部分空间也是被所有进程所共享的。
用户空间共享内存区和内核空间的存在,为进程间通信的实现提供了可能。所有进程间通信基本都是基于这两部分空间实现的。
进程间通信场景
数据传输
一个集成需要将它的数据发送给另一个进程
资源共享
多个进程之间共享同样的资源
事件通知
一个进程需要想另一个或一组进程发送消息,通知他们发生了某种事件
进程控制
有些进程希望完全控制另一个进程的执行,该控制进程希望能够拦截另一个进程的所有操作,并能够及时知道它的状态改变。
进程间通信实现方式
进程间通信主要包括管道, system V IPC
(包括消息队列,信号量,共享存储), SOCKET
.
管道,英文为pipe
。这是一个我们在学习Linux命令行的时候就会引入的一个很重要的概念。它的发明人是道格拉斯.麦克罗伊,这位也是UNIX上早期shell的发明人。他在发明了shell之后,发现系统操作执行命令的时候,经常有需求要将一个程序的输出交给另一个程序进行处理,所以,管道的概念应运而生。
Unix发展做出重大贡献的两大主力AT&T的贝尔实验室及BSD(加州大学伯克利分校的伯克利软件发布中心)在进程间通信方面的侧重点有所不同。前者对Unix早期的进程间通信手段进行了系统的改进和扩充,形成了“system V IPC”,通信进程局限在单个计算机内;后者则跳过了该限制,形成了基于套接口(socket)的进程间通信机制。Linux则把两者继承了下来.
管道
遵循UNIX
“一切皆文件”的设计原则,管道本质上就是个文件。使用方法与文件类似,都能使用read
、write
、open
等普通IO函数。
虽然实现形态上是文件,但是管道本身并不占用磁盘或者其他外部存储的空间。在Linux的实现上,它占用的是内存空间。所以,Linux上的管道就是一个操作方式为文件的内存缓冲区。
管道是基于内核空间实现进程间通信的。
Linux上的管道分两种类型:
- 匿名管道
- 命名管道
匿名管道
匿名管道最常见的形态就是我们在shell操作中最常用的”|”。
因为是匿名管道,不同的进程无法找到这块内存进行访问,所以只能在父子进程中使用。
父进程在产生子进程前必须打开一个管道文件,然后fork产生子进程,这样子进程通过拷贝父进程的进程地址空间获得同一个管道文件的描述符,以达到使用同一个管道通信的目的。此时除了父子进程外,没人知道这个管道文件的描述符,所以通过这个管道中的信息无法传递给其他进程。这保证了传输数据的安全性,当然也降低了管道了通用性。
匿名管道实现的是一个半双工通信的机制。使用同一个管道的父子进程可以分时给对方发送消息。
在管道中没有数据时,对管道的读操作会阻塞,知道管道内有数据为止。当一次写的数据量不超过管道容量的时候,对管道的写操作一般不会阻塞,直接将要写的数据写入管道缓冲区即可。
命名管道
由于匿名管道值限制于本地具有亲缘关系的进程之间通信, 极大地限制了匿名管道的使用. 但命名管道就解决了这两个问题.
Linux
提供了FIFO
方式进行进行间通信, FIFO又叫做命名管道(named pipe).
FIFO (First in, First out)
为一种特殊的文件类型, 不同于普通文件, 它在文件系统中有对应的路径. 当一个进程以读(r)的方式
打开该文件,而另一个进程以写(w)的方式打开该文件, 那么内核就会在这两个进程之间建立管道. 所以FIFO
实际上也
由内核管理,不与硬盘打交道. 之所以叫FIFO
,是因为这个管道本质上是一个先进先出的队列数据结构,最早放入的数据
被最先读出来, 从而保证信息交流的顺序. FIFO
只是借用了文件系统来为管道命名, 有了文件名, 就好让不具有亲缘关系的
进程也可以访问到同一块缓冲区. 当删除FIFO
文件时,管道连接也随之消失 .
system V IPC
System V (SysV) UNIX
规范描述了以下三种 IPC
机制,它们统称为 SysV IPC
:
- 消息队列
- 信号量
- 共享内存
而这三种IPC
方式通常还有另一种规范,即POSIX
System V(System Five)
,是Unix
操作系统众多版本中的一支。POSIX兼容了绝大部分System V
的规格,减少了各类操作系统之间移植的麻烦。
消息队列
消息队列是一个FIFO
的消息链表。与传统的PIPE
相比较,消息队列的优点是:一、队列内容以消息格式存放,而不是无格式的字节流,并且消息的格式可以自行定义;二、每个消息都应有一个类型域,接收进程可以根据它有选择地接收消息。
信号量
信号量是具有整数值的对象,它支持两种原子操作P()
和V()
。P()
操作减少信号量的值,如果新的值小于零则操作阻塞;V()
操作增加信号量的值,如果新的值大于等于零,则唤醒一个等待的线程或进程(如果有的话)。
信号量主要用来实现同步协议,为相互操作的进程提供复杂同步的支持。
共享内存
共享内存区域是被多个进程所共享的一部分物理内存,这些区域被进程映射到各自的地址空间(用户空间)后,就可以向其他内存地址一样被访问,而不需要另外的读写调用。所以共享内存是进程之间共享数据最快的方法。
共享内存的主要问题是它本身没有提供同步,为了避免数据混淆,使用共享内存的进程必须设计自己的同步协议。
共享内存是进程间通信方式中效率最高的。但是因为共享内存没有提供相应的互斥机制,所以一般共享内存都和信号量配合起来使用。
为什仫共享内存的方式比其他进程间通信的方式效率高?
消息队列,FIFO
,管道的消息传递方式一般为 :
- 服务器获取输入的信息;
- 通过管道,消息队列等写入数据至内存中,通常需要将该数据拷贝到内核中;
- 客户从内核中将数据拷贝到自己的客户端进程中;
- 然后再从进程中拷贝到输出文件;
而共享内存只需要:
- 输入内容到共享内存区;
- 从共享内存输出到文件
上述过程不涉及到内核的拷贝,这些进程间数据的传递就不再通过执行任何进入内核的系统调用来传递彼此的数据,节省了时间,所以共享内存是这五种进程间通信方式中效率最高的。
socket
socket API
原本是为网络通讯设计的,但后来在socket
的框架上发展出一种IPC机制,就是UNIX Domain Socket
。虽然网络socket
也可用于同一台主机的进程间通讯(通过loopback
地址127.0.0.1
),但是UNIX Domain Socket
用于IPC更有效率:不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。这是因为,IPC机制本质上是可靠的通讯,而网络协议是为不可靠的通讯设计的。UNIX Domain Socket
也提供面向流和面向数据包两种API接口,类似于TCP和UDP,但是面向消息的UNIX Domain Socket
也是可靠的,消息既不会丢失也不会顺序错乱。
UNIX Domain Socket是
全双工的,API接口语义丰富,相比其它IPC机制有明显的优越性,目前已成为使用最广泛的IPC机制,比如X Window
服务器和GUI程序之间就是通过UNIXDomain Socket
通讯的