进程通信——消息队列

https://zhuanlan.zhihu.com/p/268389190

1.概念

IPC的意思是“ 进程间通信机制”,Linux内核有三种常用IPC对象可以拿来做进程间通信–消息队列,共享内存,信号量。这三种IPC对象在Linux内核中都以链表的形式存储,它们都有特定的ID来标识(消息队列标识符msqid、共享内存标识符shmid,信号量标识符semid)。

每个 IPC 对象都对应一个ipc_perm结构体, ipc_perm结构体中保留了该ipc对象对应的一些权限信息:

struct ipc_perm {
  key_t __key;                 /* Key supplied to semget(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 */
  unsignedshort mode;          /* Read/write permission.*/
  unsignedshort __seq;         /* Sequence number */
};

1.0 IPC

1.1 什么是消息队列

消息队列称为报文队列也叫做信箱,是linux的一种通信机制这种通信机制传递的数据具有某种结构,而不是简单的字节流。

  • 消息队列的本质是内核提供的链表,内核基于这个链表,实现了一个数据结构。内核又基于此提供了从一个进程向另一个进程发送一块数据的方法
  • 向消息队列写数据,实际上是向这个数据结构中插入一个新节点;从消息队列读数据,实际上是从这个数据结构中删除一个节点。
  • 消息队列也有管道一样的不足,就是每个数据块的最大长度是有限的,系统上全体队列的最大值也有一个上限。

1.2 消息队列工作机制

在这里插入图片描述

1.3 消息队列与其他进程通信机制的比较:

与信号量相比,消息队列可以承载更多的通信数据。

与管道的默认接收相比,消息队列可以让接收进程有选择地接收通信数据,还可以设置接收的优先级。当使用消息队列的进程终止时,消息队列不会自动删除。但所有引用管道的进程终止时,管道会自动删除。

与共享内存相比,共享内存的速度更快,因为对共享内存的处理不经过内核调用,而消息队列需要经过内核调用。但是在多核系统上,为了避免产生高速缓存一致性问题,更推荐使用消息队列。


2.使用System-V版

2.1 用户消息缓冲区

无论是发送进程还是接收进程,都需要在进程空间中用消息缓冲区来暂存消息,该消息缓冲区的结构定义如下

struct msgbuf{
long mtype;
char mtext[1];
};

  • 可以通过mtype来区分数据类型,通过判断mtype,是否为需要接收的数据
  • mtext[1]为存放消息的正文数组,可以根据消息的大小定义该数组的长度

2.2 创建消息队列msgget

函数原型如下

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg);

参数
key: 某个消息队列的名字,通过宏值定义,或者使用ftok函数生成。
msgflg:由九个权限标志构成,用法和创建文件时使用的mode模式标志是一样的,这里举两个来说明

IPC_CREAT
	如果消息队列对象不存在,则创建之,否则则进行打开操作
IPC_EXCL
	如果消息对象不存在则创建之,否则产生一个错误并返回

返回值:
成功返回一个非负整数,即该消息队列的标识码;
失败则返回-1


2.3 添加消息到消息队列msgsend

函数原型如下:

int  msgsnd(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);

参数
msgid: 由msgget函数返回的消息队列标识码
msg_ptr:是一个指针,指针指向准备发送的消息缓冲区
msg_sz:是msg_ptr指向的消息长度,消息缓冲区结构体中mtext的大小,不包括数据的类型
msgflg:控制着当前消息队列满或到达系统上限时将要发生的事情
如:
msgflg = IPC_NOWAIT 表示队列满不等待,返回EAGAIN错误

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


2.4 从消息队列读取消息、

函数原型如下:

int  msgrcv(int msgid, void *msg_ptr, size_t msgsz,
		 long int msgtype, int msgflg);

参数:
msgid: 由msgget函数返回的消息队列标识码
msg_ptr:是一个指针,指针指向准备接收的消息,
msgsz:是msg_ptr指向的消息长度,消息缓冲区结构体中mtext的大小,不包括数据的类型
msgtype:它可以实现接收优先级的简单形式
msgtype=0返回队列第一条信息
msgtype>0返回队列第一条类型等于msgtype的消息 
msgtype<0返回队列第一条类型小于等于msgtype绝对值的消息
msgflg:控制着队列中没有相应类型的消息可供接收时将要发生的事
msgflg=IPC_NOWAIT,队列没有可读消息不等待,返回ENOMSG错误。
msgflg=MSG_NOERROR,消息大小超过msgsz时被截断

注意
msgtype>0且msgflg=MSC_EXCEPT,接收类型不等于msgtype的第一条消息

返回值:
成功返回实际放到接收缓冲区里去的字符个数
失败,则返回-1


2.5 消息队列的控制函数msgctrl

函数原型:

int  msgctl(int msqid, int command, strcut msqid_ds *buf);

