RT-Thread 消息队列(学习笔记)

本文参考自[野火EmbedFire]《RT-Thread内核实现与应用开发实战——基于STM32》,仅作为个人学习笔记。更详细的内容和步骤请查看原文(可到野火资料下载中心下载)

消息队列的基本概念

消息队列是一种常用的线程间通讯方式,它能够接收来自线程或中断服务例程中不固定长度的消息,并把消息缓存在自己的内存空间中。其他线程也能够从消息队列中读取相应的消息,而当消息队列是空的时候,可以挂起读取线程。当有新的消息到达时,挂起的线程将被唤醒以接收并处理消息。消息队列是一种异步的通信方式。

——RT-Thread官方中文手册

通过消息队列服务,线程或中断服务例程可以将一条或多条消息放入消息队列中。同样,一个或多个线程可以从消息队列中获得消息。当有多个消息发送到消息队列时,通常是将先进入消息队列的消息先传给线程,也就是说,线程先得到的是最先进入消息队列的消息,即先进先出原则(FIFO)。同时 RT- Thread 中的消息队列支持优先级,也就是说在所有等待消息的线程中优先级最高的会先获得消息。

RT-Thread 中使用队列数据结构实现线程异步通信工作,具有如下特性:

  • 消息支持先进先出方式排队与优先级排队方式,支持异步读写工作方式。
  • 读队列支持超时机制。
  • 支持发送紧急消息,这里的紧急消息是往队列头发送消息。
  • 可以允许不同长度(不超过队列节点最大值)的任意类型消息。
  • 一个线程能够从任意一个消息队列接收和发送消息。
  • 多个线程能够从同一个消息队列接收和发送消息。
  • 当队列使用结束后,需要通过删除队列操作释放内存函数回收

——原文

消息队列的运作机制

如图消息队列的工作示意图所示,通过消息队列服务,线程或中断服务例程可以将一条或多条消息放入消息队列中。同样,一个或多个线程可以从消息队列中获得消息。当有多个消息发送到消息队列时,通常应将先进入消息队列的消息先传给线程,也就是说,线程先得到的是最先进入消息队列的消息,即先进先出原则(FIFO)。

——RT-Thread官方中文手册

图片来源:RT-Thread官方中文手册

在这里插入图片描述

消息队列的阻塞机制

消息队列并不属于某个线程,所以多个线程在对消息队列进行操作时,要保护每个线程对队列的读写,不过操作系统已经有这种保护操作了,叫做消息队列的阻塞机制。

每个对消息队列进行读写操作的函数,都自带阻塞机制。当一个线程读到空队列时,可以有3种选择:

  1. 不等待消息,直接跳过读取;
  2. 等待一定时间,等到则进入就绪态,超时则进入阻塞态;
  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线程打印接收到的数据。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_43772810/article/details/123689088