消息队列是另一种常用的线程间通讯方式,是邮箱的扩展。可以应用在多种场合:线程间的消息交换、使用串口接收不定长数据等。
消息队列的工作机制
消息队列能够接收来自线程或中断服务例程中不固定长度的消息,并把消息缓存在自己的内存空间中。其他线程也能够从消息队列中读取相应的消息,而当消息队列是空的时候,可以挂起读取线程。当有新的消息到达时,挂起的线程将被唤醒以接收并处理消息。消息队列是一种异步的通信方式。
如下图所示,线程或中断服务例程可以将一条或多条消息放入消息队列中。同样,一个或多个线程也可以从消息队列中获得消息。当有多个消息发送到消息队列时,通常将先进入消息队列的消息先传给线程,也就是说,线程先得到的是最先进入消息队列的消息,即先进先出原则 (FIFO)。
RT-Thread 操作系统的消息队列对象由多个元素组成,当消息队列被创建时,它就被分配了消息队列控制块:消息队列名称、内存缓冲区、消息大小以及队列长度等。同时每个消息队列对象中包含着多个消息框,每个消息框可以存放一条消息;消息队列中的第一个和最后一个消息框被分别称为消息链表头和消息链表尾,对应于消息队列控制块中的 msg_queue_head 和 msg_queue_tail;有些消息框可能是空的,它们通过 msg_queue_free 形成一个空闲消息框链表。所有消息队列中的消息框总数即是消息队列的长度,这个长度可在消息队列创建时指定。
消息对列控制块
在 RT-Thread 中,消息队列控制块是操作系统用于管理消息队列的一个数据结构,由结构体 struct rt_messagequeue 表示。另外一种 C 表达方式 rt_mq_t,表示的是消息队列的句柄,在 C 语言中的实现是消息队列控制块的指针。消息队列控制块结构的详细定义请见以下代码:
struct rt_messagequeue
{
struct rt_ipc_object parent;
void* msg_pool; /* 指向存放消息的缓冲区的指针 */
rt_uint16_t msg_size; /* 每个消息的长度 */
rt_uint16_t max_msgs; /* 最大能够容纳的消息数 */
rt_uint16_t entry; /* 队列中已有的消息数 */
void* msg_queue_head; /* 消息链表头 */
void* msg_queue_tail; /* 消息链表尾 */
void* msg_queue_free; /* 空闲消息链表 */
rt_list_t suspend_sender_thread; /* 发送线程的挂起等待队列 */
};
typedef struct rt_messagequeue* rt_mq_t;
rt_messagequeue 对象从 rt_ipc_object 中派生,由 IPC 容器所管理。
消息队列的管理方式
消息队列控制块是一个结构体,其中含有消息队列相关的重要参数,在消息队列的功能实现中起重要的作用。消息队列的相关接口如下图所示,对一个消息队列的操作包含:创建消息队列 - 发送消息 - 接收消息 - 删除消息队列。
创建和删除消息队列
消息队列在使用前,应该被创建出来,或对已有的静态消息队列对象进行初始化,创建消息队列的函数接口如下所示:
rt_mq_t rt_mq_create(const char* name, rt_size_t msg_size,
rt_size_t max_msgs, rt_uint8_t flag);
创建消息队列时先从对象管理器中分配一个消息队列对象,然后给消息队列对象分配一块内存空间,组织成空闲消息链表,这块内存的大小 =[消息大小 + 消息头(用于链表连接)的大小]X 消息队列最大个数,接着再初始化消息队列,此时消息队列为空。下表描述了该函数的输入参数与返回值:
参数 | 描述 |
---|---|
name | 消息队列的名称 |
msg_size | 消息队列中一条消息的最大长度,单位字节 |
max_msgs | 消息队列的最大个数 |
flag | 消息队列采用的等待方式,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO |
返回 | —— |
RT_EOK | 发送成功 |
消息队列对象的句柄 | 成功 |
RT_NULL | 失败 |
注:RT_IPC_FLAG_FIFO 属于非实时调度方式,除非应用程序非常在意先来后到,并且你清楚地明白所有涉及到该消息队列的线程都将会变为非实时线程,方可使用 RT_IPC_FLAG_FIFO,否则建议采用 RT_IPC_FLAG_PRIO,即确保线程的实时性。
当消息队列不再被使用时,应该删除它以释放系统资源,一旦操作完成,消息队列将被永久性地删除。删除消息队列的函数接口如下:
rt_err_t rt_mq_delete(rt_mq_t mq);
删除消息队列时,如果有线程被挂起在该消息队列等待队列上,则内核先唤醒挂起在该消息等待队列上的所有线程(线程返回值是 - RT_ERROR),然后再释放消息队列使用的内存,最后删除消息队列对象。下表描述了该函数的输入参数与返回值:
参数 | 描述 |
---|---|
mq | 消息队列对象的句柄 |
返回 | —— |
RT_EOK | 成功 |
初始化和脱离消息队列
初始化静态消息队列对象跟创建消息队列对象类似,只是静态消息队列对象的内存是在系统编译时由编译器分配的,一般放于读数据段或未初始化数据段中。在使用这类静态消息队列对象前,需要进行初始化。初始化消息队列对象的函数接口如下:
rt_err_t rt_mq_init(rt_mq_t mq, const char* name,
void *msgpool, rt_size_t msg_size,
rt_size_t pool_size, rt_uint8_t flag);
初始化消息队列时,该接口需要用户已经申请获得的消息队列对象的句柄(即指向消息队列对象控制块的指针)、消息队列名、消息缓冲区指针、消息大小以及消息队列缓冲区大小。消息队列初始化后所有消息都挂在空闲消息链表上,消息队列为空。下表描述了该函数的输入参数与返回值:
参数 | 描述 |
---|---|
mq | 消息队列对象的句柄 |
name | 消息队列的名称 |
msgpool | 指向存放消息的缓冲区的指针 |
msg_size | 消息队列中一条消息的最大长度,单位字节 |
pool_size | 存放消息的缓冲区大小 |
flag | 消息队列采用的等待方式,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO |
返回 | —— |
RT_EOK | 成功 |
脱离消息队列将使消息队列对象被从内核对象管理器中脱离。脱离消息队列使用下面的接口:
rt_err_t rt_mq_detach(rt_mq_t mq);
复制错误复制成功
使用该函数接口后,内核先唤醒所有挂在该消息等待队列对象上的线程(线程返回值是 -RT_ERROR),然后将该消息队列对象从内核对象管理器中脱离。下表描述了该函数的输入参数与返回值:
参数 | 描述 |
---|---|
mq | 消息队列对象的句柄 |
返回 | —— |
RT_EOK | 成功 |
发送消息
线程或者中断服务程序都可以给消息队列发送消息。当发送消息时,消息队列对象先从空闲消息链表上取下一个空闲消息块,把线程或者中断服务程序发送的消息内容复制到消息块上,然后把该消息块挂到消息队列的尾部。当且仅当空闲消息链表上有可用的空闲消息块时,发送者才能成功发送消息;当空闲消息链表上无可用消息块,说明消息队列已满,此时,发送消息的的线程或者中断程序会收到一个错误码(-RT_EFULL)。发送消息的函数接口如下:
线程或者中断服务程序都可以给消息队列发送消息。当发送消息时,消息队列对象先从空闲消息链表上取下一个空闲消息块,把线程或者中断服务程序发送的消息内容复制到消息块上,然后把该消息块挂到消息队列的尾部。当且仅当空闲消息链表上有可用的空闲消息块时,发送者才能成功发送消息;当空闲消息链表上无可用消息块,说明消息队列已满,此时,发送消息的的线程或者中断程序会收到一个错误码(-RT_EFULL)。发送消息的函数接口如下:
rt_err_t rt_mq_send (rt_mq_t mq, void* buffer, rt_size_t size);
发送消息时,发送者需指定发送的消息队列的对象句柄(即指向消息队列控制块的指针),并且指定发送的消息内容以及消息大小。在发送一个普通消息之后,空闲消息链表上的队首消息被转移到了消息队列尾。下表描述了该函数的输入参数与返回值:
参数 | 描述 |
---|---|
mq | 消息队列对象的句柄 |
buffer | 消息内容 |
size | 消息大小 |
返回 | —— |
RT_EOK | 成功 |
-RT_EFULL | 消息队列已满 |
-RT_ERROR | 失败,表示发送的消息长度大于消息队列中消息的最大长度 |
等待方式发送消息
用户也可以通过如下的函数接口向指定的消息队列中发送消息:
rt_err_t rt_mq_send_wait(rt_mq_t mq,
const void *buffer,
rt_size_t size,
rt_int32_t timeout);
rt_mq_send_wait() 与 rt_mq_send() 的区别在于有等待时间,如果消息队列已经满了,那么发送线程将根据设定的 timeout 参数进行等待。如果设置的超时时间到达依然没有空出空间,这时发送线程将被唤醒并返回错误码。下表描述了该函数的输入参数与返回值:
参数 | 描述 |
---|---|
mq | 消息队列对象的句柄 |
buffer | 消息内容 |
size | 消息大小 |
timeout | 超时时间 |
返回 | —— |
RT_EOK | 成功 |
-RT_EFULL | 消息队列已满 |
-RT_ERROR | 失败,表示发送的消息长度大于消息队列中消息的最大长度 |
发送紧急消息
发送紧急消息的过程与发送消息几乎一样,唯一的不同是,当发送紧急消息时,从空闲消息链表上取下来的消息块不是挂到消息队列的队尾,而是挂到队首,这样,接收者就能够优先接收到紧急消息,从而及时进行消息处理。发送紧急消息的函数接口如下:
rt_err_t rt_mq_urgent(rt_mq_t mq, void* buffer, rt_size_t size);
下表描述了该函数的输入参数与返回值:
参数 | 描述 |
---|---|
mq | 消息队列对象的句柄 |
buffer | 消息内容 |
size | 消息大小 |
返回 | —— |
RT_EOK | 成功 |
-RT_EFULL | 消息队列已满 |
-RT_ERROR | 失败 |
接收消息
当消息队列中有消息时,接收者才能接收消息,否则接收者会根据超时时间设置,或挂起在消息队列的等待线程队列上,或直接返回。接收消息函数接口如下:
rt_ssize_t rt_mq_recv (rt_mq_t mq, void* buffer,
rt_size_t size, rt_int32_t timeout);
接收消息时,接收者需指定存储消息的消息队列对象句柄,并且指定一个内存缓冲区,接收到的消息内容将被复制到该缓冲区里。此外,还需指定未能及时取到消息时的超时时间。如下图所示,接收一个消息后消息队列上的队首消息被转移到了空闲消息链表的尾部。下表描述了该函数的输入参数与返回值:
参数 | 描述 |
---|---|
mq | 消息队列对象的句柄 |
buffer | 消息内容 |
size | 消息大小 |
timeout | 指定的超时时间 |
返回 | —— |
接收到消息的长度 | 成功收到 |
-RT_ETIMEOUT | 超时 |
-RT_ERROR | 失败,返回错误 |
消息队列应用示例
这是一个消息队列的应用例程,例程中初始化了 2 个静态线程,一个线程会从消息队列中收取消息;另一个线程会定时给消息队列发送普通消息和紧急消息,如下代码所示:
#include <rtthread.h>
/* 消息队列控制块 */
static struct rt_messagequeue mq;
/* 消息队列中用到的放置消息的内存池 */
static rt_uint8_t msg_pool[2048];
ALIGN(RT_ALIGN_SIZE)
static char thread1_stack[1024];
static struct rt_thread thread1;
/* 线程 1 入口函数 */
static void thread1_entry(void *parameter)
{
char buf = 0;
rt_uint8_t cnt = 0;
while (1)
{
/* 从消息队列中接收消息 */
if (rt_mq_recv(&mq, &buf, sizeof(buf), RT_WAITING_FOREVER) > 0)
{
rt_kprintf("thread1: recv msg from msg queue, the content:%c\n", buf);
if (cnt == 19)
{
break;
}
}
/* 延时 50ms */
cnt++;
rt_thread_mdelay(50);
}
rt_kprintf("thread1: detach mq \n");
rt_mq_detach(&mq);
}
ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
/* 线程 2 入口 */
static void thread2_entry(void *parameter)
{
int result;
char buf = 'A';
rt_uint8_t cnt = 0;
while (1)
{
if (cnt == 8)
{
/* 发送紧急消息到消息队列中 */
result = rt_mq_urgent(&mq, &buf, 1);
if (result != RT_EOK)
{
rt_kprintf("rt_mq_urgent ERR\n");
}
else
{
rt_kprintf("thread2: send urgent message - %c\n", buf);
}
}
else if (cnt>= 20)/* 发送 20 次消息之后退出 */
{
rt_kprintf("message queue stop send, thread2 quit\n");
break;
}
else
{
/* 发送消息到消息队列中 */
result = rt_mq_send(&mq, &buf, 1);
if (result != RT_EOK)
{
rt_kprintf("rt_mq_send ERR\n");
}
rt_kprintf("thread2: send message - %c\n", buf);
}
buf++;
cnt++;
/* 延时 5ms */
rt_thread_mdelay(5);
}
}
/* 消息队列示例的初始化 */
int msgq_sample(void)
{
rt_err_t result;
/* 初始化消息队列 */
result = rt_mq_init(&mq,
"mqt",
&msg_pool[0], /* 内存池指向 msg_pool */
1, /* 每个消息的大小是 1 字节 */
sizeof(msg_pool), /* 内存池的大小是 msg_pool 的大小 */
RT_IPC_FLAG_PRIO); /* 如果有多个线程等待,优先级大小的方法分配消息 */
if (result != RT_EOK)
{
rt_kprintf("init message queue failed.\n");
return -1;
}
rt_thread_init(&thread1,
"thread1",
thread1_entry,
RT_NULL,
&thread1_stack[0],
sizeof(thread1_stack), 25, 5);
rt_thread_startup(&thread1);
rt_thread_init(&thread2,
"thread2",
thread2_entry,
RT_NULL,
&thread2_stack[0],
sizeof(thread2_stack), 25, 5);
rt_thread_startup(&thread2);
return 0;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(msgq_sample, msgq sample);
仿真运行结果如下:
\ | /
- RT - Thread Operating System
/ | \ 3.1.0 build Aug 24 2018
2006 - 2018 Copyright by rt-thread team
msh > msgq_sample
msh >thread2: send message - A
thread1: recv msg from msg queue, the content:A
thread2: send message - B
thread2: send message - C
thread2: send message - D
thread2: send message - E
thread1: recv msg from msg queue, the content:B
thread2: send message - F
thread2: send message - G
thread2: send message - H
thread2: send urgent message - I
thread2: send message - J
thread1: recv msg from msg queue, the content:I
thread2: send message - K
thread2: send message - L
thread2: send message - M
thread2: send message - N
thread2: send message - O
thread1: recv msg from msg queue, the content:C
thread2: send message - P
thread2: send message - Q
thread2: send message - R
thread2: send message - S
thread2: send message - T
thread1: recv msg from msg queue, the content:D
message queue stop send, thread2 quit
thread1: recv msg from msg queue, the content:E
thread1: recv msg from msg queue, the content:F
thread1: recv msg from msg queue, the content:G
…
thread1: recv msg from msg queue, the content:T
thread1: detach mq
例程演示了消息队列的使用方法。线程 1 会从消息队列中收取消息;线程 2 定时给消息队列发送普通消息和紧急消息。由于线程 2 发送消息 “I” 是紧急消息,会直接插入消息队列的队首,所以线程 1 在接收到消息 “B” 后,接收的是该紧急消息,之后才接收消息“C”。
消息队列的使用场合
消息队列可以应用于发送不定长消息的场合,包括线程与线程间的消息交换,以及中断服务例程中给线程发送消息(中断服务例程不能接收消息)。下面分发送消息和同步消息两部分来介绍消息队列的使用。