Bootstrap

进程间通信详解之管道&&消息队列&&共享内存

目录

1.进程间通信简介

2.进程间通信机制

2.1管道和FIFO

2.1.1匿名管道 pipe:(用的多)

2.1.2有名管道 name_pipe(FIFO):

2.1.3管道的读写行为

2.2消息队列

2.3消息队列接口及使用示例

2.3.1ftok函数

2.3.2msgget

2.3.3msgsnd&&msgrcv

2.3.4msgctl

2.4消息队列的特点

2.5消息队列-----实现Client&Server

2.6共享内存


1.进程间通信简介

进程间通信(interprocess communication,简称 IPC)指两个进程之间的通信。系统中的每一个进程都有各自的地址空间,并且相互独立、隔离,每个进程都处于自己的地址空间中。所以同一个进程的不同模块(譬如不同的函数)之间进行通信都是很简单的,譬如使用全局变量等。

两个不同的进程之间要进行通信通常是比较难的,因为这两个进程处于不同的地址空间中;通常情况下,大部分的程序是不要考虑进程间通信的,因为大家所接触绝大部分程序都是单进程程序(可以有多个线程),对于一些复杂、大型的应用程序,则会根据实际需要将其设计成多进程程序,譬如 GUI、服务区应用程序等。

2.进程间通信机制
2.1管道和FIFO

管道的是进程间通信(IPC - InterProcess Communication)的一种方式,管道的本质其实就是内核中的一块内存(或者叫内核缓冲区),这块缓冲区中的数据存储在一个环形队列中,因为管道在内核里边,因此我们不能直接对其进行任何操作。

管道在内核中, 不能直接对其进行操作,我们通过什么方式去读写管道呢?其实管道操作就是文件IO操作,内核中管道的两端分别对应两个文件描述符,通过写端的文件描述符把数据写入到管道中,通过读端的文件描述符将数据从管道中读出来。读写管道的函数就是Linux中的文件IO函数


// 读管道
ssize_t read(int fd, void *buf, size_t count);
// 写管道的函数
ssize_t write(int fd, const void *buf, size_t count);

为什么可以使用管道进行进程间通信?

最后分析一下为什么可以使用管道进行进程间通信,先看一下下面的图片:

在上图中假设父进通过一系列操作可以通过文件描述符表中的文件描述符fd3写管道,通过fd4读管道,然后再通过 fork() 创建出子进程,那么在父进程中被分配的文件描述符 fd3, fd4也就被拷贝到子进程中,子进程通过 fd3可以将数据写入到内核的管道中,通过fd4将数据从管道中读出来

也就是说管道是独立于任何进程的,并且充当了两个进程用于数据通信的载体,只要两个进程能够得到同一个管道的入口和出口(读端和写端的文件描述符),那么他们之间就可以通过管道进行数据的交互。

管道特性:

管道对应的内核缓冲区大小是固定的,默认为4k(也就是队列最大能存储4k数据)

管道分为两部分:读端和写端(队列的两端),数据从写端进入管道,从读端流出管道。

管道中的数据只能读一次,做一次读操作之后数据也就没有了(读数据相当于出队列)。

管道是单工的:数据只能单向流动, 数据从写端流向读端。

对管道的操作(读、写)默认是阻塞的

读管道:管道中没有数据,读操作被阻塞,当管道中有数据之后阻塞才能解除 写管道:管道被写满了,写数据的操作被阻塞,当管道变为不满的状态,写阻塞解除

2.1.1匿名管道 pipe:(用的多)

特点:

拥有上面列举的管道特性

匿名管道只能实现有血缘关系的进程间通信,什么叫有血缘的进程关系呢,比如:父子进程,兄弟进程,爷孙进程,叔侄进程。最后说一下创建匿名管道的函数,函数原型如下:

#include <unistd.h>
int pipe(int pipefd[2]);

参数:传出参数,需要传递一个整形数组的地址,数组大小为 2,也就是说最终会传出两个元素

pipefd[0]: 对应管道读端的文件描述符,通过它可以将数据从管道中读出
pipefd[1]: 对应管道写端的文件描述符,通过它可以将数据写入到管道中

