一、消息队列的概念
1.定义:
消息队列是消息的链表,存放在内核中并由消息队列标识符表示
。消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法,这个数据块由消息类型和数据等信息组成
。消息队列遵循先进先出的策略,但因为存在消息类型,有优先级,故消息队列类似于一个优先级队列
。
内核为每个消息队列维护了一个数据结构msqid_ds,用于标识消息队列,成员有:
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; //队列中允许最大字节数
pid_t msg_lspid; //最后一次添加的消息
pid_t msg_lrpid; //最后一次获取的消息
……
};
通过msg_perm标识一个消息队列,通过msqid_ds表示一个消息队列,根据msg_first,msg_last成员维护一个先进先出的msg链表队列(固定类型),每次发送消息时,将其构造成msg结构对象,即自己定义的消息结构体,里面可以包含消息类型,内容等信息,
并添加到msqid_ds成员维护的链表队列中,如下图所示:
消息队列和管道类似,在消息长度和数目上都有一定的限制,具体的系统限制如下表所示:
导出的:表示这种限制来源于其他限制,如最大消息数是根据最大队列数和队列允许的最大数据量决定的。
可以使用:
cat /proc/sysy/kernel/msgmax 查看具体的数据
2.特点:
- 消息队列可以实现消息的随机读取,消息的类型不固定时,就
不一定以先进先出的次序读取
,可以根据类型的优先级进行读取。 - 消息允许一个或多个进程向它写入或者读取信息。
- 与无名管道,有名管道一样,从消息队列中读出消息,消息队列中对应的数据都会被删除。
- 每个消息队列都有消息队列标识符,在msg_perm中存储,是唯一的标识符。
- 消息队列是消息的链表,存放在内存中,由内核维护,
生命周期随内核
,即只有内核重启或人工删除消息队列时,该消息队列才会被删除,若不人工删除消息队列,消息队列会一直存在于系统中。 - 消息队列可以
双向通信
。 - 克服了管道只能承载无格式字节流的缺点,
不会出现数据粘包的现象,因为消息之间有间隔
。
二、消息队列相关函数
利用消息队列进行进程间铜通讯的方式,我们可以类比为:A需要发送信息给B,但是因为距离原因当面给A,那么就需要A先把信息给XX地址的SS驿站,驿站将其存储,存储箱编号为YYYYY,B要取出这个信息,必须到XX地址的SS驿站取出编号为YYYYY的箱子,打开才可以获取。
在消息队列中,键(key)值相当于XX地址,消息队列标识符相当于SS驿站,消息类型相当于YYYYY编号的箱子。
同一个键(key)值可以保证是同一个消息队列,同一个消息队列标识符才能保证不同进程可以相互通信,同一个消息类型才能保证某个进程取出的是对方的信息。
1.键值
System V提供的进程间通信机制需要一个key值,通过key值就可以在系统内获得一个唯一的消息队列标识符, key值可以是人为指定的数字,强转为key_t类型即可,也可以通过ftok 函数获得。
- 人为指定
(key_t)1234;//1234就为key值,在创建获取消息队列函数的第一个参数可以这样写
- 通过函数获得key值
# include<sys/types.h>
# include<sys/ipc.h>
key_t ftok(const char* pathname,int proj_id);
成功返回key值,失败返回-1
参数为:路径名;项目ID,非0整数,只有低8位有效。
2. 打开/创建队列 msgget函数
调用的第一个函数,功能是打开一个现存队列或者创建一个新队列,如果多个进程想通过同一个消息队列完成数据通信,则每个进程使用key值创建或者获取同一个消息队列的内核对象ID
。key就类似一个标识符。函数原型如下:
# include<sys/msg.h>
int msgget(key_t key,int flag);
成功则返回消息队列ID,出错返回-1
参数:
- key对应IPC的键值,我们可以使用两种方法来获得。
- msgflg:标识函数的行为及消息队列的权限,取值如下:
取值 | 含义 |
---|---|
IPC_CREATE | 创建消息队列 |
IPC_EXCL | 检测消息队列是否存在 |
位或权限位 | 可以设置消息队列的访问权限,和其他两个参数可以或表示,取值和open函数的open_t一样,一般为0664 |
当创建一个新的队列时,系统会初始化msqid_ds结构的下列成员:
- 对ipc_perm结构体赋值(该结构体中的mode成员按msgget函数的msgflg参数中的对应权限位设置。
- msg_qnum、msg_lspid、msg_lrpid、msg_stime、msg_rtime都设置为0。
- msg_ctime设置为当前时间。
- msg_qbytes设置为系统限制值。
3.队列设置函数msgctl
msgctl函数对队列执行多种操作如修改消息队列的属性,删除消息队列等,它和另外两个与信号量和共享存储函数(semctl,shmctl)都类似于垃圾桶函数,函数原型为:
# include<sys/msg.h>
int msgctl(int msqid,int cmd,struct msqid_ds* buf);
成功返回0,失败返回-1
参数:
-
msqid:消息标识符,即由msgget返回的消息队列表示码
-
cmd,有三个可选的值:
(1)IPC_STAT:把msqid_ds结构体中个元素的当前值存入到由buf指向的结构体中。
(2)IPC_SET:把msqid结构体中的元素设置为buf中的对应值。
(3)IPC_RMID:从系统中删除该消息队列以及仍在该队列中的所有数据,这种删除立即生效。 -
buff:msqid_ds数据类型的地址,用于存放或更改消息队列的属性。
4.增加数据msgsnd函数
将新消息添加到消息队列中,函数原型为:
# include<sys/msg.h>
int msgsnd(int msqid,const void* ptr,size_t nbytes ,int flag)
成功返回0,出错返回-1
函数返回成功后,与消息队列对应的msqid_ds结构得到更新,表明调用的进程ID,调用的时间及队列中新增的消息。
参数
-
msqid:消息标识符
-
ptr参数:指向一个长整型数,包含了正的整型消息类型,在其后紧跟着消息数据,若nbytes为0,则无消息数据。若发送的最长消息是512字节,则可定义下列结构:结构体名称,正文成员自行定义,不固定。
struct msmesg{ long mtype; //消息类型 char mtext[512];//实际消息数据,可以设置小于512的大小 …… //可以有多个消息正文成员 };
ptr参数实际指向一个msmesg结构体的指针,
注意,这个结构体需要自己声明在头文件中,系统并不提供。
-
nbytes:如果ptr指针指向的结构体有两个以上的成员,则此参数对应ptr指向的结构体中mtext成员大小,表示消息内容的大小。
-
flag:
(1)默认填0。如果msgsnd函数阻塞,则进程阻塞直到下列情况出现为止:有空间可以容纳要发送的消息,返回EINTR;从系统中删除此队列,或捕捉到一个信号,并从信号处理程序返回,返回EIDRM标识符被删除。(2)IPC_NOWAIT:类似文件IO的非阻塞IO标志,若消息队列已满,或者队列中的消息总数等于系统限制值,或队列中的字节总数等于系统限制值,
则指定IPC_NOWAIT使得msgsnd函数阻塞立即出错返回EAGAIN
。
5.获取消息msgrcv函数
从标识符为msqid的消息队列中接收一个消息,一旦接收消息成功,则消息在消息队列中被删除。函数原型为:
# include<sys/msg.h>
ssize_t msgrcv(int msqid,void* ptr,size_t nbytes,long type,int flag)
成功返回消息的数据部分的长度,出错返回-1
msgrcv成功执行后,内核更新与该消息队列相关联的msqid_ds结构,表明调用的进程ID,调用时间,并指示队列中的消息减少了1个。
参数:
- msqid:消息队列标识符。
- ptr:和msgsnd函数一样,ptr指向一个存放消息结构体的地址。
- nbytes:数据缓冲区长度。若返回的消息长度大于nytes,而且在flag中设置了MSG_NOERROR位,则该消息会被截断,系统不会通知我们,若没有设置这一标志,则出错返回E2BIG,消息仍留在队列中。
- type:执行从消息队列中取出哪一类型的消息:
(1)type==0:返回队列中的第一个消息。
(2)type>0:返回队列中消息类型为type的第一个消息。这时以非先进先出的次序读消息。
(3)type<0:返回队列中消息类型值小于等于type绝对值的消息,如果这种消息有若干个,则取类型值最小的消息。 - flag参数:
(1)默认为0:msgrcv出现阻塞,直到出现下列情况才终止:有了指定类型的消息,从系统中删除此队列,返回-1;或捕捉到一个信号并从信号处理程序返回,返回-1。
(2)IPC_NOWAIT:函数阻塞会立即返回-1。
(3)MSG_NOERROR:若返回的消息长度大于nytes,则该消息会被截断,系统不会通知。
我们画出存在消息类型时的消息队列图:
可以看到有两个消息类型1,2;2的优先级>1的优先级,那么此时的队列为优先级队列。msgrcv一次只能获取一条消息,数据之间存在间隔,故不会出现粘连数据的现象。
如果我们要取2类型的数据,因为2的优先级>1,所以无论前面有多少个1,都无所谓,都可以取出2,但是如果c取1类型的数据,那么会先取到a进程指向的数据,因为是一种先进先出的结构,所以会先拿到最开始的1。
6.两个命令:
ipcs -q //显示消息队列类型
ipcrm -q mspid(消息队列的id) //删除消息队列类型
三、实例
题目: 利用消息队列实现连个进程间的通信,A进程给B进程发送指定消息“hello",B进程接收后将其打印,定义结构中的:消息类型为100,字节大小为128;
将存储消息的结构体在头文件中定义:
# include<stdio.h>
# include<stdlib.h>
# include<assert.h>
# include<sys/msg.h>
# include<string.h>
# include<unistd.h>
typedef struct msgdata //定义消息结构体
{
long mytype;//消息类型
char madata[128];//消息内容
}MsgData;
A.c
# include"./msg.h" //引入头文件
int main()
{
MsgData data;//定义结构体
memset(&data,0,sizeof(data)); //初始化
data.mytype=100;
strcpy(data.madata,"hello");
int msqid=msgget((key_t)1234,IPC_CREAT|0664);//创建消息队列
assert(msqid!=-1);
msgsnd(msqid,&data,strlen(data.madata),0);//将消息添加到消息队列中
exit(0);
}
B.c
# include"./msg.h"
int main()
{
int msqid=msgget((key_t)1234,IPC_CREAT|0664);//必须以相同的key值创建和打开消息队列
assert(msqid!=-1);
MsgData data;
memset(&data,0,sizeof(data));
msgrcv(msqid,&data,127,100,0);//从消息队列中取出消息类型为100的消息
printf("read:%s\n",data.madata);//打印消息内容
exit(0);
}
运行测试结果:
1.先运行B,不运行A:
如果先运行B会阻塞,因为此时没有消息发送。
运行A进程会立马将消息发送,B打印消息数据。
2.执行A3次之后,再执行B:
如果多次执行A,如执行3次,那么就会一直往消息队列中填充数据,消息队列中由3个内容为hello的消息。这时开始运行B,则执行一次,读取一次,而不是一次性读取3次的数据,执行三次将消息数据读完,读完后再执行就会阻塞。
3.消息队列的建立删除
如果A创建消息队列,发送信息后,再利用ipsc -q msqid删除消息队列,此时再执行B,B会阻塞,因为没有消息,但是B会创建出一个新的消息队列,只不过消息队列中没有信息。如下图所示:
加油哦!💪。