参数:

  • msqid: 由msgget函数返回的消息队列标识码
  • command:是将要采取的动作,(有三个可取值)分别如下:
命令说明
IPC_STAT获取该消息队列的信息,获取到的信息会储存在结构体msqid_ds类型的buf中
IPC_SET在进程有足够权限时,把消息队列的属性值设置为msqid_ds数据结构中给出的值
IPC_RMID删除消息队列
IPC_INFO获得系统对消息队列做的限制

注意若选择删除队列,第三个参数传NULL;
返回值:
如果操作成功,返回“0”;如果失败,则返回“-1”


2.6 msqid_ds

常用于SYSTEM_V版的函数,这个结构体用于控制一个消息队列

struct msqid_ds{
    struct ipc_perm msg_perm;
    time_t msg_stime;               //发送到队列的最后一个消息的时间戳
    time_t msg_rtime;               //从队列中获取的最后一个消息的时间戳
    time_t msg_ctime;               //对队列进行最后一次变动的时间戳
    unsigned long __msg_cbytes;     //在队列上所驻留的字节总数
    msgqnum_t msg_qnum;             //当前队列中消息的数量
    msglen_t msg_qbytes;            //当前队列允许的最大bytes数
    pid_t msg_lspid;                //发送最后一个消息的进程PID
    pid_t msg_lrpid;                //接收最后一个消息的进程PID
};

3. 使用POSIX版本

3.1 mq_open

创建或打开消息队列

#include <mqueue.h>
typedef int mqd_t;
mqd_t mq_open(const char *name, int oflag); 
mqd_t mq_open(const char *name, int oflag, mode_t mode, struct mq_attr *attr);

//链接librt.so动态库
gcc -a.cpp -o a.exe -lrt

参数:
name:消息队列名字
oflag:指定打开消息队列的方式,包括读写方式,是否创建、是否非阻塞等
常见取值:

  • O_RDONLY–以只接收消息的形式打开消息队列
  • O_WRONLY–以只发送消息的形式打开消息队列
  • O_RDWR–以可接收可发送的形式打开消息队列
  • 还有O_CREAT, O_EXCL, O_NONBLOCK

mode:指定了新创建的消息队列的权限模式,类似于open函数中的mode参数,只有在oflag包含O_CREAT时, 才需要指定mode参数,包含:

  • S_IRUSR(4):用户读取权限
  • S_IWUSR(2):用户写入权限
  • S_IXUSR(1):用户执行权限
  • S_IWGGRP(4):组写入权限
  • S_IWOTH(1):其他用户写入权限

attr:指定新队列的属性,若attr为NULL,则使用默认属性

返回:
若成功,返回消息队列描述符mqdes。若失败,返回-1。


3.2 mq_send

向消息队列发送消息

#include <mqueue.h>
int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned msg_prio);

参数:
–msg_ptr: 指向需要发送的消息的指针
–msg_len: 消息长度
–msg_prio: 消息的优先级

返回:
若成功,返回0。若失败,返回-1

此处的优先级可以作为不同进程识别消息类型的依据


3.3 mq_receive

从消息队列接受消息

#include <mqueue.h>
int mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned *msg_prio);

参数:
–msg_ptr: 指向接收到的消息的指针
–msg_len: 消息长度
–msg_prio: 消息的优先级
返回值:
若成功,返回接收到消息的字节数。若失败,返回-1

如果消息队列已满,mq_send()函数将阻塞,直到队列有可用空间或该调用被信号打断。
如果消息队列为空,> mq_receive()函数将阻塞,直到队列有新的消息被放进来。
如果创建消息队列时,oflag传入了O_NONBLOCK,则两个函数不会阻塞,而是会立马报错返回。

如果有多个消费进程,如何保证自己能获取到想要的


3.4 mq_notify

mq_notify用于注册消息队列的通知,当消息队列收到新的消息时,能够及时向指定的线程发送信号。

#include <mqueue.h>
int mq_notify(mqd_t mqdes, const struct sigevent *sevp);

参数说明
mqdes:消息队列描述符,由mq_open或mq_timedsend等函数返回。
sevp:指向sigevent结构体的指针,该结构体定义了如何处理信号。sigevent
返回值
如果成功,mq_notify返回0。
如果失败,mq_notify返回-1,并设置errno以指示错误。


3.5 mq_close

关闭消息队列的访问,该消息队列仍然存在,其他进程仍然可以访问

#include <mqueue.h>
int mq_close(mqd_t mqdes);

参数说明
–mqdes:要关闭消息队列的描述符
返回值
若成功,返回0。若失败,返回-1。


3.6 mq_unlink

销毁消息队列,任何在使用该消息队列的进程都将失败。

#include <mqueue.h>
int mq_unlink(const char *name);

