RT-Thread记录(七、IPC机制之邮箱、消息队列)

举报
矜辰所致 发表于 2022/09/21 21:34:45 2022/09/21
【摘要】 讲完了线程同步的机制,我们要开始线程通讯的学习, 线程通讯中的邮箱消息队列也属于 RT-Thread 的IPC机制。
讲完了线程同步的机制,我们要开始线程通讯的学习,
线程通讯中的邮箱消息队列也属于 RT-Thread 的IPC机制。

前言

与上篇文章的介绍的信号量、互斥量和事件集,邮箱、消息队列同样为 RT-Thread IPC机制。但是信号量它们属于线程同步机制,并不能在线程之间传递消息,我们本文介绍的 邮箱、消息队列就是实现线程间消息传递的机制。

相对于上一篇文章的内容,线程通讯的学习会相对复杂些,因为涉及到消息的传递,消息在实际项目中的可能存在多种不同的情况,所以 邮箱和消息队列的使用场景和方式是关键,尤其是消息队列。基本上实际项目中的所有消息类型都可以使用消息队列的方式。消息队列应用于串口通信我会单独用一篇博文来说明,本文先做基础介绍和基本示例的讲解。

本 RT-Thread 专栏记录的开发环境:
RT-Thread记录(一、RT-Thread 版本、RT-Thread Studio开发环境 及 配合CubeMX开发快速上手)
RT-Thread记录(二、RT-Thread内核启动流程 — 启动文件和源码分析
RT-Thread 内核篇系列博文链接:
RT-Thread记录(三、RT-Thread 线程操作函数及线程管理与FreeRTOS的比较)
RT-Thread记录(四、RT-Thread 时钟节拍和软件定时器)
RT-Thread记录(五、RT-Thread 临界区保护)
RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)

一、邮箱

RT-Thread 中的邮件是线程、中断服务、定时器向线程发送消息的有效手段(中断和定时器需要非阻塞方式,不能等待发送,也不能接收)。

邮箱中的每一封邮件只能容纳固定的 4 字节内容(32位内核正好可以传递一个指针)。

邮箱特点 RAM空间占用少,效率较高。

RT-Thread 有点类似 FreeRTOS 的任务通知,同样的只能传递4个字节内容。
但是 FreeRTOS 的任务通知是属于任务自己的,每个任务有且只有一个通知,
而 RT-Thread 的邮箱由邮箱控制块统一管理,新建一个邮箱,可以包含多封邮件(每封4个字节)。

1.1 邮箱控制块

老规矩用源码,解释看注释(使用起来也方便复制 ~ ~!)

#ifdef RT_USING_MAILBOX
/**
 * mailbox structure
 */
struct rt_mailbox
{
    struct rt_ipc_object parent;             /**< inherit from ipc_object */
    rt_ubase_t          *msg_pool;           /**< 邮箱缓冲区的开始地址  */
    rt_uint16_t          size;               /**< 邮箱缓冲区的大小      */
    rt_uint16_t          entry;              /**< 邮箱中邮件的数目 */
    rt_uint16_t          in_offset;          /**< 邮箱缓冲的入口指针 */
    rt_uint16_t          out_offset;         /**< 邮箱缓冲的出口指针 */
    rt_list_t            suspend_sender_thread;   /**< 发送线程的挂起等待队列 */
};
typedef struct rt_mailbox *rt_mailbox_t;
#endif

1.2 邮箱操作

1.2.1 创建和删除

同以前的线程那些一样,动态的方式,先定义一个邮箱结构体的指针变量,接收创建好的句柄。

创建邮箱:

/**
参数的含义:
1、name 		邮箱名称
2、size			邮箱容量(就是多少封邮件,4的倍数)
3、flag 		邮箱标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回值:
RT_NULL 		创建失败
邮箱对象的句柄 	创建成功 
 */
rt_mailbox_t rt_mb_create(const char *name, rt_size_t size, rt_uint8_t flag)

最后的 flag 和信号量一样建议 RT_IPC_FLAG_PRIO
在这里插入图片描述
删除邮箱:

