一文搞懂进程间通信

文章目录

    • 一文搞懂进程间通信
      • 管道
        • 管道的创建
        • 管道在进程间通信
        • 管道的分类
          • 匿名管道
          • 命名管道
      • 消息队列
        • 不足之处
      • 共享内存
      • 信号量
        • 控制信号量的操作
      • 信号
        • 信号的处理方式
      • Socket
        • 创建Socket
        • Socket的通信方式
          • 针对TCP协议通信的socket编程模型
          • 针对UDP协议通信的socket编程模型
          • 针对本地进程间通信的socket编程模型
      • 总结

一文搞懂进程间通信

​ 由于每个进程的用户地址空间都是独立存在的,一般来说不能相互访问,但是内核空间是每个进程都共享的。所以进程之间可以通过内核空间进行通信。

管道

​ 首先需要明确的是,管道传输数据是单向的,导致效率低下,不适合进程间频繁的交换数据。但是好处就是简单,我们也容易得知管道里的数据已经被另一个进程读取了。

管道的创建

int pipe(int fd[2])

​ 这里创建了一个匿名管道,并返回了两个描述符,一个是管道的读取端描述符fd[0],另一个是管道的写入端描述符fd[1]。

​ 其值存在于内存,不存在与文件系统中,其实质就是内核里面的一串缓存。

​ 管道传输的数据是无格式的流且大小受限。而且是遵循先进先出原则

管道在进程间通信

​ 使用fork创建子进程,由于创建的子进程会赋值父进程的文件描述符,由于两个进程可以通过各自的fd写入和读取同一管道文件实现跨进程通信了

​ 但是由于管道只能一端写入一端读出,由于上面的模式可以同时写入或读取,造成混乱,所以父进程关闭读取(fd[0]),只保留写入(fd[1]),子进程关闭写入(fd[1]),只保留读取(fd[1])。

​ 当需要双向通信的时候就创建两个管道

管道的分类

匿名管道

​ 对于匿名管道来说,它的通信范围只能是父子进程中

​ 生命周期是随着进程的创建而创建,随着进程的结束而销毁

命名管道

​ 对于命名管道来说,由于有命名,所以它可以在不相关的进程间也能相互通信

消息队列

  • 适合频繁的数据交换
  • 信息通信方式:一方将数据放到消息队列,一方再获取。
  • 消息队列是保存在内核中的消息链表,在发送数据的时候会分成一个一个独立的数据单元(消息体),一旦有进程读取了消息体,此消息体就会被删除
  • 消息队列的生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会一直存在。

不足之处

  • 通信不及时,附件大小有限制

  • 不适合传输较大的数据,因为在内核中每个消息体都有一个最大长度的限制,同时所有队列所包含的全部信息体的总长度也是有上限的。

    在linux内核中有两个宏定义MSGMAX和MSGMNB,以字节为单位,分别规定了一条消息的最大长度和一个队列的最大长度

  • 消息队列通信过程中存在用户态和内核态之间的数据拷贝开销

共享内存

​ 共享内存的机制就是拿出一块虚拟地址空间来,映射到相同的物理内存中。

信号量

​ 为了防止多进程竞争共享资源,而造成的数据错乱,所以需要保护机制,使得共享的资源,在任意时刻只能被一个进程访问。

​ 信号量其实是一个整形的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据

控制信号量的操作

​ 信号量表示资源的数据,控制信号量的方式有两种原子操作

  • P操作:会把信号量减一,要是结果小于0,表示资源已经被占用,进程需要阻塞等待。要是大于等于0,表示还有资源可以使用,进程正常继续执行

  • V操作:会把信号量加一,相加后要是小于等于0,则表示当前有阻塞中的进程,于是会将该进程唤醒,如果大于0 表示没有阻塞中的进程

    P操作是用在进入共享资源之前,V操作是用于离开共享资源以后,必须成对出现。

信号初始化为1,表示是互斥信号量

信号初始化为0,表示是同步信号量

信号

​ 上面说的进程间通信,都是在常规状态下的工作模式,对于异常情况下的工作模式,就需要用信号的方式通知进程。

​ 信号事件的来源主要有硬件来源和软件来源

信号的处理方式

​ 信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程,一旦有信号产生我们就有下面几种,用户进程对信号的处理方式

  • 执行默认操作:linux对每种信号都规定了默认操作。

  • 捕捉信号:我们可以为信号定义一个信号处理函数。当信号发生时,我们就执行相应的信号处理函数

  • 忽略信号:当我们不希望处理某些信号的时候,就可以忽略该信号。

    但是有两个信号是应用进程无法捕捉和忽略的,即SIGKILL和SEGSTOP,它们用于在任何时候中断或结束某一进程

Socket

​ 前面提到的管道、消息队列、共享内存、信号量和信号都是在同一台主机上进行进程间的通信,要想跨网络与不同主机上的进程之间通信就需要Socket通信。

​ Socket通信不仅可以跨网络与不同主机的进程间通信,还可以在同主机上进程间通信

创建Socket

​ 首先先看一下创建Socket的系统调用

int socket(int domain,int type,int protocal)

三个参数的含义:

  • domain:用来指定协议族,比如AF_INET用于IPV4、AF_INET6 用于 IPV6、AF_LOCAL/AF_UNIX 用于本机;
  • type:用来指定通信特性,比如 SOCK_STREAM 表示的是字节流,对应 TCP、SOCK_DGRAM 表示的是数据报,对应 UDP、SOCK_RAW 表示的是原始套接字;
  • protocal:原本是用来指定通信协议,但是基本废弃,一般写0

Socket的通信方式

