• 进程间通信的六大方式


    进程间通信

    一、进程间通信的概念

    所谓进程间通信,就是让不同进程在内核的帮助下看到同一份系统资源,以达到数据交互的目的。

    二、进程间通信的方式

    Linux提供了六种进程间通信的机制:管道共享内存信号量、消息队列、信号、套接字。

    1、管道

    管道基于管道文件,本质上是一个存在于内核缓冲区的环形队列,满足先进先出(FIFO)原则,允许两个进程以**“生产者/消费者模型”**进行通信。

    管道分为匿名管道和命名管道,他们拥有几乎相同的底层原理,区别就是创建和使用方式不同

    I.匿名管道

    匿名管道只能由**“有血缘关系”**的进程使用,即拥有共同祖先的进程,常见的就是父子进程

    a.匿名管道的创建

    进程通过系统调用pipe()完成匿名管道的创建:

    int pipe(int pipefd[2])

    成功则返回0,失败则返回-1。

    其中pipefd是一个输出型参数,表示匿名管道文件的两个文件描述符:

    • pipefd[0]具有==“只读”==属性,进程可通过该文件描述符进行读操作
    • pipefd[1]具有==“只写”==属性,进程可通过该文件描述符进行写操作

    b.匿名管道的使用事项

    匿名管道使用文件描述符继承的方式保证进程通信:

    在父进程使用pipe创建匿名管道时,文件描述符表中就会维护两个文件描述符pipefd[0]和pipefd[1],子进程会继承父进程的文件描述符表。

    但是,匿名管道是一种单向通信方式,只能有一个进程读,另一个进程写。因此,负责读的进程最好将写文件描述符关闭,负责写的进程最好将读文件描述符关闭,避免用户的误操作!

    image-20220806125401154

    II.命名管道

    不同于匿名管道仅使用文件描述符进行操作,命名管道是一个具有文件名的真正的文件,拥有独立的inode,因此允许任意进程打开该文件以实现进程间通信。

    注:与匿名管道相同,命名管道文件只在使用时将数据存在内核缓冲区中。

    a.命名管道的创建
    1. 使用mkfifo命令创建命名管道:

    mkfifo pipe_file_name

    1. 使用mkfifo函数创建命名管道:

    int mkfifo(const char *pathname, mode_t mode)

    其中pathname是管道文件的存储路径(包括文件名);mode是该文件的权限(八进制);成功则函数返回0,失败返回-1。


    b.命名管道的使用事项

    在确定好哪个进程读,哪个进程写后,就可以通过对应的方式(O_RDONLY/O_WRONLY)打开管道文件,利用read/write进行读写,完成进程间通信。

    III.管道的特点

    1. 管道采取半双工的通信模式,一个进程只能选择写或者读;
    2. 当管道的读端进程全部退出时,写端进程会收到SIGPIPE信号而退出;
    3. 当管道的写端进程全部退出时,读端进程会最终读到EOF(即read返回值为0);
    4. 管道基于**“生产者消费者模型”**,自带同步机制,因此是并发安全的;
    5. 管道是基于字节流的,进程以字符串格式对管道文件读写;
    6. 管道文件有大小限制,经测试,最大容量为64KB
    7. 如果写端一次写入的数据小于PIPE_BUF(4KB),那么内核将保证写操作的原子性;否则,写操作的原子性将不被保证。这也是为什么ulimit -a命令查看得到的pipe size为4KB而测试的最大容量为64KB的原因。

    IV.管道文件的意义

    • 管道文件能够保证实时通信,而普通文件存在于磁盘,内核并不会立即将缓冲区内容刷新到磁盘,因此无法完成实时通信。

    • 管道文件的读写是并发安全的,相当于内核为用户提供了一种同步通信机制,使用起来比较方便

    2、共享内存

    共享内存是一段由内核维护的物理内存空间。

    由于不同进程拥有独立的虚拟地址空间和页表,因此可以将共享内存通过进程页表映射到不同进程地址空间的共享区。如此,不同进程就可以通过自己的虚拟地址访问相同的物理地址,从而达到通信的目的。

    I.共享内存的创建

    int shmget(key_t key, size_t size, int shmflg)

    • key:用来标识共享内存的键值,不同进程可以通过相同的key获取到同一块共享内存。key可以由用户随意指定,也可以通过ftok函数获取。
    • size:向系统申请的共享内存大小。建议申请PAGE_SIZE(虚拟页大小)的整数倍,因为系统是以页大小的整数倍开辟共享内存的,如果size不是页的整数倍,那么系统为了对齐而额外开辟的空间会导致内存浪费
    • shmflg:标志位。如果以key为标识的共享内存不存在,则可以通过IPC_CREAT创建该内存;如果以key为标识的共享内存已存在,且使用IPC_CREAT | IPC_EXCL时,则shmget函数返回-1;该标志位还可以用来设置共享内存的读写权限(按位或八进制权限)。
    • RetVal:成功则返回一个shmid,之后通过该id使用共享内存;失败则返回-1。

    注:尽管申请的空间会被对齐至PAGE_SIZE的整数倍,但是用户能够使用的大小依然是size


    key_t ftok(const char *pathname, int proj_id)

    pathname是一个路径名,proj_id是一个项目id,它们都可以随意填写。对于相同的pathnameproj_idftok()会返回相同的key值。

    创建共享内存示例
    key_t key = ftok(PATH, PROJ_ID);
    int shmid = shmget(key, 4096, IPC_CREAT | IPC_EXCL | 0644);
    
    • 1
    • 2

    II.共享内存的使用(attach)

    void *shmat(int shmid, const void *shmaddr, int shmflg)

    • shmidshmget函数成功执行返回的id值;
    • shmaddr:可以由用户指定一个共享区的地址作为共享内存映射到本进程地址空间的起始地址,一般设为NULL,表示由系统选取合适的地址;
    • shmflg:指明对共享内存的权限,如SHM_RDONLY表示只读,不过该参数一般设为0,表示使用该内存的创建进程设置的权限
    • RetVal:返回将shmid标识的共享内存映射到共享区的起始地址,用户可以通过该地址读写共享内存,进行通信。

    注:该函数用来实现对共享内存的attach(挂接)。只有挂载到同一个共享内存的进程才能通过这段内存通信。

    使用共享内存示例
    // 写进程wrproc.c每秒追加一个字符x
    void* addr = shmat(id, NULL, 0);
    for (int i = 0; i < shm_size; ++i)
    {
        addr[i] = 'x';
        sleep(1);
    }
    
    // 读进程rdproc.c每秒打印一次共享内存的内容
    void* addr = shmat(id, NULL, 0);
    while (int i = 0; i < shm_size; ++i)
    {
        sleep(1);
        printf("%s\n", addr);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    III.共享内存的分离(de-attach)

    int shmdt(const void *shmaddr)

    shmaddrshmat函数成功执行返回的起始地址,进程调用该函数表示不再使用这段共享内存,即de-attach(分离,取消挂接)。

    如果成功分离则返回0,失败返回-1。

    IV.共享内存的控制(销毁)

    int shmctl(int shmid, int cmd, struct shmid_ds *buf)

    • shmidshmget函数成功执行返回的id值;
    • cmd:控制选项。IPC_RMID,表示销毁这段共享内存;另外还有IPC_STATIPC_SET等选项,这里不关注;
    • buf:指向一个保存着共享内存的模式状态和访问权限的数据结构,当使用IPC_RMID销毁共享内存时,该参数设为NULL即可。

    V.指令查看和销毁共享内存

    使用ipcs可以查看进程间通信的相关信息,其中ipcs -m仅查看共享内存的相关信息。

    img

    ipcrm -m id即可删除指定id的共享内存。

    VI.共享内存的特性

    1. 不同的进程可以通过共享内存的起始虚拟地址直接访问相同的一块物理内存,避免了管道文件需要将数据从用户拷贝到内核的问题,因此该通信方式效率很高
    2. 虽然效率较高,但是共享内存没有同步功能,因此需要用户自己通过**“信号量”**等手段实现并发安全。
    3. 共享内存的生命周期是随内核的。因此一旦不再使用,一定要使用shmctl函数或 ipcrm -m指令销毁它。

    3、信号量

    信号量(semaphore)是为了弥补"多进程竞争共享内存导致数据错乱"而引入的同步和互斥机制。

    信号量本质就是一个计数器,对信号量的操作主要是PV操作

    • P 操作:把信号量减1。
      1. 相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待,且当前阻塞等待的进程数为信号量的绝对值
      2. 相减后如果信号量 >= 0,则表明还有资源可用,进程可正常继续执行。
    • V 操作:把信号量加1。
      1. 相加后如果信号量 <= 0,则表明当前有阻塞中的进程,将该进程唤醒运行。
      2. 相加后如果信号量 > 0,则表明当前没有阻塞中的进程。

    PV操作具有原子性,因此又被称为PV原语

    I.信号量实现同步机制

    进程同步,即让并发的进程按要求有序地执行。

    以两个进程之间的同步为例:

    1. 设置同步信号量S,初始为0。
    2. B进程进行P(S)操作,阻塞等待信号量
    3. A进程进行V(S)操作,此时B进程可以获得信号量,继续向下执行

    如此,就保证了A进程在B进程之前运行。

    II.信号量实现互斥机制

    进程互斥,即保证不同进程不能同时进入一个临界区。

    实现步骤:

    1. 设置互斥信号量S,初始为1。
    2. 进程在进入临界区之前执行P(S)以获得互斥信号量,此时其它进程必须阻塞等待。
    3. 进程在退出临界区之后执行V(S)以释放互斥信号量,此时其它进程可以获取信号量。

    4、消息队列

    消息队列,即MQ(Message Queue),本质是保存在内核中的消息链表

    消息队列的特性

    1. 待发送的数据会被分成一个一个独立的数据单元,也就是消息体(数据块)。由于消息体是发送方和接收方约定好的数据类型,因此每个消息体都是固定大小的,而非像管道那样的字节流。
    2. 消息体的生命周期随内核,只有被读取或操作系统关闭时,它们才会被释放。
    3. 消息队列基于==“生产者消费者模型”==,是并发安全的。
    4. 和管道通信一样,消息队列的数据同样需要进行用户和内核之间的相互拷贝,因此相比于共享内存效率差一些。

    5、信号

    信号是进程间通信机制中唯一的一个异步通信机制

    当A进程向B进程发送信号时,B进程不一定立即处理该信号,而是在CPU切换到用户态之前检查是否有信号,然后进行对应处理。
    信号详解戳这里

    6、套接字

    套接字可用于相同主机上的两个进程或是网络上不同主机上的两个进程进行通信,详见网络编程socket部分。
    套接字详解戳这里

  • 相关阅读:
    golang基础复合类型—结构体
    一文了解ZooKeeper基础命令和应用
    【二分】Pythagorean Triples—CF1487D
    java计算机毕业设计国漫论坛网站源码+mysql数据库+系统+lw文档+部署
    Shell Script概述
    课程设计 | 通讯录管理系统
    【算法|虚拟头节点|链表】移除链表元素
    MySql 数据库【Union、Limit】
    【JS】最大公约数
    云原生:使用HPA和VPA实现集群扩缩容
  • 原文地址:https://blog.csdn.net/Wyf_Fj/article/details/126208559