返回值:成功返回 0,失败返回 -1

进程间通信代码:

使用匿名管道只能够实现有血缘关系的进程间通信

需求描述: 在父进程中创建一个子进程, 父子进程分别执行不同的操作:

子进程: 执行一个shell命令 "ps aux", 将命令的结果传递给父进程 父进程: 将子进程命令的结果输出到终端

需求分析:

子进程中执行shell命令相当于启动一个磁盘程序,因此需要使用 execl()/execlp()函数

execlp(“ps”, “ps”, “aux”, NULL)

子进程中执行完shell命令直接就可以在终端输出结果,如果将这些信息传递给父进程呢?

数据传递需要使用管道,子进程需要将数据写入到管道中 将默认输出到终端的数据写入到管道就需要进行输出的重定向,需要使用 dup2()做这件事情

dup2(fd[1], STDOUT_FILENO);

父进程需要读管道,将从管道中读出的数据打印到终端 父进程最后需要释放子进程资源,防止出现僵尸进程

在使用管道进行进程间通信的注意事项:*必须要保证数据在管道中的单向流动。**

为了避免两个进程都读管道,但是可能其中某个进程由于读不到数据而阻塞的情况,我们可以关闭进程中用不到的那一端的文件描述符,这样数据就只能单向的从一端流向另外一端了,如下图,我们关闭了父进程的写端,关闭了子进程的读端

具体实现代码如下:

/*代码描述:
   在父进程中创建一个子进程, 父子进程分别执行不同的操作:
     - 子进程: 执行一个shell命令 "ps aux", 将命令的结果传递给父进程
     - 父进程: 将子进程命令的结果输出到终端*/
​
#include <fcntl.h>
#include <sys/wait.h>
#include <unistd.h>
​
int main()
{
    //创建匿名管道,得到两个文件描述符
    //fd[0].读端,fd[1],写端
    int fd[2];
    int ret = pipe(fd);
    if(ret == -1)
    {
        perror("pipe error");
        exit(-1);
    }
//2.创建子进程->能够操作管道的文件描述符被复制到子进程中
    pid_t pid = fork();
    if(pid == 0)
    {
        //关闭读端
        close(fd[0]);
//3.在子进程中执行execlp(...)
//在子进程中完成输出的重定向,原来输出到终端现在要写管道
//进程打印数据默认输出到终端,终端对应的文件描述符,stdout_fileno
//标准输出,重定向到管道的写端。
        dup2(fd[1], STDOUT_FILENO);
        execlp("ps", "ps", "aux", NULL);
        perror("execlp");
    }
//4.父进程读管道
    else if(pid > 0) {
        //关闭管道的写端
        close(fd[1]);
    //5。父进程打印读到的数据信息
        char buf[4096];
//读管道
//如果管道中没有数据,read会阻塞
//有数据之后,read解除阻塞,直接读数据
//需要循环读数据,管道有容量,写满之后就不写了
//数据被读走之后,继续写管道,那么就需要再继续读数据
        while(1)
        {
            memset(buf, 0, sizeof(buf));
            int len = read(fd[0], buf, sizeof(buf));
            if(len == 0){
                //管道的写端关闭了,如果管道中没有数据,管道读端不会阻塞
                //没有数据直接返回0,如果有数据, 将数据读完之后返回0
                break;
            }
            printf("%s,len = %d\n", buf, len);
        }
        close(fd[0]);
//回收子进程资源
        wait(NULL);
    }
​
    exit(0);
}
2.1.2有名管道 name_pipe(FIFO):

拥有上面列举的管道特性

之所以称之为有名是因为管道在磁盘上有实体文件, 文件类型为p ,有名管道文件大小永远为0,因为有名管道也是将数据存储到内存的缓冲区中,打开这个磁盘上的管道文件就可以得到操作有名管道的文件描述符,通过文件描述符读写管道存储在内核中的数据

有名管道也可以称为 fifo (first in first out),使用有名管道既可以进行有血缘关系的进程间通信,也可以进行没有血缘关系的进程间通信。创建有名管道的方式有两种,一种是通过命令,一种是通过函数。