/**
参数的含义:
mb 	邮箱对象的句柄
返回
RT_EOK 	成功
 */
rt_err_t rt_mb_delete(rt_mailbox_t mb)

1.2.2 初始化和脱离

静态的方式,先定义一个邮箱结构体,然后对他进行初始化。

这里要注意,还要定义一个数组,用来做邮箱的内存空间,和静态初始化线程一样。

初始化邮箱:

/**
参数含义:
1、mb	 	邮箱对象的句柄,需要取自定义的结构体地址
2、name	 	邮箱名称
3、msgpool 	缓冲区指针(用户自定义的数组的地址,第一个数组元素的地址)
4、size 	邮箱容量(就是数组的大小/4)
5、flag 	邮箱标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回
RT_EOK 	成功
 */
rt_err_t rt_mb_init(rt_mailbox_t mb,
                    const char  *name,
                    void        *msgpool,
                    rt_size_t    size,
                    rt_uint8_t   flag)

脱离邮箱:

/**
参数的含义:
mb 	邮箱对象的句柄
返回
RT_EOK 	成功
 */
rt_err_t rt_mb_detach(rt_mailbox_t mb)

1.2.3 发送邮件

在 RT-Thread 中发送邮件分为 有无等待方式发送邮件,以及发送紧急邮件。

在我建的工程版本中,并没有发送紧急邮件函数了,这里按照工程源码来说明,就不介绍发送紧急邮件的函数了,在一般的 STM32 应用中,个人认为紧急邮件有没有都没有影响!

无等待方式适用于所有的线程和中断,等待方式不能用于中断中!

无等待发送邮件:

/**
参数:
1、mb	 	邮箱对象的句柄
2、value 	邮件内容
返回
RT_EOK 		发送成功
-RT_EFULL 	邮箱已经满了
看函数原型,其实就是把等待方式发送的时间改成了0
 */
rt_err_t rt_mb_send(rt_mailbox_t mb, rt_ubase_t value)
{
    return rt_mb_send_wait(mb, value, 0);
}

无等待发送其实就是使用等待方式发送邮件,等待时间为0:。

等待方式发送邮件:

/**
参数:
1、mb 		邮箱对象的句柄
2、value 	邮件内容
3、timeout 	超时时间
返回:
RT_EOK 			发送成功
-RT_ETIMEOUT 	超时
-RT_ERROR 		失败,返回错误
 */
rt_err_t rt_mb_send_wait(rt_mailbox_t mb,
                         rt_ubase_t   value,
                         rt_int32_t   timeout)

1.2.4 接收邮件

接收邮件时,除了指定接收邮件的邮箱句柄,并指定接收到的邮件存放位置(需要有一个变量来保存接收到的数据)。

/**
参数含义:
1、mb 		邮箱对象的句柄,从哪个邮件控制块取邮件
2、value 	邮件内容,需要用一个变量保存
3、timeout 	超时时间
返回值:
RT_EOK 	接收成功
-RT_ETIMEOUT 	超时
-RT_ERROR 	失败,返回错误
 */
rt_err_t rt_mb_recv(rt_mailbox_t mb, rt_ubase_t *value, rt_int32_t timeout)

1.3 示例(指针传递)

2个示例,第一个是正常的消息传递,第二个是与邮箱创建个数有关的引导示例。

1.3.1 邮箱消息传递

前面说到过,邮箱中的每一封邮件只能容纳固定的 4 字节内容,但是4字节可以传递指针,我们分别做个简单的演示。

示例中,我们使用两个不同的按键来发送邮件,通过一个事件来接收邮件,并打印收到的邮件内容。

按键key3,发送4字节的内容,按键Key2,发送一个字符串指针:
在这里插入图片描述

邮件创建:
在这里插入图片描述

在接收线程中,我们打印出接收到的数值:
在这里插入图片描述
测试结果,两个按键按下,线程不仅能收到直接传过来的4字节数据,还能通过传递的指针发送一个字符串:
在这里插入图片描述

1.3.2 邮箱个数示例

