说说IPC

954 阅读9分钟

说说IPC

IPC基本概念

IPC的全称是Inter-Process Communication,即进程间通讯。每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走。

 pic | center |

大概的流程是如图上所示那样。IPC大概可以通过几种方式实现,我们将展开说下其中的优缺点和特点。

管道

特性

  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。
  • 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)。
  • 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件。但它不同于普通文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。
  • 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。

示意图大概是这样的。

局限

  • 只支持单向数据流。
  • 只能用于具有亲缘关系的进程之间。
  • 没有名字。
  • 管道的缓冲区是有限的(管道制存在于内存中,在管道创建时,为缓冲区分配一个页面大小)。
  • 管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令、或记录)等等。

为了解决无名管道的问题,进而衍生出有名管道。有名管道不同于匿名管道之处在于它提供了一个路径名与之关联,以有名管道的文件形式存在于文件系统中。这样,即使与有名管道的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过有名管道相互通信。因此,通过有名管道不相关的进程也能交换数据。值的注意的是,有名管道严格遵循先进先出。

实质
管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据,管道一端的进程顺序的将数据写入缓冲区,另一端的进程则顺序的读出数据。

该缓冲区可以看做是一个循环队列,读和写的位置都是自动增长的,不能随意改变,一个数据只能被读一次,读出来以后在缓冲区就不复存在了。

当缓冲区读空或者写满时,有一定的规则控制相应的读进程或者写进程进入等待队列,当空的缓冲区有新数据写入或者满的缓冲区有数据读出来时,就唤醒等待队列中的进程继续读写。

看上去非常像java中的队列。

信号

特性

  • 信号是Linux系统中用于进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态。
  • 如果该进程当前并未处于执行状态,则该信号就有内核保存起来,知道该进程回复执行并传递给它为止。
  • 如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消是才被传递给进程。

大概的流程是这样的。

生命周期

  • 信号被某个进程产生,并设置此信号传递的对象(一般为对应进程的pid),然后传递给操作系统。
  • 操作系统根据接收进程的设置(是否阻塞)而选择性的发送给接收者,如果接收者阻塞该信号(且该信号是可以阻塞的),操作系统将暂时保留该信号而不传递,直到该进程解除了对此信号的阻塞(如果对应进程已经退出,则丢弃此信号),如果对应进程没有阻塞,操作系统将传递此信号。
  • 目的进程接收到此信号后,将根据当前进程对此信号设置的预处理方式,暂时终止当前代码的执行,保护上下文(主要包括临时寄存器数据,当前程序位置以及当前CPU的状态)、转而执行中断服务程序,执行完成后在回复到中断的位置。当然,对于抢占式内核,在中断返回时还将引发新的调度。

消息队列

特性

  • 消息队列是存放在内核中的消息链表,每个消息队列由消息队列标识符表示。
  • 与管道(无名管道:只存在于内存中的文件 | 命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启或者显示地删除一个消息队列时,该消息队列才会被真正的删除。
  • 另外与管道不同的是,消息队列在某个进程往一个队列写入消息之前,并不需要另外某个进程在该队列上等待消息的到达。

对比

  • 消息承载:消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺。
  • 消息读取:消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。比FIFO更有优势。

分类

  • POSIX消息队列。
  • System V消息队列,目前使用较多。

共享内存

特性

  • 使得多个进程可以可以直接读写同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。
  • 为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大提高效率。
  • 由于多个进程共享一段内存,因此需要依靠某种同步机制(如信号量)来达到进程间的同步及互斥。

举个例子,我们经常提及的mmap就是共享内存的一种方式。

套接字

套接字的特性由3个属性确定,它们分别是:域、端口号、协议类型。

  1. AF_INET:它指的是网络。当客户使用套接字进行跨网络的连接时,它就需要用到服务器计算机的IP地址和端口来指定一台联网机器上的某个特定服务。所以在使用socket作为通信的终点,服务器应用程序必须在开始通信之前绑定一个端口,服务器在指定的端口等待客户的连接。
  2. AF_UNIX:表示UNIX文件系统,它就是文件输入/输出,而它的地址就是文件名。

端口号
每一个基于TCP/IP网络通讯的进程都被赋予了唯一的端口和端口号,端口是一个信息缓冲区,用于保留Socket中的输入/输出信息。端口号是一个16位无符号整数,以区别主机上的每一个程序(端口号就像房屋中的房间号),低于256的端口号保留给标准应用程序。每一个套接字都组合进了IP地址、端口,这样形成的整体就可以区别每一个套接字。

协议类型

  • 流套接字:是在AF_INET域中通过TCP/IP连接实现,同时也是AF_UNIX中常用的套接字类型。流套接字提供的是一个有序、可靠、双向字节流的连接,因此发送的数据可以确保不会丢失、重复或乱序到达,而且它还有一定的出错后重新发送的机制。
  • 数据报套接字:它不需要建立连接和维持一个连接,它们在域中通常是通过UDP/IP协议实现的。它对可以发送的数据的长度有限制,数据报作为一个单独的网络消息被传输。它可能会丢失、复制或错乱到达。UDP不是一个可靠的协议,但是它的速度比较高,因为它并一需要总是要建立和维持一个连接。
  • 原始套接字:原始套接字允许对较低层次的协议直接访问,比如IP、 ICMP协议。它常用于检验新的协议实现,或者访问现有服务中配置的新设备,因为原始套接字可以自如地控制Windows下的多种协议,能够对网络底层的传输机制进行控制,所以可以应用原始套接字来操纵网络层和传输层应用。比如,我们可以通过原始套接字来接收发向本机的ICMP、IGMP协议包,或者接收TCP/IP栈不能够处理的IP包,也可以用来发送一些自定包头或自定协议的IP包。网络监听技术很大程度上依赖于SOCKET_RAW

原始套接字和标准套接字
原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送数据必须使用原始套接字。

信号量

信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程(或线程)间同步。为了获得共享资源,进程需要执行下列操作:

  • 创建一个信号量:这要求调用者指定初始值,对于二值信号量来说,它通常是1,也可是0。
  • 等待一个信号量:该操作会测试这个信号量的值,如果小于0,就阻塞。也称为P(通过)操作。
  • 挂出一个信号量:该操作将信号量的值加1,也称为V(释放)操作。

进程间同步

线程间同步

我之前在说Go中的lock的时候说过,go底层的锁就是通过一个信号量来实现的。