17. 消息队列
一、什么是消息队列
消息队列是一种先进先出的队列型数据结构,实际上是系统内核中的一个内部链表。消息被顺序插入队列中,其中发送进程将消息添加到队列末尾,接受进程从队列头读取消息。
多个进程可同时向一个消息队列发送消息,也可以同时从一个消息队列中接收消息。发送进程把消息发送到队列尾部,接受进程从消息队列头部读取消息,消息一旦被读出就从队列中删除。
二、消息队列的API
2.1、消息队列的创建
我们可以使用 mq_open()
函数 创建或打开一个已存在的消息队列。
/**
* @brief 创建或打开一个已存在的消息队列
*
* @param __name 消息队列的名称
* @param __oflag 消息队列的控制权限
* @param ...
* @return mqd_t int类型的别名,成功返回消息队列描述符,失败则返回-1
*/
mqd_t mq_open(const char *__name, int __oflag, ...);
参数 __name
是 消息队列的名称。消息队列的名称必须以 /
开头,以 \0
结尾,中间可以包含若干字符,但不能有 /
。
参数 __oflg
用来 指定打开模式,可以是以下模式。
O_RDONLY
:打开消息队列只用于接收消息。O_WRONLY
:打开消息队列只用于发送消息。O_RDWR
:打开的消息队列可以用于收发消息。O_CLOEXEC
:设置 close-on-exec 标记,这个标记表示执行 exec 是关闭文件描述符。O_CREAT
:如果文件描述符不存在时创建它。如果指定这个标记,需要额外提供 mode 和 attr 参数。O_EXCL
:创建一个当前进程独占的消息队列,要同时指定 O_CREAT ,要求创建的消息队列不存在,否则将会失败,并提示错误 EEXIST。O_NONBLOCK
:以非阻塞模式打开消息队列,如果设置了这个选项,在默认情况下收发消息阻塞时,会失败,并提示 EAGAIN。
如果参数 __oflg
包含 O_CREAT
模式,则第三个参数被取为 mode_t
,即 创建的消息队列的模式。每个消息队列在 mqueue 文件系统对应一个文件,mode 就是用来指定消息队列对应文件的权限。第四个参数被取为 struct mq_attr *
,即 指向消息队列属性的指针。如果第四个参数为 NULL,则使用默认属性。
struct mq_attr
{
__syscall_slong_t mq_flags; // 标记,对于mq_open()函数,忽略它,因为这个标记是通过前者的调用传递的
__syscall_slong_t mq_maxmsg; // 队列可以容纳的消息的最大数量
__syscall_slong_t mq_msgsize; // 单条消息的最大允许大小,以字节为单位
__syscall_slong_t mq_curmsgs; // 当前队列中的消息数量,对于mq_open()函数,忽略它
__syscall_slong_t __pad[4];
};
2.2、往消息队列发送消息
我们可以使用 mq_send()
函数 将消息追加到消息队列的尾部。
/**
* @brief 将消息追加到消息队列的尾部
*
* @param __mqdes 消息队列描述符
* @param __msg_ptr 指向消息的指针
* @param __msg_len 消息长度
* @param __msg_prio 消息的优先级
* @return int 成功返回0,失败返回-1
*/
int mq_send(mqd_t __mqdes, const char *__msg_ptr, size_t __msg_len, unsigned int __msg_prio);
参数 __msg_prio
用来 指定消息的优先级,消息队列中的数据是按照优先级降序排列的。如果新旧消息的优先级相同,则新消息排在后面。
如果指定的消息队列已满,并且在与 __mqdes
相关联的消息队列描述中未设置 O_NONBLOCK
,那么 mq_send()
会一直阻塞,直到消息队列中有空余空间。
如果当消息队列中有可用空间时有多个进程或线程正在等待发送,并且支持 优先级调度 选项,那么将取消阻塞等待时间最长的最高优先级的进程或线程以发送其消息。否则,未指定哪个正在等待的线程将被取消阻塞。
如果指定的消息队列已满,并且在与 __mqdes
关联的消息队列描述中设置了 O_NONBLOCK
,那么消息不会排队,并且会直接报错。
此时,我们可以使用 mq_timedsend()
往消息队列中发送消息。如果消息队列已满,会在规定时间内等待队列是否有空位可以允许新的消息入队。如果到达规定时间还没有空位,会返回一个信号处理函数。在与 __mqdes
关联的消息队列描述中设置了 O_NONBLOCK
,那么超时后,会直接报错。
/**
* @brief 将消息追加到消息队列的尾部
*
* @param __mqdes 消息队列描述符
* @param __msg_ptr 指向消息的指针
* @param __msg_len 消息长度
* @param __msg_prio 消息的优先级
* @param __abs_timeout 阻塞等待的时间
* @return int 成功返回0,失败返回-1
*/
int mq_timedsend(mqd_t __mqdes, const char *__msg_ptr, size_t __msg_len, unsigned int __msg_prio, const struct timespec *__abs_timeout);
消息的长度不能超过队里额 mg_msgsize 属性指定的队列最大容量,长度为 0 的消息是被允许的。
2.3、从消息队列中读取消息
我们可以使用 mq_reveive()
函数 从消息队列中读取最早入队且权限最高的消息。
/**
* @brief 从消息队列中读取最早入队且权限最高的消息
*
* @param __mqdes 消息队列描述符
* @param __msg_ptr 接收消息的缓存
* @param __msg_len 接收消息的缓冲区的大小
* @param __msg_prio 如果不为NULL,则用来接收收到消息的优先级
* @return int 成功返回接收的消息的字符数,失败则返回-1
*/
ssize_t mq_receive(mqd_t __mqdes, char *__msg_ptr, size_t __msg_len, unsigned int *__msg_prio);
/**
* @brief 从消息队列中读取最早入队且权限最高的消息
*
* @param __mqdes 消息队列描述符
* @param __msg_ptr 接收消息的缓存
* @param __msg_len 接收消息的缓冲区的大小
* @param __msg_prio 如果不为NULL,则用来接收收到消息的优先级
* @param __abs_timeout 阻塞等待的时间
* @return int 成功返回接收的消息的字符数,失败则返回-1
*/
int mq_timedsend(mqd_t __mqdes, const char *__msg_ptr, size_t __msg_len, unsigned int __msg_prio, const struct timespec *__abs_timeout);
2.4、释放消息队列
用完消息队列之后,我们需要使用 mq_unlink()
函数释放消息队列。
/**
* @brief 释放消息队列
*
* @param __name 消息队列的名称
* @return int 成功返回0,失败返回-1
*/
int mq_unlink(const char *__name);
当我们清除消息队列时,mqeueue 文件系统中的对应文件被立即七年给出。消息队列本身的清除必须等待指向该消息队列的描述符全部关闭之后才会发生。
三、消息队列的使用
我们新建一个 main.c 文件。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <mqueue.h>
#include <time.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid = 0;
mqd_t mq_des = 0;
// 创建消息队列
char mq_name[] = "/mq_test";
struct mq_attr attr = {0};
attr.mq_maxmsg = 10; // 队列可以容纳的消息的最大数
attr.mq_msgsize = 100; // 单条消息的最大允许大小,以字节为单位
if ((mq_des = mq_open(mq_name, O_RDWR | O_CREAT, 0644, &attr)) == -1)
{
perror("create message queue failed!");
exit(EXIT_FAILURE);
}
// 创建父子进程
if ((pid = fork()) < 0)
{
perror("create child process failed!");
exit(EXIT_FAILURE);
}
else if (pid == 0)
{
// 这里是子进程要指定的代码
// 子进程用来向消息队列中发送消息
char data[100] = {0};
struct timespec time_info = {0};
// 循环发送10条消息
for (int i = 0; i < 10; i++)
{
// 清空数据
memset(data, 0, sizeof(data));
sprintf(data, "child process send a message (%d)\n", i + 1);
// 获取当前的具体时间
clock_gettime(0, &time_info);
// 当前消息累加5s
time_info.tv_sec += 5;
if (mq_timedsend(mq_des, data, strlen(data), 0, &time_info) == -1)
{
perror("send messagew failed!");
}
}
}
else
{
// 这里是父进程要指定的代码
// 等待子进程完成,确保子进程已经发送消息到消息队列中
char data[100] = {0};
struct timespec time_info = {0};
// 循环接收消息
for (int i = 0; i < 10; i++)
{
// 清空缓冲区
memset(data, 0, sizeof(data));
// 设置接收数据的等待时间
clock_gettime(0, &time_info);
time_info.tv_sec += 15;
if (mq_timedreceive(mq_des, data, sizeof(data), NULL, &time_info) == -1)
{
perror("receive data failed!");
}
printf("parent receive data: %s\n", data);
}
// 父进程等待子进程结束
waitpid(pid, NULL, 0);
}
// 子进程和父进程都需要释放消息队列引用
close(mq_des);
// 清除消息队列只需要执行一次
if (pid > 0)
{
mq_unlink(mq_name);
}
return 0;
}
我们在终端中使用 gcc 编译程序,生成可执行文件,然后运行它。
gcc main.c -o main
./main
四、生产者消费者模型
我们新建一个 producer.c 文件,用来存放生产者相关的代码。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <mqueue.h>
#include <time.h>
int main(void)
{
mqd_t mq_des = 0;
char data[100];
struct timespec time_info = {0};
// 创建消息队列
char mq_name[] = "/mq_test";
struct mq_attr attr = {0};
attr.mq_maxmsg = 10; // 队列可以容纳的消息的最大数
attr.mq_msgsize = 100; // 单条消息的最大允许大小,以字节为单位
if ((mq_des = mq_open(mq_name, O_RDWR | O_CREAT, 0644, &attr)) == -1)
{
perror("create message queue failed!");
exit(EXIT_FAILURE);
}
// 不断接收控制台中的数据,发送到消息队列
while (1)
{
// 清空缓冲区
memset(data, 0, sizeof(data));
ssize_t length = read(STDIN_FILENO, data, 100);
// 获取当前时间
clock_gettime(0, &time_info);
// 累加5s
time_info.tv_sec += 5;
if (length == -1)
{
perror("read data from terminal failed!");
continue;
}
else if (length == 0)
{
// 使用Ctrl+D关闭控制台输入,此时将EOF当作一条消息发送到消息队列
char eof = EOF;
printf("EOF, exit...\n");
if (mq_timedsend(mq_des, &eof, 1, 0, &time_info) == -1)
{
perror("send eof message failed!");
}
break;
}
else
{
// 正常接收到控制台的可读信息
if (mq_timedsend(mq_des, data, strlen(data), 0, &time_info) == -1)
{
perror("send message failed!");
}
printf("data received from the terminal has been sent to the message queue!\n");
}
}
// 关闭消息队列描述符
close(mq_des);
return 0;
}
接着,我们新建一个 customer.c 文件,用来存放消费者相关的代码。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <mqueue.h>
#include <time.h>
int main(void)
{
mqd_t mq_des = 0;
char data[100];
struct timespec time_info = {0};
// 创建消息队列
char mq_name[] = "/mq_test";
struct mq_attr attr = {0};
attr.mq_maxmsg = 10; // 队列可以容纳的消息的最大数
attr.mq_msgsize = 100; // 单条消息的最大允许大小,以字节为单位
if ((mq_des = mq_open(mq_name, O_RDWR | O_CREAT, 0644, &attr)) == -1)
{
perror("create message queue failed!");
exit(EXIT_FAILURE);
}
// 不断接收控制台中的数据,发送到消息队列
while (1)
{
// 清空缓冲区
memset(data, 0, sizeof(data));
// 获取当前时间
clock_gettime(0, &time_info);
// 累加5s
time_info.tv_sec += 15;
// 读取消息队列的一条数据
if (mq_timedreceive(mq_des, data, sizeof(data), NULL, &time_info) == -1)
{
perror("receive data failed!");
}
else
{
if (data[0] == EOF)
{
printf("receive the ending information sent by the producer\n");
break;
}
else
{
printf("receive data from producer: %s\n", data);
}
}
}
// 关闭消息队列描述符
close(mq_des);
// 清除消息队列
mq_unlink(mq_name);
return 0;
}
接着,我们在终端中输入 gcc 编译程序,生成对应的可执行程序。
gcc producer.c -o producer
gcc consumer.c -o consumer
然后,我们再开一个终端窗口,在两个终端窗口中先后运行生成的可执行程序。
第一个终端窗口。
./producer
第二个终端窗口。
./consumer