在上面的例子中,我们开始创建的邮箱大小就一个,我们测试下,如果没有线程接收,是不是就会打印邮箱满的消息,我们把线程接收邮箱代码注释掉,其他还是和前面测试一样:
在这里插入图片描述
我们再来改一下,使用一个按键测试一下这个 size 是字节呢,还是直接是邮件个数,直接看图说明:

在这里插入图片描述
在静态初始化邮件时候,我们需要注意我们开辟的空间大小,需要是4的倍数,我们一般都是用数组除以4直接表示邮箱的size大小,如下:
在这里插入图片描述

RT-Thread 是通过控制块来管理这些IPC机制,在实际测试中,为了加深对某个对象的理解,比如这里的邮箱,可以直接打印出邮箱的参数来查看当前邮箱的状态。学会测试!!!
在这里插入图片描述

二、消息队列

消息队列能够接收来自线程或中断服务例程中不固定长度的消息,并把消息缓存在自己的内存空间中。

消息队列和邮箱的区别是长度并不限定在 4 个字节以内,但是如果如果把消息队列的每条消息的最大字节规定在4个字节以内,那么消息队列就和邮箱一样了。

典型应用,使用串口接收不定长数据(后期会单独有博文介绍消息队列在串口接收上的应用)。

2.1 消息队列控制块

消息队列控制块的这些属性,我们等会用示例来打印出来看,加深一下对这些属性的认识。

#ifdef RT_USING_MESSAGEQUEUE
/**
 * message queue structure
 */
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;                /**< list head 链表头 */
    void                *msg_queue_tail;                /**< list tail 链表尾*/
    void                *msg_queue_free;                /**< 空闲消息链表 */

    rt_list_t            suspend_sender_thread;         /**< 挂起的发送线程 */
};
typedef struct rt_messagequeue *rt_mq_t;
#endif

2.2 消息队列操作

2.2.1 创建和删除

先定义一个邮箱结构体的指针变量,接收创建好的句柄。

创建消息队列:

/**
参数:
1、name 		消息队列的名称
2、msg_size 	消息队列中一条消息的最大长度,单位字节
3、max_msgs 	消息队列的最大个数
4、flag 		消息队列采用的等待方式,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回:
RT_EOK 				发送成功
消息队列对象的句柄 	成功
RT_NULL 			失败
 */
rt_mq_t rt_mq_create(const char *name,
                     rt_size_t   msg_size,
                     rt_size_t   max_msgs,
                     rt_uint8_t  flag)

注意!msg_size 单位是字节,在32位系统中 RT-Thread 默认#define RT_ALIGN_SIZE 4 ,所以如果 msg_size 不是4字节对齐,系统会自动补全。

比如用户定义为9,那么系统会自动把消息队列大小设置为 12,定义为1,设置为4。

还有flag的使用,依然得注意一下,和邮箱信号量等一样,注意实时性问题。

删除消息队列:

/**
参数
mq 		消息队列对象的句柄
返回
RT_EOK 	成功
 */
rt_err_t rt_mq_delete(rt_mq_t mq)

2.2.2 初始化和脱离

静态的方式,先定义一个消息队列结构体,然后对他进行初始化。

初始化消息队列:

/**
参数:
1、mq 			消息队列对象的句柄,需要取自定义的结构体地址
2、name 		名称
3、msgpool 		存放消息的地址
4、msg_size 	消息队列中一条消息的最大长度,单位字节
5、pool_size 	存放消息的缓冲区大小
6、flag 		消息队列采用的等待方式,
返回:
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 		消息队列对象的句柄
返回:
RT_EOK 	成功
 */
rt_err_t rt_mq_detach(rt_mq_t mq)

2.2.3 发送消息

和邮件一样,在 RT-Thread 中发送邮件分为 有无等待方式发送,以及紧急消息发送。

无等待方式适用于所有的线程和中断,等待方式不能用于中断中!

无等待发送消息:

/**
看函数原型,其实就是把等待方式发送的时间改成了0
参数:
1、mq 		消息队列对象的句柄
2、buffer 	消息内容
3、size 	消息大小
返回:
RT_EOK 		成功
-RT_EFULL 	消息队列已满
-RT_ERROR 	失败,表示发送的消息长度大于消息队列中消息的最大长度
 */
