zoukankan      html  css  js  c++  java
  • 进程间通信方式

    引言

    每个进程都拥有自己的用户地址空间,任何一个进程的全局变量在另一个进程中完全不可见,但是内核空间中每个进程都是共享的,所以进程之间要交换数据必须通过内核空间进行。在内核中开辟一块缓冲区,进程1把数据从用户空间拷贝到内核缓冲区,进程2再从内核缓冲区把数据读取走,内核提供的这种机制称之为进程间通信(ICPC,InterProcess Communication)。

    进程间通信机制共有七种,分别是匿名管道、命名管道、消息队列、共享内存、信号量、信号以及Scocket。各有不同的优缺点。下面我们分别来解释一下。

    匿名管道

    管道的本质实际上就是一个内核缓冲区,进程以先进先出(FIFO)的方式从缓冲区中存取数据。管道一端的进程将数据写入缓冲区中,写入的内容每次都添加在缓冲区的末尾;管道的另一端则从缓冲区中读取数据,每次读取的时候都是从缓冲区的头部读取数据,如下所示:

    由此,也可以看出,管道是半双工的,数据只能向一个方向流动。当双方都需要进行通信时,就需要建立起两个管道。

    Linux中,管道一个常见的表现形式就是「|」这个竖线了,如常见的操作

    ps -ef | grep redis
    

    ps是显示某个进程,grep是查找,中间的「|」就是一个创建管道的命令、这个「|」的功能就是将前一个命令(ps -ef)的输出当做是后一个命令的输入(grep redis)。同时可以看出,这种管道是没有任何名字的,所以「|」这种形式的管道也称之为匿名管道,当这条命令执行结束之后该匿名管道就会被销毁

    匿名管道的缺点

    • 半双工,数据只能单向流动,要是想双向流动,必须创建两个管道;
    • 匿名管道只适用于具有父子关系的进程间
    • 没有名字;
    • 效率低下,不适合进程间频繁交换数据
    • 管道的缓冲区大小有限,在管道创建的时候,就会为缓冲区分配一个页面大小;
    • 管道传输的数据都是无格式字节流,这就要求管道的写入端和读取端必须事先约定好数据的格式,便于写入和读取;

    匿名管道的创建

    Linux中,通过下面这个系统调用就可以创建一个匿名管道:

    int pipe(int fd[2])
    

    这里表示创建一个匿名管道,并返回两个文件描述符(PS,在Linux中一切皆文件),一个是管道的读取端描述符fd[0],一个是管道的写入端描述符fd[1]。这个匿名管道在Linux中构成一种特殊的文件系统,只存在于内存当中,不存在于文件系统中。

    在调用pipe函数创建匿名管道时,返回的两个文件描述符都是在同一个进程中,并没有起到进程间通信的作用,那么怎么样才能使得管道跨进程通信呢?

    我们可以使用fork命令来创建子进程,创建的子进程会复制父进程的文件描述符,这样两个进程分别就各有两个fd[0]fd[1],两个进程就可以通过各自的fd写入和读取同一个管道文件实现跨进程通信了。

    但是这样的话会造成混乱,因为父子进程都可以同时写入或者同时读取,但管道只能一端写入,另一端读取。因而,通常情况下会进行一些操作进行修正:

    • 父进程关闭读取的fd[0],只保留写入的fd[1]
    • 子进程关闭写入的fd[1],只保留读取的fd[0]

    这里也再次说明了管道半双工特性,要是想双向通信,需要创建两个管道。

    shell中的进程通信

    上面解释了使用管道进行父子进程间的通信过程,shell中还不一样。对于shell来说,当执行A|B这类命令时,AB进程其实都是shell进程的子进程,AB之间不存在父子关系。此时的通信过程如下所示:

    shell中,通过「|」匿名管道将多个命令连接在一起,实际上也就是创建了多个子进程,那么在我们编写 shell 脚本时,能使用一个管道搞定的事情,就不要多用一个管道,这样可以减少创建子进程的系统开销

    对于匿名管道,它的通信范围是存在父子关系的进程。因为管道没有实体,也就是没有管道文件,只能通过 fork 来复制父进程 fd 文件描述符,来达到通信的目的。

    命名管道

    匿名管道,由于没有名字,只能用于父子进程间的通信。为了克服这个缺陷,命名管道(FIFO)应运而生。命名管道与匿名管道之间的区别在于,命令管道提供了一个路径名与之相关联,从而以文件的形式存在于文件系统中。这样,即使与创建命名管道进程不存在父子关系的进程,只要可以访问该路径,就能够通过彼此该命名管道进行通信。这样就实现了不存在父子进程间的通信。**命名管道的名字存在于文件系统当中,而内容存在于内存当中。下面是以Linux为例创建命名管道的流程。

    1.使用mkfifo命令来创建命名管道

    mkfifo mypipe
    

    2.mypipe就是这个命名管道的名称,在Linux中,一切皆是文件,所以管道也是以文件的形式存在的。这个文件的类型是p,即pipe管道。

    3.向管道中写入数据

    echo "hello" > mypipe //将数据写入管道
                          // 停住了
    

    在输入完回车之后,你会发现就停在这了,这是因为管道里的内容并没有被读取,只有当管道里的内容被读取走后,命令才可以正常退出。

    4.读取数据

    cat < mypipe        //读取管道里的数据
    hello
    

    在执行完cat命令后,数据显示在终端界面上,同时写命令echo也正常退出了。

    综上,对于命名管道,它可以实现不相关的进程间的通信。因为命名管道提前创建了一个类型为管道的设备文件,在进程中只要使用了这个设备文件,就可以相互通信。

    匿名管道 vs 命名管道

    • 管道是特殊类型的文件,在满足FIFO的原则下进行读写,但不能进行定位读写
    • 匿名管道是单向的,只适用于具有父子关系的进程间通信;命令管道以磁盘文件的形式存在,可以实现不相关进程间的通信;
    • 匿名管道阻塞问题:匿名管道创建时直接返回文件描述符,在读写时需要确认对方的存在,否则退出。如果当前进程向匿名管道的一端写入数据,必须确认另一端存在另一进程。如果写入的匿名管道数据超过其限制,写操作将会被阻塞;如果匿名管道中没有数据,读操作将会被阻塞;如果匿名管道的某一端退出,将自动退出;
    • 命名管道的阻塞问题:命名管道在打开时需要确认对方的存在,否则将阻塞,即若是以读方式打开某管道,在此之前,必须有一个进程以写方式打开管道,否则阻塞。另外,还可以以读写(O_RDWR)模式打开命名管道,即当前进程读,当前进程写,不会出现阻塞。

    消息队列

    现有两个进程,AB,使用消息队列进行数据传输时,A进程只需要把数据放到相应的消息队列即可返回;B进程在需要的时候只需要去消息队列中读取相关数据即可。同理,B进程给A进程发送消息也是如此。

    消息队列本质上是保存在内核当中的消息链表,在发送数据时,会被分成一个一个独立的数据单元,称之为消息体。消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型。每个消息体都是固定大小的存储块。如果进程从消息队列中读取了消息,则内核会将该消息从消息队列中移除。

    与管道的不同

    • 生命周期不同:
      • 管道的生命周期随着进程的创建而建立,随着进程的结束而销毁;
      • 消息队列的生命周期与内核有关,除非是重启内核或者是显示地删除一个消息队列,否则消息队列一直存在;
    • 数据不同:
      • 管道传输的是无格式的字节流;
      • 消息队列传输的是消息体,消息体都是固定大小的存储块;

    消息队列缺点

    • 通信不及时;
    • 消息体大小有限制,不适合大数据的传输。在内核中每个消息体都有一个最大长度限制,同时所有队列所包含的消息体总长度也有上限。在Linux内核中,有两个宏定义MSGMAXMSGMNB,以字节为单位,分别定义了一条消息的最大长度和一个队列的最大长度;
    • 涉及到用户态和内核态之间的数据拷贝开销。进程向消息队列写入数据时,会发生从用户态拷贝数据到内核态的过程;同理,进程从消息队列读取消息时,会发生内核态拷贝数据到用户态的过程;

    共享内存

    消息队列的读取过程会发生用户态和内核态之间的消息拷贝过程,使用共享内存这种方式就可以很好地解决这个问题。

    共享内存可以使得多个进程可以直接读写在同一块内存空间中,这是效率最高的进程间通信方式。

    现代操作系统中,对于内存管理,采用的是虚拟内存技术,也就是每个进程都有自己的独立虚拟空间。不同进程的虚拟内存映射到不同的物理内存。因而为了在多个进程间交换信息,内核专门开辟了一块内存区域,不同的进程将其映射到自己的私有地址空间。这样A进程写入的数据,另一个B进程立刻就可以看到,避免了数据的拷贝,大大提高了进程间的通信速度。如下所示:

    信号量

    虽然共享内存大大提高进程间通信的速度,但也带来了新的问题。假如某个时刻多个进程同时对同一个变量进行修改就会产生冲突。为了防止多进程竞争共享资源,需要一些进程间同步机制,使得在某一时刻只有一个进程可以访问共享资源。信号量就是其中之一。

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

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

    • P操作:该操作会把信号量减去1。相减后如果信号量<0,表示该资源已被占用,进程需要被阻塞进行等待;相减后如果信号量>=0,表明资源还可被访问和使用,进程正常执行操作即可;
    • V操作:该操作会把信号量加上1。相加后信号量>=0,表明当前有被阻塞着的进程,将该进程唤醒运行;相加后如果信号量>0,表明当前没有被阻塞的进程;

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

    信号

    在异常情况下,需要使用信号这种方式来通知进程。信号可以在应用进程和内核之间直接交互,内核也可以利用信号来通知用户空间的进程发生了哪些系统事件。

    Linux操作系统中,为了响应各种各样的事件,提供了很多种信号,分别代表不同的含义,如常见的SIGINT信号,表示终止该进程;SIGTSTP信号,表示停止该进程,但还未结束。

    Linux中有两种方式触发信号,一种是通过键盘组合键(如CTRL+C)的方式,一种是命令(如KILL)的方式。

    信号是进程间通信唯一的异步通信机制,因为可以在任何时候发送信号给某一个进程。一旦接收到信号,用户有以下三种处理方式:

    • 执行默认操作;
    • 捕捉信号。可以为信号定义一个信号处理函数。当进程接收信号时,执行相应的信号处理函数;
    • 忽略信号。当接收到信号时,进程可以选择不做处理。但有两个信号无法使用该功能,即SIGKILLSEGSTOP,他们用于在任何时候中断或者结束某一进程。

    Socket

    Socket不仅可以用于跨网络与不同主机间的主机进行通讯,还可以在同主机上进程间通信。

    Linux中创建socket的函数如下:

    int socket(int domain,int type,int protocol)
    

    三个入参意义分别如下:

    • domain:指定协议族,AF_INET 用于IPV4AF_INET用于IPV6AF_LOCAL/AF_UNIX用于本机;
    • type:指定通信类别,SOCK_STREAM表示字节流,对应TCPSOCK_DGRAM表示数据报,对应UDPSOCK_RAW表示原始套接字;
    • protocol:指定通信协议的,但一般写作0,因为前两个参数基本确定了。

    根据创建 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;

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

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

    对于本地字节流 socket,其 socket 类型是 AF_LOCAL SOCK_STREAM

    对于本地数据报 socket,其 socket 类型是 AF_LOCAL SOCK_DGRAM

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

    总结

  • 相关阅读:
    HDU 5583 Kingdom of Black and White 水题
    HDU 5578 Friendship of Frog 水题
    Codeforces Round #190 (Div. 2) E. Ciel the Commander 点分治
    hdu 5594 ZYB's Prime 最大流
    hdu 5593 ZYB's Tree 树形dp
    hdu 5592 ZYB's Game 树状数组
    hdu 5591 ZYB's Game 博弈论
    HDU 5590 ZYB's Biology 水题
    cdoj 1256 昊昊爱运动 预处理/前缀和
    cdoj 1255 斓少摘苹果 贪心
  • 原文地址:https://www.cnblogs.com/reecelin/p/13442983.html
Copyright © 2011-2022 走看看