参数说明
–mqdes:要关闭消息队列的名字
返回值
若成功,返回0。若失败,返回-1。


3.7 mq_getattr/mq_setattr

#include <mqueue.h>
int mq_setattr(mqd_t mqdes, const struct mq_attr *newattr, struct mq_attr *oldattr);
int mq_getattr(mqd_t mqdes, struct mq_attr *attr);

返回值
若成功,返回0。若失败,返回-1


4. 代码示例

生产者

//A.cpp
// g++ A.cpp -o A.exe -lrt
#include <mqueue.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

typedef int mqd_t;

int main()
{
    // 设置消息队列属性
    struct mq_attr attr;
    attr.mq_flags = 0;
    attr.mq_maxmsg = 10;
    attr.mq_msgsize = 100;
    attr.mq_curmsgs = 0;
    mq_unlink("/test");//先删除
    mqd_t mqd = mq_open("/test", O_CREAT|O_RDWR, S_IRWXU, &attr);
    if(mqd == -1)
    {
        printf("mq_open fail! Error: %s\n", strerror(errno));
        return -1;

    }

    for(int i = 0; i < 5; i++)
    {
        char buf[] = "HELLO";
        if(-1 == mq_send(mqd, buf, sizeof(buf), i))
        {
            printf("mq_send() fail!\n");
        }
        else
        {
            printf("mq_send() succ!\n");
        }
    }

    sleep(10);
    // 发送最后一条消息
    char buf[] = "last notice";
    mq_send(mqd, buf, sizeof(buf), 0);

    if(-1 == mq_close(mqd))
    {
        return -1;
    }
    sleep(10);
    if(-1 == mq_unlink("/test"))
    {
        return -1;
    }
    return 0;
}

使用者

//B.cpp
#include <mqueue.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>

typedef int mqd_t;

int main()
{
    struct mq_attr attr;
    attr.mq_flags = 0;
    attr.mq_maxmsg = 10;
    attr.mq_msgsize = 100;
    attr.mq_curmsgs = 0;
    mqd_t mqd = mq_open("/test", O_RDWR, S_IRWXU, &attr);
    if(mqd == -1)
    {
        printf("mq_open fail! Error: %s\n", strerror(errno));
        return -1;
    }
    char buf[128];

    for(int i = 0; i < 6; i++)
    {
        memset(buf, 0, sizeof(buf));
        if(-1 == mq_receive(mqd, buf, sizeof(buf), NULL))
        {
            printf("mq_receive fail! Error:%s\n", strerror(errno));
        }
        else
        {
            printf("mq_receive succ:%s\n", buf);
        }
    }

    if(-1 == mq_close(mqd))
    {
        return -1;
    }
    return 0;
}

5. SYSTEM_V和POSIX的差异

‌System V和POSIX的主要差异在于它们的设计目标、实现方式、性能、可靠性、以及应用场景。‌

  1. 设计目标和实现方式‌:
  • System V IPC(Inter-Process Communication,进程间通信)主要由贝尔实验室和BSD开发,侧重于进程之间的通信手段的改进,基于内核实现。它提供了信号量、消息队列和共享内存等机制,用于在进程间进行通信和同步‌1。
  • POSIX(Portable Operating System Interface,可移植操作系统接口)则是由IEEE制定的标准,旨在为运行在不同操作系统上的软件提供统一的接口。POSIX的实现由不同的操作系统内核开发人员完成,旨在提高系统的可移植性‌1。
  1. 性能和可靠性‌:
  • 在性能方面,POSIX在无竞争条件下不会陷入内核,而System V无论何时都要陷入内核,因此在性能上稍逊一筹‌1。
  • 在可靠性方面,POSIX的sem_wait函数成功获取信号量后,如果进程意外终止,将无法释放信号量。而System V提供了SEM_UNDO选项来解决这个问题,因此后者更加可靠‌1。
  1. 应用场景‌:
  • System V的应用更为广泛,尤其是在一些较旧的操作系统或未实现POSIX标准的系统中。然而,考虑到可移植性,POSIX成为了一个趋势,尤其在需要跨平台兼容的应用中更为常见‌1。
  • 在进程间通信和同步方面,POSIX的使用似乎更为普遍,尽管在共享内存方面,System V仍然占据主流‌12。

总的来说,System V和POSIX各有优势和适用场景。System V提供了更广泛的兼容性和对旧系统的支持,而POSIX则以其标准化的接口和高可移植性受到青睐。选择使用哪种机制取决于具体的应用需求和目标平台的特性‌12。

6. 遇到的错误

遇到错误可使用strerror打印

#include <errno.h>
#include <string.h>

5.1 Message too long

可能原因

  1. 接收缓存buf比实际消息队列内容短
  2. 接收缓存buf比消息队列属性中的mq_msgsize小,即使缓冲区长度>实际消息长度
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值