一、消息队列概念及原理
消息队列是消息的结构体,我们在发送消息的时候,是以结构体的方式发送,结构体的元素包含消息类型和消息内容。消息队列存放内核当中,一个消息队列由一个标识符(即队列ID)来标识;。
消息队列的特点
(1) 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级;
(2) 消息队列独立于发送与接收进程。进程终止时,消息队列的内容并不会被删除;
(3) 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取,我们通常会使用按类型来读取消息。
二、消息队列相关API介绍
这些函数都在以下头文件当中,使用时不要忘了引用:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
1. int msgget( key_t key,int flag);
获取一个消息队列,成功返回0,失败返回 -1。
参数介绍:
1. key 是一个索引值,可以通过它来找到相应的消息队列;
2. flag 为标志位,它其中包含了PIC_CREAT 宏,可以用它来创建一个消息队列;
返回值: 成功返回0;失败返回 -1。
2. int msgsnd(int msgid, const void *ptr, size_t size ,int flag)
消息发送函数。
参数介绍:
1. msgid 需要发送的消息队列ID;
2. *ptr 需要发送的消息(消息结构体);
3. size 消息内容的大小;
4. flg 消息的标志位,0 为默认发送。
返回值: 成功返回 0;失败返回 -1。
3.int msgrcv(int msgid, const void *ptr, size_t size ,long type,int flag)
这个是读取消息函数。
参数介绍:
1. msgid 需要读取的消息队列的ID;
2. *ptr 读取的消息,读取到的消息放在这个,这个同样是结构体;
3. size 想要读取消息内容的大小;
4. type 要读取消息的类型,为保证读取正常,type应该和发送的保持一致;
5. flg 消息的标志位,0 为默认方式读取。
返回值: 成功返回 0;失败返回 -1。
4. int msgctl(int msgid, int cmd,struct msgid_ds *buf)
消息控制函数,可以启动消息或移除等操作。
参数介绍:
1. msgid 需要控制的消息队列的ID;
2. cmd 控制指令;
3. *buf 用来存放指令执行之后的一些信息,大部分情况都赋值:NULL。
返回值: 成功返回 0;失败返回 -1。
API介绍完毕,为了加深印象,我们可以用做两个小实验来熟悉一下它们的用法。
三、练手小实验
实验一、消息的读取及发送
编写两个程序,暂且命名为 MSG_demo1.c 负责读取消息队列的数据,MSG_demo2.c 负责往消息队列写入消息。
MSG_demo1.c 代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdlib.h>
#include <string.h>
#define msgkey 0x5466 //定义消息队列的索引值
//定义一个需要读取的消息结构体
struct msgbuf
{
long msgtype; //消息的类型
char ptr[128];//消息的内容
};
int main()
{
struct msgbuf Rcv_Buf; //定义一个结构体变量
int MsgID=msgget(msgkey,IPC_CREAT|0777);//创建消息队列,并赋予可读可写可执行权限
/* 调试信息 */
if(MsgID==-1){
printf("MSG creat failuer!\n");
}
int nread=msgrcv(MsgID,&Rcv_Buf,sizeof(Rcv_Buf),123,0);//开始读取,注意 123 这个消息类型
if(nread==-1){
printf("msgrcv failuer!\n");
}
printf("MsgRcv data:%s\n",Rcv_Buf.ptr);//打印读取的消息内容
return 0;
}
如果该消息队列里面的消息是空的话,即消息链表中没有节点,这个程序会一直卡在读取函数及msgrcv 处,直到队列里面被发送了消息才会读取成功并结束程序。
MSG_demo2.c 代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdlib.h>
#include <string.h>
#define msgkey 0x5466 //定义消息队列的索引,和上面的程序保持一致,否则会读取失败
//定义一个需要发送的消息结构体
struct msgbuf
{
long mytype;
char mtext[128];
};
int main()
{
struct msgbuf Snd_Buf={123,"This Msg form Send!"};//定义消息的类型和内容
int MsgID=msgget(msgkey,IPC_CREAT|0777); //创建消息队列,并赋予可读可写可执行权限
/* 调试信息 */
if(MsgID==-1){
printf("MSG creat failuer!\n");
}
int nsend=msgsnd(MsgID,&Snd_Buf,strlen(Snd_Buf.mtext),0);//开始发送消息
if(nsend==-1){
printf("msgsend failuer!\n");
}
printf("MsgSend data succes!\n"); //打印调试是信息
return 0;
}
这个程序会往消息队列发送一个类型为123,内容为“This Msg form Send!"的消息。
实验结果:
左边为MSG_demo1.c 运行的结果,右边为MSG_demo2.c 运行的结果,在 MSG_demo2.c 提示消息发送成功(MsgSend data succes),然后MSG_demo1.c成功读取了MSG_demo2.c发送的消息。
实验二、同时读写
消息队列这种通信方式区别于管道的一点是:不分读取端和发送端,即读取消息和发送消息可以在同一程序下进行,我们以上面的两个程序 做一下修改,只要再定一个消息就可以了:
MSG_demo1.c 修改后代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdlib.h>
#include <string.h>
#define msgkey 0x5466
struct msgbuf
{
long msgtype;
char ptr[128];
};
int main()
{
struct msgbuf Rcv_Buf;
struct msgbuf Send_Buf={521,"This MSG form Rcv!"}; //定义一个需要发送的消息
int MsgID=msgget(msgkey,IPC_CREAT|0777);
if(MsgID==-1){
printf("MSG creat failuer!\n");
}
int nread=msgrcv(MsgID,&Rcv_Buf,sizeof(Rcv_Buf),123,0);
if(nread==-1){
printf("msgrcv failuer!\n");
}
printf("MsgRcv data:%s\n",Rcv_Buf.ptr);
int nSend=msgsnd(MsgID,&Send_Buf,strlen(Send_Buf.ptr),0);//开始发送
if(nSend==-1){
printf("msgsenf failuer!");
}
printf("Msg send success!\n"); //打印消息发送结果
return 0;
}
这次,MSG_demo1.c 发送的是:类型为521,内容为“This MSG form Rcv!"的消息
MSG_demo2.c 修改后代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdlib.h>
#include <string.h>
#define msgkey 0x5466
struct msgbuf
{
long mytype;
char mtext[128];
};
int main()
{
struct msgbuf Snd_Buf={123,"This Msg form Send!"};
struct msgbuf Rcv_Buf; //定义一个接收的消息
int MsgID=msgget(msgkey,IPC_CREAT|0777);
if(MsgID==-1){
printf("MSG creat failuer!\n");
}
int nsend=msgsnd(MsgID,&Snd_Buf,strlen(Snd_Buf.mtext),0);
if(nsend==-1){
printf("msgsend failuer!\n");
}
printf("MsgSend data succes!\n");
int nread=msgrcv(MsgID,&Rcv_Buf,sizeof(Rcv_Buf.mtext),521,0);//开始读取类型为521的消息
if(nread==-1){
printf("Msg Rcv failuer!\n");
}
printf("Msg Rcv Data:%s\n",Rcv_Buf.mtext);
return 0;
}
运行结果:
同样把两个 .c 文件编译为 read 和 send;
可以看到,当 ./send 运行之后,打印出"MsgSend data succes!",证明发送成功,而 ./read 同时打印了读取到的消息内容和发送成功信息。右边也读取到了 ./read 发送的消息内容。
实验三、键值获取和消息移除
以上的两个实验,我们都是写死了一个索引 (#define msgkey 0x5466),但是实际编程中,内核是有自己的索引的,也就是索引节点。所以用键值来让内核分配索引,就不用我们自己定义索引,可以避免因为消息索引而出现读取错误的情况出现。下面介绍键值转换函数:
ftok函数:
系统IPC键值的格式转换函数。系统建立IPC通信时,必须指定一个值,通常情况下,该ID 值通过 ftok函数得到。使用它,需要 <sys/type.h>和<sys/ipc.h> 两个头文件。
函数原型: key_t ftok(consy char * fname,int id)
参数说明:
1. *fname:就是你指定得文件名(已经存在的文件),一般使用当前目录 “.”;
2. id: 这是子序号。虽然时int 类型,但是使用8bits(1~255);
在一般的UNIX实现中,是将文件的索引节点号取出,前面加上子序号得到key_t的返回值。如指定文件的索引节点号为65538,换算成16进制为 0x010002,而你指定的ID值为38,换算成16进制为0x26,则最后的key_t返回值为0x26010002。
查询文件索引节点号的方法是: ls -i
而消息移除就是使用msgctl函数来实现,具体是个怎么样的实现方式,我们放在代码里面说。我们把实验一的代码修改一下,并且重命名为:MSG_demo1Key.c 和 MSG_demo2Key.c ;
MSG_demo1Key.c的代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdlib.h>
#include <string.h>
struct msgbuf
{
long msgtype;
char ptr[128];
};
int main()
{
key_t key;//定义键值索引
struct msgbuf Rcv_Buf;
key=ftok(".",20);//键值格式转换
printf("Key=%x\n",key);//打印键值 16进制
int MsgID=msgget(key,IPC_CREAT|0777);
if(MsgID==-1){
printf("MSG creat failuer!\n");
}
int nread=msgrcv(MsgID,&Rcv_Buf,sizeof(Rcv_Buf),123,0);
if(nread==-1){
printf("msgrcv failuer!\n");
}
printf("MsgRcv data:%s\n",Rcv_Buf.ptr);
msgctl(MsgID,IPC_RMID,NULL);//移除消息队列
return 0;
}
MSG_demo1Key.c的功能: 读取消息之后,把消息队列从内核中移除。
MSG_demo2Key.c的代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdlib.h>
#include <string.h>
struct msgbuf
{
long mytype;
char mtext[128];
};
int main()
{
key_t key;//定义键值索引
struct msgbuf Snd_Buf={123,"This Msg form Send!"};
key =ftok(".",20); //键值格式转换
printf("Key=%x\n",key);//打印键值16进制
int MsgID=msgget(key,IPC_CREAT|0777);
if(MsgID==-1){
printf("MSG creat failuer!\n");
}
int nsend=msgsnd(MsgID,&Snd_Buf,strlen(Snd_Buf.mtext),0);
if(nsend==-1){
printf("msgsend failuer!\n");
}
printf("MsgSend data succes!\n");
return 0;
}
MSG_demo1Key.c的功能和原本的一样。
运行结果:
两边的Key=14053f05。
在实验过程中,我们应该知道,消息队列创建时,只是内核创建了消息链表。但是链表中没有任何的节点,发送消息其实就是往链表里面写入节点。为确保读取发送的消息,要保持两个一致:1.消息队列索引一致;2.消息类型一致。