本文已参与[新人创作礼]活动,一起开启掘金创作之路。
(欢迎大家关注我的微信公众号——控制工程研习,上面会分享很多我学习过程中总结的笔记。)
在完成一个工作任务时,我需要将一个模块单独作为一个枢纽性质的模块,即其他模块(可能有很多个)将数据/消息传送给它,而它集中进行处理,例如集中将这些数据存入文件中。之前使用ROS做过类似的模块,因为ROS有专业的消息传送方式,即rostopic的方式,而现在我希望在基于Linux系统的基础上设计一个更为通用的方法。
最后网罗到的方法就是进行进程间通信,而进程间通信的方式有很多,有简单地通过往指定的文件进行读写的方式,也有这篇文章介绍的使用消息队列的方式,因为我觉得使用有封装好的库的方式比起自己写会更加安全和稳定。故该推文介绍C++使用消息队列的相关知识。
1. 消息队列的特点
(1)消息队列是面向记录,其中的消息具有特定的格式以及特定的优先级;
(2)消息队列独立于发送和接收进程,进程终止时,消息队列机器内容并不会被删除;
(3)消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取;
(4)有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息;
(5)消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
2. 相关函数
本节C++消息队列的常用的四个函数,这些函数的输入参数是重点,我也是参考了很多网站进行的总结:
(1)msgget()
该函数用于得到消息队列标识符或创建一个消息队列对象。
所需头文件 | #include <sys/types.h>#include <sys/ipc.h>#include <sys/msg.h> | ||
---|---|---|---|
函数说明 | 得到消息队列标识符或创建一个消息队列对象并返回消息队列标识符 | ||
函数原型 | int msgget(key_t key, int msgflg) | ||
函数传入值 | key | 0(IPC_PRIVATE):会建立新的消息队列大于0的32位整数:视参数msgflg来确定操作。通常要求此值来源于ftok返回的IPC键值(不同的进程规定相同的key之后,就可以通过相同的消息队列标识符进行指定消息队列的读写了) | |
msgflg | 0:取消息队列标识符,若不存在则函数会报错;IPC_CREAT:当msgflg&IPC_CREAT为真时,如果内核中不存在键值与key相等的消息队列,则新建一个消息队列;如果存在这样的消息队列,返回此消息队列的标识符;**IPC_CREAT | IPC_EXCL**:如果内核中不存在键值与key相等的消息队列,则新建一个消息队列;如果存在这样的消息队列则报错;还可以填入数字:如0644:这表示了所创建队列的属性:-rw-r--r--(110100100); | |
函数返回值 | 成功:返回消息队列的标识符出错:-1,错误原因存于error中 | ||
附加说明 | 上述msgflg参数为模式标志参数,使用时需要与IPC对象存取权限(如0600)进行 | 运算来确定消息队列的存取权限 | |
错误代码 | EACCES:指定的消息队列已存在,但调用进程没有权限访问它EEXIST:key指定的消息队列已存在,而msgflg中同时指定IPC_CREAT和IPC_EXCL标志ENOENT:key指定的消息队列不存在同时msgflg中没有指定IPC_CREAT标志ENOMEM:需要建立消息队列,但内存不足ENOSPC:需要建立消息队列,但已达到系统的限制 |
**
**
(2)msgsnd()
该函数是将消息写入消息队列
所需头文件 | #include <sys/types.h>#include <sys/ipc.h>#include <sys/msg.h> | |
---|---|---|
函数说明 | 将msgp消息写入到标识符为msqid的消息队列 | |
函数原型 | int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg) | |
函数传入值 | msqid | 消息队列标识符:一般由msgget()函数创建所得 |
msgp | 发送给队列的消息。msgp可以是任何类型的结构体,但第一个字段必须为long类型,即表明此发送消息的类型,msgrcv根据此接收消息。msgp定义的参照格式如下:struct s_msg{ /msgp定义的参照格式/ long type; /* 必须大于0,消息类型 */char mtext[256]; /消息正文,可以是其他任何类型/} msgp; | |
msgsz | 要发送消息的大小,不含消息类型占用的4个字节,即mtext的长度 | |
msgflg | 0:当消息队列满时,msgsnd将会阻塞,直到消息能写进消息队列IPC_NOWAIT:当消息队列已满的时候,msgsnd函数不等待立即返回IPC_NOERROR:若发送的消息大于size字节,则把该消息截断,截断部分将被丢弃,且不通知发送进程 | |
函数返回值 | 成功:0出错:-1,错误原因存于error中 | |
错误代码 | EAGAIN:参数msgflg设为IPC_NOWAIT,而消息队列已满EIDRM:标识符为msqid的消息队列已被删除EACCESS:无权限写入消息队列EFAULT:参数msgp指向无效的内存地址EINTR:队列已满而处于等待情况下被信号中断EINVAL:无效的参数msqid、msgsz或参数消息类型type小于0 |
注意:
msgsnd()为阻塞函数,当消息队列容量满或消息个数满会阻塞。
消息队列已被删除,则返回EIDRM错误;被信号中断返回E_INTR错误。
如果设置IPC_NOWAIT消息队列满或个数满时会返回-1,并且置EAGAIN错误。
msgsnd()解除阻塞的条件有以下三个条件:
①不满足消息队列满或个数满两个条件,即消息队列中有容纳该消息的空间。
②msqid代表的消息队列被删除。
③调用msgsnd函数的进程被信号中断。
(3)msgrcv()
该函数用于从消息队列读取消息
所需头文件 | #include <sys/types.h>#include <sys/ipc.h>#include <sys/msg.h> | |
---|---|---|
函数说明 | 从标识符为msqid的消息队列读取消息并存于msgp中,读取后把此消息从消息队列中删除 | |
函数原型 | ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg); | |
函数传入值 | msqid | 消息队列标识符 |
msgp | 存放消息的结构体,结构体类型要与msgsnd函数发送的类型相同 | |
msgsz | 要接收消息的大小,不含消息类型占用的4个字节 | |
msgtyp | 0:接收第一个消息>0:接收类型等于msgtyp的第一个消息<0:接收类型等于或者小于msgtyp绝对值的第一个消息 | |
msgflg | 0: 阻塞式接收消息,没有该类型的消息msgrcv函数一直阻塞等待IPC_NOWAIT:如果没有返回条件的消息调用立即返回,此时错误码为ENOMSGIPC_EXCEPT:与msgtype配合使用返回队列中第一个类型不为msgtype的消息IPC_NOERROR:如果队列中满足条件的消息内容大于所请求的size字节,则把该消息截断,截断部分将被丢弃 | |
函数返回值 | 成功:实际读取到的消息数据长度出错:-1,错误原因存于error中 | |
错误代码 | E2BIG:消息数据长度大于msgsz而msgflag没有设置IPC_NOERROREIDRM:标识符为msqid的消息队列已被删除EACCESS:无权限读取该消息队列EFAULT:参数msgp指向无效的内存地址ENOMSG:参数msgflg设为IPC_NOWAIT,而消息队列中无消息可读EINTR:等待读取队列内的消息情况下被信号中断 |
注意:
msgrcv()解除阻塞的条件有以下三个:
① 消息队列中有了满足条件的消息。
② msqid代表的消息队列被删除。
③ 调用msgrcv()的进程被信号中断。
(4)mssgctl()
该函数用于获取和设置消息队列的属性
所需头文件 | #include <sys/types.h>#include <sys/ipc.h>#include <sys/msg.h> | |
---|---|---|
函数说明 | 获取和设置消息队列的属性 | |
函数原型 | int msgctl(int msqid, int cmd, struct msqid_ds *buf) | |
函数传入值 | msqid | 消息队列标识符 |
cmd | IPC_STAT:获得msgid的消息队列头数据到buf中IPC_SET:设置消息队列的属性,要设置的属性需先存储在buf中,可设置的属性包括:msg_perm.uid、msg_perm.gid、msg_perm.mode以及msg_qbytesIPC_RMID:用于删除一个已经无需继续使用的信号量标识符 | |
buf | 消息队列管理结构体,请参见消息队列内核结构说明部分,没有的时候写nullptr即可 | |
函数返回值 | 成功:0出错:-1,错误原因存于error中 | |
错误代码 | EACCESS:参数cmd为IPC_STAT,确无权限读取该消息队列EFAULT:参数buf指向无效的内存地址EIDRM:标识符为msqid的消息队列已被删除EINVAL:无效的参数cmd或msqidEPERM:参数cmd为IPC_SET或IPC_RMID,却无足够的权限执行 |
3.使用案例
下面这段代码源自网上,我自己通过调试测试了其效果。它通过使用fork()函数创建了两个一模一样的进程,一个进程用于发送数据到消息队列,另一个进程则是从消息队列读取消息。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/msg.h>
#include <sys/types.h>
#include<unistd.h>
int main() {
//IPC_PRIVATE
int id = msgget(0x121212, IPC_CREAT | 0644);
if (-1 == id) {
perror("msgget error");
return 1;
}
// 创建一个一模一样的进程 => 两个进程,一个发消息,一个收消息 => 测试需要
pid_t pid = fork();
//如果返回值小于0,则创建失败
if (pid < 0)
{
printf(" error\n");
exit(0);
}
if (pid == 0) { //返回值等于0,表示创建成功,是子进程运行
printf("子进程写队列 pid=%d \n", getpid());
for (int i = 0; i < 100; ++i) {
sleep(1);
char text[50] = "x-is sub test";
// 要发送消息的大小
int buf_size = sizeof(text) + sizeof(long);
// 发送的消息
struct msgbuf* buf = (msgbuf*)malloc(buf_size);
buf->mtype = i;// 消息类型
strcpy(buf->mtext, text);// 消息正文
// 发送消息
int snd_ret = msgsnd(id, buf, buf_size, 0);
free(buf);
}
}
else { // 主进程读取队列
printf("主进程读取队列 pid=%d \n", getpid());
for (int i = 0; i < 100; ++i) {
char text[50];
// 要接收消息的大小
int buf_size = sizeof(text) + sizeof(long);
struct msgbuf* buf = (msgbuf*)malloc(buf_size);
// buf内容清零
bzero(buf, buf_size);
// 接收消息
int rcv_ret = msgrcv(id, buf, buf_size, 0, 0);
if (rcv_ret >= 0) {// 接收到了数据
// 打印出数据
printf("rcv rcv_ret=%d,mtype=%ld,mtext=%s\n", rcv_ret, buf->mtype, buf->mtext);
free(buf);
buf = nullptr;
}
else {
printf("rcv rcv_ret=%d \n", rcv_ret);
break;
}
}
}
// 删除信号标识符
int ctl_ret = msgctl(id, IPC_RMID, nullptr);
printf("pid=%d,ctl_ret=%d \n", getpid(), ctl_ret);
}
测试结果:
参考网站:
[1] 消息队列函数(msgget、msgctl、msgsnd、msgrcv)及其范例:blog.csdn.net/guoping16/a…
[2] msgget(key, 0644 | IPC_CREAT)是什么意思,0644代表什么?:blog.csdn.net/laji946/art…
[3] msgget(2) — Linux manual page:man7.org/linux/man-p…
[4] linux C/C++ 进程间通信(共享消息队列):zhuanlan.zhihu.com/p/341799803