通过命令:

$ mkfifo 有名管道的名字

通过函数:

#include <sys/types.h>
#incdlude <sys/stst.h>
​
int mkfifo(const char *pathname,mode_t mode);

参数:

pathname: 要创建的有名管道的名字 mode: 文件的操作权限, 和open()的第三个参数一个作用,最终权限: (mode & ~umask)

返回值:创建成功返回 0,失败返回 -1

进程间通信:

不管是有血缘关系还是没有血缘关系,使用有名管道实现进程间通信的方式是相同的,就是在两个进程中分别以读、写的方式打开磁盘上的管道文件,得到用于读管道、写管道的文件描述符,就可以调用对应的read()、write()函数进行读写操作了。

有名管道操作需要通过 open() 操作得到读写管道的文件描述符,如果只是读端打开了或者只是写端打开了,进程会阻塞在这里不会向下执行,直到在另一个进程中将管道的对端打开,当前进程的阻塞也就解除了。所以当发现进程阻塞在了open()函数上不要感到惊讶。·

1)写管道的进程:

/*
    1. 创建有名管道文件 
        mkfifo()
    2. 打开有名管道文件, 打开方式是 o_wronly
        int wfd = open("xx", O_WRONLY);
    3. 调用write函数写文件 ==> 数据被写入管道中
        write(wfd, data, strlen(data));
    4. 写完之后关闭文件描述符
        close(wfd);
*/


#include <fcntl.h>
#include <sys/stat.h>
​
int main()
{
    // 1. 创建有名管道文件
    int ret = mkfifo("./testfifo", 0664);
    if(ret == -1)
    {
        perror("mkfifo");
        exit(0);
    }
    printf("管道文件创建成功...\n");
​
    // 2. 打开管道文件
    // 因为要写管道, 所有打开方式, 应该指定为 O_WRONLY
    // 如果先打开写端, 读端还没有打开, open函数会阻塞, 当读端也打开之后, open解除阻塞
    int wfd = open("./testfifo", O_WRONLY);
    if(wfd == -1)
    {
        perror("open");
        exit(0);
    }
    printf("以只写的方式打开文件成功...\n");
​
    // 3. 循环写管道
    int i = 0;
    while(i<100)
    {
        char buf[1024];
        sprintf(buf, "hello, fifo, 我在写管道...%d\n", i);
        write(wfd, buf, strlen(buf));
        i++;
        sleep(1);
    }
    close(wfd);
​
    return 0;
}

2)读管道的进程

/*
    1. 这两个进程需要操作相同的管道文件
    2. 打开有名管道文件, 打开方式是 o_rdonly
        int rfd = open("xx", O_RDONLY);
    3. 调用read函数读文件 ==> 读管道中的数据
        char buf[4096];
        read(rfd, buf, sizeof(buf));
    4. 读完之后关闭文件描述符
        close(rfd);
*/


#include <fcntl.h>
#include <sys/stat.h>
​
int main()
{
    // 1. 打开管道文件
    // 因为要read管道, so打开方式, 应该指定为 O_RDONLY
    // 如果只打开了读端, 写端还没有打开, open阻塞, 当写端被打开, 阻塞就解除了
    int rfd = open("./testfifo", O_RDONLY);
    if(rfd == -1)
    {
        perror("open");
        exit(0);
    }
    printf("以只读的方式打开文件成功...\n");
​
    // 2. 循环读管道
    while(1)
    {
        char buf[1024];
        memset(buf, 0, sizeof(buf));
        // 读是阻塞的, 如果管道中没有数据, read自动阻塞
        // 有数据解除阻塞, 继续读数据
        int len = read(rfd, buf, sizeof(buf));
        printf("读出的数据: %s\n", buf);
        if(len == 0)
        {
            // 写端关闭了, read解除阻塞返回0
            printf("管道的写端已经关闭, 拜拜...\n");
            break;
        }
​
    }
    close(rfd);
​
    return 0;
}
2.1.3管道的读写行为