rt_err_t rt_mq_send(rt_mq_t mq, const void *buffer, rt_size_t size)
{
    return rt_mq_send_wait(mq, buffer, size, 0);
}

等待方式发送邮件:

/**
除了最后多一个时间,其他参数,和上面无等待方式一样
timeout 	超时时间(时钟节拍)
*/
rt_err_t rt_mq_send_wait(rt_mq_t     mq,
                         const void *buffer,
                         rt_size_t   size,
                         rt_int32_t  timeout)

发送紧急消息:

/**
参数:
1、mq 		消息队列对象的句柄
2、buffer 	消息内容
3、size 	消息大小
返回:
RT_EOK 		成功
-RT_EFULL 	消息队列已满
-RT_ERROR 	失败
 */
rt_err_t rt_mq_urgent(rt_mq_t mq, const void *buffer, rt_size_t size)

2.2.4 接收消息

接收消息时,接收者需指定存储消息的消息队列对象句柄,并且指定一个内存缓冲区,接收到的消息内容将被复制到该缓冲区里。

/**
参数:
mq 				消息队列对象的句柄
buffer 			消息内容
size 			消息大小
timeout 		指定的超时时间
返回:
RT_EOK 			成功收到
-RT_ETIMEOUT 	超时
-RT_ERROR 		失败,返回错误
 */
rt_err_t rt_mq_recv(rt_mq_t    mq,
                    void      *buffer,
                    rt_size_t  size,
                    rt_int32_t timeout)

2.3 消息队列原理简析

消息队列控制块:

要理解 消息队列 的原理,就得从他初始化的状态开始说起:
在这里插入图片描述

发送消息,其实所有的步骤都是在rt_mq_send_wait函数中的,再次强调,学会看源码!

关键的几个地方说明一下:
在这里插入图片描述
当然这里没有特意的说明等待时间问题,因为发送和接收都可以阻塞等待,这里不是要理解的重点。
在这里插入图片描述

发送完完成以后如果发现有线程在等待消息队列,会发生一次调度:
在这里插入图片描述
接收消息,其实类似,可以自己查看源码,试着分析。

对于上述过程的理解,我单独写了个例子,结合例子去理解上面的步骤,更加直观!请看下面 理解消息队列原理示例。

2.4 示例(消息队列原理理解)

2个示例,第一个为了更加直观的理解消息队列原理,第二个是简单的消息传递。

对于典型的串口接收不定长度数据的示例,我会单独使用一篇文章来介绍。

2.4.1 理解消息队列原理

我们在上面 《2.3 消息队列原理简析》 分析了一下消息队列的原理,我们再来通过一个例子直观的加深一下理解。

新建一个消息队列(注意新建时候的参数):
在这里插入图片描述

我们2个按键,通过Key2按键发送消息:
在这里插入图片描述
通过 Key3 打印 消息队列 对应的状态值:
在这里插入图片描述
我们测试的时候,通过观察消息队列初始化以后的状态,然后每次发送以后观察 head,tail,free的变化情况,加深我们对消息队列的理解:

在这里插入图片描述

通过上面的示例再去理解消息队列的原理,就很直观了,如果有消息接收,观察地址的变化,同样的可以分析出接收消息时候的原理。

2.4.2 消息传递

消息传递相对来说,就简单多了,直接在上面的基础上,新建一个任务接收消息(因为没有做长度识别,这里没有做解析):
在这里插入图片描述
还是通过上面的Key2按键发送消息:
在这里插入图片描述

结语

本文虽然只介绍了2个IPC机制,但是在项目中,它们的使用无处不在。

消息队列的应用在我们实际使用中,是很重要的,串口通信接收数据就是使用消息队列来实现。对于消息队列的串口应用,我会单独开一片博文来总结。

本文针对消息队列的实现原理给出了很好的示例,还是那句话,学会多看源码,多动手测试!

博主会用心写好每一篇博文,希望大家支持!谢谢!

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。