讲完了线程同步的机制,我们要开始线程通讯的学习,
线程通讯中的邮箱消息队列也属于 RT-Thread 的IPC机制。
目录
前言
一、邮箱
1.1 邮箱控制块
1.2 邮箱操作
1.2.1 创建和删除
1.2.2 初始化和脱离
1.2.3 发送邮件
1.2.4 接收邮件
1.3 示例(指针传递)
1.3.1 邮箱消息传递
1.3.2 邮箱个数示例
二、消息队列
2.1 消息队列控制块
2.2 消息队列操作
2.2.1 创建和删除
2.2.2 初始化和脱离
2.2.3 发送消息
2.2.4 接收消息
2.3 消息队列原理简析
2.4 示例(消息队列原理理解)
2.4.1 理解消息队列原理
2.4.2 消息传递
结语
与上篇文章的介绍的信号量、互斥量和事件集,邮箱、消息队列同样为 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个字节)。
老规矩用源码,解释看注释(使用起来也方便复制 ~ ~!)
#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、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、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)
在 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、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)
2个示例,第一个是正常的消息传递,第二个是与邮箱创建个数有关的引导示例。
前面说到过,邮箱中的每一封邮件只能容纳固定的 4 字节内容,但是4字节可以传递指针,我们分别做个简单的演示。
示例中,我们使用两个不同的按键来发送邮件,通过一个事件来接收邮件,并打印收到的邮件内容。
按键key3,发送4字节的内容,按键Key2,发送一个字符串指针:
邮件创建:
在接收线程中,我们打印出接收到的数值:
测试结果,两个按键按下,线程不仅能收到直接传过来的4字节数据,还能通过传递的指针发送一个字符串:
在上面的例子中,我们开始创建的邮箱大小就一个,我们测试下,如果没有线程接收,是不是就会打印邮箱满的消息,我们把线程接收邮箱代码注释掉,其他还是和前面测试一样:
我们再来改一下,使用一个按键测试一下这个 size 是字节呢,还是直接是邮件个数,直接看图说明:
在静态初始化邮件时候,我们需要注意我们开辟的空间大小,需要是4的倍数,我们一般都是用数组除以4直接表示邮箱的size
大小,如下:
RT-Thread 是通过控制块来管理这些IPC机制,在实际测试中,为了加深对某个对象的理解,比如这里的邮箱,可以直接打印出邮箱的参数来查看当前邮箱的状态。学会测试!!!
消息队列能够接收来自线程或中断服务例程中不固定长度的消息,并把消息缓存在自己的内存空间中。
消息队列和邮箱的区别是长度并不限定在 4 个字节以内,但是如果如果把消息队列的每条消息的最大字节规定在4个字节以内,那么消息队列就和邮箱一样了。
典型应用,使用串口接收不定长数据(后期会单独有博文介绍消息队列在串口接收上的应用)。
消息队列控制块的这些属性,我们等会用示例来打印出来看,加深一下对这些属性的认识。
#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
先定义一个邮箱结构体的指针变量,接收创建好的句柄。
创建消息队列:
/**
参数:
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)
静态的方式,先定义一个消息队列结构体,然后对他进行初始化。
初始化消息队列:
/**
参数:
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)
和邮件一样,在 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)
接收消息时,接收者需指定存储消息的消息队列对象句柄,并且指定一个内存缓冲区,接收到的消息内容将被复制到该缓冲区里。
/**
参数:
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)
消息队列控制块:
要理解 消息队列 的原理,就得从他初始化的状态开始说起:
发送消息,其实所有的步骤都是在rt_mq_send_wait
函数中的,再次强调,学会看源码!
关键的几个地方说明一下:
当然这里没有特意的说明等待时间问题,因为发送和接收都可以阻塞等待,这里不是要理解的重点。
发送完完成以后如果发现有线程在等待消息队列,会发生一次调度:
接收消息,其实类似,可以自己查看源码,试着分析。
对于上述过程的理解,我单独写了个例子,结合例子去理解上面的步骤,更加直观!请看下面 理解消息队列原理示例。
2个示例,第一个为了更加直观的理解消息队列原理,第二个是简单的消息传递。
对于典型的串口接收不定长度数据的示例,我会单独使用一篇文章来介绍。
我们在上面 《2.3 消息队列原理简析》 分析了一下消息队列的原理,我们再来通过一个例子直观的加深一下理解。
新建一个消息队列(注意新建时候的参数):
我们2个按键,通过Key2按键发送消息:
通过 Key3 打印 消息队列 对应的状态值:
我们测试的时候,通过观察消息队列初始化以后的状态,然后每次发送以后观察 head,tail,free的变化情况,加深我们对消息队列的理解:
通过上面的示例再去理解消息队列的原理,就很直观了,如果有消息接收,观察地址的变化,同样的可以分析出接收消息时候的原理。
消息传递相对来说,就简单多了,直接在上面的基础上,新建一个任务接收消息(因为没有做长度识别,这里没有做解析):
还是通过上面的Key2按键发送消息:
本文虽然只介绍了2个IPC机制,但是在项目中,它们的使用无处不在。
消息队列的应用在我们实际使用中,是很重要的,串口通信接收数据就是使用消息队列来实现。对于消息队列的串口应用,我会单独开一片博文来总结。
本文针对消息队列的实现原理给出了很好的示例,还是那句话,学会多看源码,多动手测试!
我会用心写好每一篇博文,希望大家支持!谢谢!
审核编辑:汤梓红
全部0条评论
快来发表一下你的评论吧 !