关于管道不管是有名的还是匿名的,在进行读写的时候,它们表现出的行为是一致的,下面是对其读写行为的总结:

读管道,需要根据写端的状态进行分析: 写端没有关闭 (操作管道写端的文件描述符没有被关闭) 如果管道中没有数据 ==> 读阻塞, 如果管道中被写入了数据, 阻塞解除 如果管道中有数据 ==> 不阻塞,管道中的数据被读完了, 再继续读管道还会阻塞 写端已经关闭了 (没有可用的文件描述符可以写管道了) 管道中没有数据 ==> 读端解除阻塞, read函数返回0 管道中有数据 ==> read先将数据读出, 数据读完之后返回0, 不会阻塞了 写管道,需要根据读端的状态进行分析: 读端没有关闭 如果管道有存储的空间, 一直写数据 如果管道写满了, 写操作就阻塞, 当读端将管道数据读走了, 解除阻塞继续写 读端关闭了,管道破裂(异常), 进程直接退出

2.2消息队列

管道、共享内存中,不可以实现两个通信进程向同一个共享资源进行写入或读取,上述方式主要用于单向通信,如果通过一些控制策略,则可以实现半双工通信。

为了实现两个进程互相收发消息时使用同一资源,我们就有了消息队列。消息队列是一种在不同进程或者不同系统之间进行通信的机制,它是一种存放消息的容器发送方向队列中发送消息,接收方从队列中接收消息。下面是消息队列的大致原理: ————————————————

创建消息队列:首先需要创建一个消息队列,通常由消息队列服务程序负责管理,进程可以通过调用相关API来进行消息队列的创建。

发送消息:发送方将待发送的消息写入到消息队列中,消息包括消息类型和消息。发送方通过指定消息队列的标识符和消息类型来发送消息。

接收消息:接收方从消息队列中获取消息,根据消息类型进行消息过滤或选择性接收。接收方通过指定消息队列的标识符和消息类型来接收消息。 ————————————————

在这里插入图片描述

系统中可能存在大量的消息队列,内核需要使用如下数据结构对消息队列进行管理

struct msqid_ds {
    struct ipc_perm msg_perm;     /* Ownership and permissions */
    time_t          msg_stime;    /* Time of last msgsnd(2) */
    time_t          msg_rtime;    /* Time of last msgrcv(2) */
    time_t          msg_ctime;    /* Time of last change */
    unsigned long   __msg_cbytes; /* Current number of bytes in
                                     queue (nonstandard) */
    msgqnum_t       msg_qnum;     /* Current number of messages
                                     in queue */
    msglen_t        msg_qbytes;   /* Maximum number of bytes
                                     allowed in queue */
    pid_t           msg_lspid;    /* PID of last msgsnd(2) */
    pid_t           msg_lrpid;    /* PID of last msgrcv(2) */
};
​
struct ipc_perm {
    key_t          __key;       /* Key supplied to msgget(2) */
    uid_t          uid;         /* Effective UID of owner */
    gid_t          gid;         /* Effective GID of owner */
    uid_t          cuid;        /* Effective UID of creator */
    gid_t          cgid;        /* Effective GID of creator */
    unsigned short mode;        /* Permissions */
    unsigned short __seq;       /* Sequence number */
};
2.3消息队列接口及使用示例
2.3.1ftok函数

作用是获取唯一的key值

在命名管道中,通过文件路径的唯一性,以保证多个通信进程能够看到同一个共享资源。在共享内存也需要保证多个通信进程看到同一份共享资源,即使用key来保证资源的唯一性。key需要使用ftok函数自动生成

函数模板:

#include <sys/types.h>
#include <sys/ipc.h>
​
key_t ftok(const char *pathname,int proj_id);

第一个参数pathname是值文件路径

第二个参数是项目id(用户自己指定,没有明确要求)。

返回计算得到的key值,但如果传入的路径不存在,则会生成失败,返回-1。

如果多个进程需要通信,只需要传入同一个文件目录及项目id就能确定唯一的key值,即能够找到同一份共享内存资源。

★ps:key

