本文参考自[野火EmbedFire]《RT-Thread内核实现与应用开发实战——基于STM32》,仅作为个人学习笔记。更详细的内容和步骤请查看原文(可到野火资料下载中心下载)
文章目录
消息队列的基本概念
消息队列是一种常用的线程间通讯方式,它能够接收来自线程或中断服务例程中不固定长度的消息,并把消息缓存在自己的内存空间中。其他线程也能够从消息队列中读取相应的消息,而当消息队列是空的时候,可以挂起读取线程。当有新的消息到达时,挂起的线程将被唤醒以接收并处理消息。消息队列是一种异步的通信方式。
——RT-Thread官方中文手册
通过消息队列服务,线程或中断服务例程可以将一条或多条消息放入消息队列中。同样,一个或多个线程可以从消息队列中获得消息。当有多个消息发送到消息队列时,通常是将先进入消息队列的消息先传给线程,也就是说,线程先得到的是最先进入消息队列的消息,即先进先出原则(FIFO)。同时 RT- Thread 中的消息队列支持优先级,也就是说在所有等待消息的线程中优先级最高的会先获得消息。
RT-Thread 中使用队列数据结构实现线程异步通信工作,具有如下特性:
- 消息支持先进先出方式排队与优先级排队方式,支持异步读写工作方式。
- 读队列支持超时机制。
- 支持发送紧急消息,这里的紧急消息是往队列头发送消息。
- 可以允许不同长度(不超过队列节点最大值)的任意类型消息。
- 一个线程能够从任意一个消息队列接收和发送消息。
- 多个线程能够从同一个消息队列接收和发送消息。
- 当队列使用结束后,需要通过删除队列操作释放内存函数回收
——原文
消息队列的运作机制
如图消息队列的工作示意图所示,通过消息队列服务,线程或中断服务例程可以将一条或多条消息放入消息队列中。同样,一个或多个线程可以从消息队列中获得消息。当有多个消息发送到消息队列时,通常应将先进入消息队列的消息先传给线程,也就是说,线程先得到的是最先进入消息队列的消息,即先进先出原则(FIFO)。
——RT-Thread官方中文手册
消息队列的阻塞机制
消息队列并不属于某个线程,所以多个线程在对消息队列进行操作时,要保护每个线程对队列的读写,不过操作系统已经有这种保护操作了,叫做消息队列的阻塞机制。
每个对消息队列进行读写操作的函数,都自带阻塞机制。当一个线程读到空队列时,可以有3种选择:
- 不等待消息,直接跳过读取;
- 等待一定时间,等到则进入就绪态,超时则进入阻塞态;
- 一直等,直接进入阻塞态。
当空闲消息链表上没有可用消息块,说明队列已满,这时发送消息方(线程或中断)会收到一个错误码,然后继续执行,发送消息是不带阻塞特性的。
消息队列的应用场景
消息队列可以应用于发送不定长消息的场合,包括线程与线程间的消息交换,以及在中断服务函数中给线程发送消息(中断服务例程不可能接收消息)。
消息队列控制块
消息队列控制块包含了消息队列的详细信息,包括队列池大小、队列消息大小、链表指针等。
struct rt_messagequeue
{
struct rt_ipc_object parent; /**< inherit from ipc_object */
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; /**< sender thread suspended on this message queue */
};
消息队列相关接口
只介绍几个常用的接口
创建消息队列 rt_mq_create()
rt_mq_t
rt_mq_create
(const char* name, rt_size_t msg_size, rt_size_t max_msgs, rt_uint8_t flag);
创建消息队列时,先创建一个消息队列控制块,然后给消息队列分配一块内存空间,组织成空闲消息链表,接着初始化消息队列。
参数 | 描述 |
---|---|
name | 消息队列的名称 |
msg_size | 消息队列中一条消息的最大长度 |
max_msgs | 消息队列的最大容量(消息个数) |
flag | 消息队列采用的等待方式 |
初始化消息队列 rt_mq_init()
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_mq_send()
rt_err_t
rt_mq_send
(rt_mq_t mq, void* buffer, rt_size_t size);
发送消息时,消息队列对象先从空闲消息链表上取一个空闲消息块,把线程或者中断服务程序发送的消息内容复制到空闲消息块上,然后把消息块挂到消息队列的尾部。(在发送一个普通消息之后,空闲消息链表上的队首消息被转移到了消息队列尾)
参数 | 描述 |
---|---|
mq | 消息队列对象的句柄 |
buffer | 消息内容 |
size | 消息大小 |
接收消息函数 rt_mq_recv()
rt_err_t ·
rt_mq_recv
(rt_mq_t mq, void* buffer, rt_size_t size, rt_int32_t timeout);
只有当消息队列不为空时才能接收消息,否则线程接收消息时会被挂起,等待时间由timeout
参数决定。
参数 | 描述 |
---|---|
mq | 消息队列对象的句柄 |
buffer | 消息内容 |
size | 消息大小 |
timeout | 指定的超时时间 |
删除队列 rt_mq_delete()
rt_err_t
rt_mq_delete
(rt_mq_t mq)
删除消息队列时,如果有线程被挂起在消息队列等待队列上,则内核先唤醒挂在该消息等待队列上的所有线程,然后再释放消息队列使用的内存,最后删除消息队列对象。
参数 | 描述 |
---|---|
mq | 消息队列对象的句柄 |
消息队列实验
要使用消息队列,需要先在rtconfig.h
中开启该功能。
此实验参考原文对应实验代码,只包含
man()
函数,外设初始化相关代码下面未给出。
和上一节的实验不同,上一个实验按键线程的优先级一定要比LED线程高(不然按键响应速度会受影响),但本实验的接收线程recv_thread基本处在挂起状态,所以send_thread(按键)线程可以和rend_thread同优先级(甚至低优先级)。
#include "board.h"
#include "rtthread.h"
// 定义线程控制块指针
static rt_thread_t recv_thread = RT_NULL;
static rt_thread_t send_thread = RT_NULL;
// 定义消息队列控制块
static rt_mq_t test_mq = RT_NULL;
/******************************************************************************
* @ 函数名 : recv_thread_entry
* @ 功 能 : 消息接收线程入口函数
* @ 参 数 : parameter 外部传入的参数
* @ 返回值 : 无
******************************************************************************/
static void recv_thread_entry(void *parameter)
{
rt_err_t uwRet = RT_EOK;
uint32_t recv_queue; // 接收数据保存的位置
while(1)
{
// 队列接收,等待方式为一直阻塞等待
uwRet = rt_mq_recv(test_mq, &recv_queue, sizeof(recv_queue),
RT_WAITING_FOREVER);
if(RT_EOK == uwRet)
{
rt_kprintf("recv_thread 接收到的数据为%d.\n", recv_queue);
}
else
{
rt_kprintf("recv_thread 接收数据出错!\n");
}
rt_thread_delay(200);
}
}
/******************************************************************************
* @ 函数名 : send_thread_entry
* @ 功 能 : 消息发送线程入口函数
* @ 参 数 : parameter 外部传入的参数
* @ 返回值 : 无
******************************************************************************/
static void send_thread_entry(void *parameter)
{
rt_err_t uwRet = RT_EOK;
uint32_t send_data1 = 1;
uint32_t send_data2 = 2;
while(1)
{
// KEY0 被按下
if(Key_Scan(KEY0_GPIO_PORT, KEY0_GPIO_PIN) == KEY_ON)
{
// 将data1发送到消息队列
uwRet = rt_mq_send(test_mq, &send_data1, sizeof(send_data1));
if(uwRet != RT_EOK)
{
rt_kprintf("send_thread 发送数据失败--data1.\n");
}
}
// WK_UP 被按下
if(Key_Scan(WK_UP_GPIO_PORT, WK_UP_GPIO_PIN) == KEY_ON)
{
{
// 将data2发送到消息队列
uwRet = rt_mq_send(test_mq, &send_data2, sizeof(send_data2));
if(uwRet != RT_EOK)
{
rt_kprintf("send_thread 发送数据失败--data2.\n");
}
}
}
rt_thread_delay(20);
}
}
int main(void)
{
// 硬件初始化和RTT的初始化已经在component.c中的rtthread_startup()完成
// 创建一个队列
test_mq = // 消息队列控制块指针
rt_mq_create("test_mq", // 消息队列名字
50, // 消息队列最大长度(单条消息)
20, // 消息队列最大容量(消息数量)
RT_IPC_FLAG_FIFO); // FIFO队列模式(先进先出)
if(test_mq != RT_NULL)
rt_kprintf("消息队列创建成功!\n");
// 创建一个动态线程
recv_thread = // 线程控制块指针
rt_thread_create("recv", // 线程名字
recv_thread_entry, // 线程入口函数
RT_NULL, // 入口函数参数
255, // 线程栈大小
5, // 线程优先级
10); // 线程时间片
// 开启线程调度
if(recv_thread != RT_NULL)
rt_thread_startup(recv_thread);
else
return -1;
// 创建一个动态线程
send_thread = // 线程控制块指针
rt_thread_create("send", // 线程名字
send_thread_entry, // 线程入口函数
RT_NULL, // 入口函数参数
255, // 线程栈大小
5, // 线程优先级
10); // 线程时间片
// 开启线程调度
if(send_thread != RT_NULL)
rt_thread_startup(send_thread);
else
return -1;
}
实验现象
按键KEY0
按下时,send_thread线程发送data1,然后recv_thread线程打印接收到的数据;按键WK_UP
按下时,send_thread线程发送data2,接着recv_thread线程打印接收到的数据。