​ 根据创建的socket类型的不同,通信的方式也就不同

  • 实现TCP字节流通信: socket 类型是 AF_INET 和 SOCK_STREAM;
  • 实现 UDP 数据报通信:socket 类型是 AF_INET 和 SOCK_DGRAM;
  • 实现本地进程间通信: 「本地字节流 socket 」类型是 AF_LOCAL 和 SOCK_STREAM,「本地数据报 socket 」类型是 AF_LOCAL 和 SOCK_DGRAM。另外,AF_UNIX 和 AF_LOCAL 是等价的,所以 AF_UNIX 也属于本地 socket;
针对TCP协议通信的socket编程模型
  • 过程:
    • 服务端和客户端初始化socket,得到文件描述符
    • 服务端调用bind,将绑定在ip地址和端口
    • 服务端调用listen,进行监听
    • 服务端调用accept,等待客户端连接
    • 客户端调用connect,向服务器端的地址和端口发起连接请求
    • 服务器accept返回用于传输的socket的文件描述符
    • 客户端调用write写入数据,服务器端调用read读取数据
    • 客户端断开连接时,会调用close,那么服务器read读取数据的时候,就会读取到了EOF,待处理完数据后,服务器调用close,表示连接关闭

​ 服务器调用accept时,连接成功了会返回一个已完成连接的socket,后续用来传输数据

​ 所以监听的socket和真正用来传输数据的socket是两个socket,一个叫做监听socket一个叫做已完成连接的socket

针对UDP协议通信的socket编程模型

​ UDP是无连接的,所以不需要三次握手,也就不需要想TCP那样调用listen和connect,但是UDP的交互仍是需要IP地址和端口的,因此也需要bind

​ 由于不需要维护连接,也就没有所谓的发送方和接受方,只要有一个socket多台机器就可以任意通信,因此每一个UDP的socket都需要bind

​ 另外每次通信时,调用sendto和recvfrom,都要传入目标主机的IP地址和端口

针对本地进程间通信的socket编程模型

本地socket被用于在同一台主机上进程间通信的场景

  • 本地socket的编程接口和IPv4 、IPv6 套接字编程接口是一致的,可以支持「字节流」和「数据报」两种协议;
  • 本地socket的实现效率大大高于IPv4和IPv6的字节流、数据报socket实现

本地字节流 socket 和 本地数据报 socket 在 bind 的时候,不像 TCP 和 UDP 要绑定 IP 地址和端口,而是绑定一个本地文件,这也就是它们之间的最大区别。

总结

​ 由于每个进程的用户空间都是独立的,不能相互访问,这时就需要借助内核空间来实现进程间通信,原因很简单,每个进程都是共享一个内核空间。

​ Linux 内核提供了不少进程间通信的方式,其中最简单的方式就是管道,管道分为「匿名管道」和「命名管道」。

匿名管道顾名思义,它没有名字标识,匿名管道是特殊文件只存在于内存,没有存在于文件系统中,shell 命令中的「|」竖线就是匿名管道,通信的数据是无格式的流并且大小受限,通信的方式是单向的,数据只能在一个方向上流动,如果要双向通信,需要创建两个管道,再来匿名管道是只能用于存在父子关系的进程间通信,匿名管道的生命周期随着进程创建而建立,随着进程终止而消失。

命名管道突破了匿名管道只能在亲缘关系进程间的通信限制,因为使用命名管道的前提,需要在文件系统创建一个类型为 p 的设备文件,那么毫无关系的进程就可以通过这个设备文件进行通信。另外,不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则,不支持 lseek 之类的文件定位操作。

消息队列克服了管道通信的数据是无格式的字节流的问题,消息队列实际上是保存在内核的「消息链表」,消息队列的消息体是可以用户自定义的数据类型,发送数据时,会被分成一个一个独立的消息体,当然接收数据时,也要与发送方发送的消息体的数据类型保持一致,这样才能保证读取的数据是正确的。消息队列通信的速度不是最及时的,毕竟每次数据的写入和读取都需要经过用户态与内核态之间的拷贝过程。

共享内存可以解决消息队列通信中用户态与内核态之间数据拷贝过程带来的开销,它直接分配一个共享空间,每个进程都可以直接访问,就像访问进程自己的空间一样快捷方便,不需要陷入内核态或者系统调用,大大提高了通信的速度,享有最快的进程间通信方式之名。但是便捷高效的共享内存通信,带来新的问题,多进程竞争同个共享资源会造成数据的错乱。

​ 那么,就需要信号量来保护共享资源,以确保任何时刻只能有一个进程访问共享资源,这种方式就是互斥访问。信号量不仅可以实现访问的互斥性,还可以实现进程间的同步,信号量其实是一个计数器,表示的是资源个数,其值可以通过两个原子操作来控制,分别是 P 操作和 V 操作

​ 与信号量名字很相似的叫信号,它俩名字虽然相似,但功能一点儿都不一样。信号是进程间通信机制中唯一的异步通信机制,信号可以在应用进程和内核之间直接交互,内核也可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令),一旦有信号发生,进程有三种方式响应信号 1. 执行默认操作、2. 捕捉信号、3. 忽略信号。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILLSEGSTOP,这是为了方便我们能在任何时候结束或停止某个进程。

​ 前面说到的通信机制,都是工作于同一台主机,如果要与不同主机的进程间通信,那么就需要 Socket 通信了。Socket 实际上不仅用于不同的主机进程间通信,还可以用于本地主机进程间通信,可根据创建 Socket 的类型不同,分为三种常见的通信方式,一个是基于 TCP 协议的通信方式,一个是基于 UDP 协议的通信方式,一个是本地进程间通信方式。

你可能感兴趣的:(所有文章,Java,多线程以及并发)