1.key是一个数字,这个数字是多少并不重要,关键是它必须在内核中具有唯一性,就能让不同的进程进行唯一性标识; 2.第一个进程可以通过key创建共享内存,第二个及以后的进程只要拿着同一个key值,就可以和第一个进程看到同一个共享内存; 3.对于一个已经创建好的共享内存,key在哪呢?答案是:key在共享内存的内核数据结构中(共享内存的描述对象); 4.为了保证多个进程能够看到同一个共享内存,需要约定唯一的pathname和项目id,避免找到不是同一个共享内存; 5.key和路径一样,具有唯一性

ftok创建key值的实例代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
​
int main()
{
    key_t key1 = ftok("/home/xiaoming", 5);
    key_t key2 = ftok("/home/xiaoming", 5);
    key_t key3 = ftok("/home/xiaoming/gitcode", 5);
    key_t key4 = ftok("/home/xiaoming/", 666);
    printf("key1 = %u\n", key1);
    printf("key2 = %u\n", key2);
    printf("key3 = %u\n", key3);
    printf("key4 = %u\n", key4);
    return 0;
}
2.3.2msgget

该函数用于创建或者获取消息队列

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
​
int msgget(key_t key,int msgflg);

key:唯一标识符,用于查找或创建消息队列。

需要使用ftok接口生成,函数具体用法如2.3.1。

第二个参数可以选择一下几种常用选项

msgflg取值描述
IPC_CRAET创建共享内存时,如果共享内存已经已经存在,获取已经存在的共享内存;不存在则创建并返回
IPC_EXCL需要与IPC_CREAT组合使用,单独使用没有意义。如果带创建的共享内存存在,则出错返回;如果不存在,则创建并返回对应的共享内存

作为读写资源的一种,消息队列也有自己的读写权限,可以通过上述第二个参数与运算对应的权限即可,如第二个参数可以填写IPC_CREAT | 0666(其中0666就表示设置的权限)。

创建实例:

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
​
int main()
{
    key_t key = ftok("/home/xxl/LinuxC/4.inter_process_communication", 777);
    int msgid = msgget(key, IPC_CREAT | 0666);
    return 0;
}

创建后使用“ipcs -q”查看系统中存在的消息队列

key是我们创建消息队列时传入的,

msqid是系统自动生成的标识消息队列唯一性的符号,

owner是该消息队列的所有者,

perms是该消息队列的权限,

used_types表示消息队列已经使用的空间大小,

messages表示消息队列中的消息数量。

2.3.3msgsnd&&msgrcv

作用:

msgsnd:发送消息到消息队列

msgrcv:从消息队列接收消息

函数模型如下

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
​
int msgsnd(int msqid,const void *msgp,size_t msgsz,int msgflg);
​
ssize_t msgrcv(int msqid,void *msgp,size_t msgsz.long msgtyp,int msgflg);

在msgsnd和msgrcv中,第二个参数需要传入一个形式如下所示的结构体,这个结构体需要用户自己定义(msgsnd用该结构体定义要发送的数据,msgrcv用该结构体接收消息队列中的数据)

The msgp argument is a pointer to caller-defined structure of the following general form:
​
struct msgbuf {
    long mtype;       /* message type, must be > 0 */
    char mtext[1];    /* message data */
};

mtext表示要发送的数据,因而mtext的数组大小由用户自己指定

mtype用于标识数据,例如:A进程只读取mtype为1的数据,B进程只读取mtype为2的数据,即mtype标识这个数据要发送给哪些进程。

函数参数:

msgsnd的第一个参数表示向哪一个消息队列中发送,需要传入能够标识队列唯一性的msgid

第三个参数msgsz用于标识msgbuf中的mtext的大小,即数据的大小

第四个参数msgflg的取值及含义如下表所示↓↓↓

msgflg取值含义
0默认行为,阻塞等待消息队列,直到消息队列中有空间
IPC_NOWAIT如果队列已满,不会阻塞等待,而是会返回错误码EAGAIN(设置在errno中)

msgrcv的第一个参数也是用于表示向哪一个消息队列中发送,需要传入具有唯一性的msgid;

第三个参数msgsz表示用于接收数据的msgbuf中的mtext的大小,即接收缓冲区的大小(如果接收缓冲区的大小小于发送过来的数据,则会出错);

第四个参数msgtyp表示只接收消息队列中msgbuf的mtype字段为msgtyp的数据

第五个参数msgflg的取值及含义如下表所示↓↓↓

示例代码:

msgsnd.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdlib.h>
#define MAX_SIZE 1024
​
struct msgbuf
{
    long mtype;
    char mtext[MAX_SIZE];
};
​
int main()
{
//设置项目id的时候,不要和权限值重复,不然会出问题
  key_t key = ftok("/home/xxl/LinuxC/4.inter_process_communication", 777);
  //此处设置权限值,不是随便设置的
  //一般设置0666,表示所有用户对该队列都有读写权限
  int msgid = msgget(key, IPC_CREAT | 0666);
  if(msgid == -1) {
      perror("msgget error");
      exit(-1);
  }
  char *sendmsg = "just for test";
  struct msgbuf buffer;
  buffer.mtype = 1;
  snprintf(buffer.mtext, MAX_SIZE, "%s", sendmsg);
​
  if(msgsnd(msgid, &buffer, MAX_SIZE, 0) == -1)
  {
      perror("msgsnd error");
      exit(-1);
  }
  printf("Message sent: %s\n", buffer.mtext);
  return 0;
}

msgrcv.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
​
#define MAX_SIZE 1024
struct msgbuf
{
    long mtype;
    char mtext[MAX_SIZE];
};
int main(){
    key_t key = ftok("/home/xxl/LinuxC/4.inter_process_communication", 777);
    int msgid = msgget(key, 0666);
    if(msgid == -1) {
        perror("msgget error");
        exit(-1);
    }
    struct msgbuf buffer;
​
    if(msgrcv(msgid,&buffer,MAX_SIZE,1,0) == -1)
    {
        perror("msgrcv error");
        exit(-1);
    }
    printf("received message:%s\n", buffer.mtext);
​
    return 0;
}

代码测试结果:

2.3.4msgctl

作用:用于控制消息队列

示例代码如下:

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
​
int msgctl(int msgid,int cmd,struct msqid_ds *buf);

msgctl的第一个参数表示对哪一个消息队列进行操作

第二个参数取值和描述如下表:

cmd取值含义
IPC_STAT获取消息队列的状态
IPC_SET设置消息队列的属性
IPC_RMID从系统中移除消息队列

第三个参数维护了一个msgid_ds的结构,如果需要设置或获取某个新消息队列,需要创建一个struct msqid_ds类型的变量,将其传入第三个参数中。

1)代码1:演示如何获取消息队列的状态

//获取消息队列状态
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
​
int main()
{
  key_t key = ftok("/home/xxl/LinuxC/4.inter_process_communication", 888);
  int msgid = msgget(key, IPC_CREAT | 0666);
  if(msgid == -1) {
      perror("msgget error");
      exit(-1);
  }
​
  sleep(2);
​
  struct msqid_ds mqs;
  memset(&mqs, 0, sizeof(mqs));
​
  msgctl(msgid, IPC_STAT, &mqs);
//num:它表示当前队列中的消息数量
//bytes:表示的是消息队列能够容纳的最大字节数,而不是队列当前的占用情况
  printf("num:   %d\n", mqs.msg_qnum);
  printf("bytes: %d\n", mqs.msg_qbytes);
​
  return 0;
}

2)代码2:演示如何设置消息队列的状态

查看消息队列shell指令:

ipcs -q

//设置消息队列状态
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdlib.h>
int main()
{
  key_t key = ftok("/home/xxl/LinuxC/4.inter_process_communication", 888);
  int msgid = msgget(key, IPC_CREAT | 0666);
  if(msgid == -1) {
      perror("msgget error");
      exit(-1);
  }
​
  sleep(2);
​
  struct msqid_ds mqs;
  memset(&mqs, 0, sizeof(mqs));
​
  mqs.msg_perm.mode = 0444;
  msgctl(msgid, IPC_SET, &mqs);
​
  return 0;
}

3)代码3:演示如何删除消息队列的状态

2.4消息队列的特点

异步通信:发送者和接收者之间的通信是异步的,发送者可以继续执行其他操作而不用等待接收者处理消息。

独立性:消息队列是独立于发送者和接收者的,发送者和接收者之间可以是不同的进程,甚至可以在不同的计算机上。

顺序性保证:消息队列通常是一种先进先出(FIFO)的数据结构,保证消息的顺序性。

灵活性:消息队列可以传递多种类型的数据,而且消息的大小通常也比较灵活。

缓冲能力:消息队列可以作为缓冲区,当接收者暂时无法处理消息时,消息可以先存储在消息队列中,等待接收者处理。

可靠性:消息队列通常会提供一定的机制来确保消息的可靠性,例如消息确认和消息重发机制。

支持半双工通信:相比于匿名/命名管道及共享内存的单向通信特征,消息队列具备半双工通信的能力。

2.5消息队列-----实现Client&Server

由于struct msgbuf中的mtype可以标识向那个进程发送消息。假设Server接收mtype为1的数据

1)Com.hpp

#pragma once
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
​
const std::string PATH = "/home/xiaoming";
const int PROJ_ID = 999;
const int BUFF_SIZE = 1024;
​
enum
{
    MSG_CREAT_ERR = 1,
    MSG_GET_ERR,
    MSG_DELETE_ERR
};
​
int CreateMsg()
{
    key_t key = ftok(PATH.c_str(), PROJ_ID);
    int msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
    if (msgid < 0)
    {
        perror("msg create error");
        exit(MSG_CREAT_ERR);
    }
    return msgid;
}
​
int GetMsg()
{
    key_t key = ftok(PATH.c_str(), PROJ_ID);
    int msgid = msgget(key, IPC_CREAT | 0666);
    if (msgid < 0)
    {
        perror("msg get error");
        exit(MSG_GET_ERR);
    }
    return msgid;
}

2)Server.cc

#include "Com.hpp"
​
int main()
{
    struct msgbuf buffer;
    int msgid = CreateMsg();
    while (true)
    {
        msgrcv(msgid, &buffer, BUFF_SIZE, 1, 0);
        std::cout << "Client say@ " << buffer.mtext << std::endl;
    }
    return 0;
}

3)Client.cc

#include "Com.hpp"
​
int main()
{
    int msgid = GetMsg();
    struct msgbuf buffer;
    buffer.mtype = 1;
    while (true)
    {
        std::cout << "Says # ";
        std::string s;
        std::getline(std::cin, s);
        strcpy(buffer.mtext, s.c_str());
        msgsnd(msgid, &buffer, BUFF_SIZE, 0);
    }
    return 0;
}
2.6共享内存

此处只是介绍基本概念,具体参考文章链接如下

原文链接:【从浅学到熟知Linux】进程间通信之共享内存(含共享内存挂接原理、ftok/shmget/shmat/shmdt/shmctl详解、共享内存实现Client&Server)_linux ftok-CSDN博客

共享内存方式,就是让两个通信进程直接访问同一块内存空间,直接对该空间执行读取或写入,避免了拷贝。

共享的建立过程是:

①在内存中申请一块空间;

②将该内存挂接(映射)到通信进程的进程地址空间的共享区

当A进程需要与B进程通信时,只需要通过共享区虚拟地址映射到物理地址,对该物理地址直接进行写入;

而B进程则是通过象取虚拟地址映射到物理地址,从该物理地址直接进行读取。这样就可以避免2次拷贝。

在这里插入图片描述

共享内存的提供者必然是操作系统。

由于通信的进程可能很多,申请的共享内存也可能很多,故操作系统需要对各个共享内存先描述再组织。

即操作系统需要创建对应的内核数据结构,对系统中的共享内存的各个属性进行描述,再通过管理这些内核数据结构,实现对共享内存的管理。

由此,我们可以得出结论:共享内存=共享内存块+对应的共享内存的内核数据结构

;