RT-Thread线程间通信
裸机编程中,经常会使用全局变量进行功能间的通信,如某些功能可能由于一些操作而改变全局变量的值,另一个功能对此全局变量进行读取,根据读取到的全局变量值执行相应的动作,达到通信协作的目的。
邮箱
邮箱实际上就是一个环形缓冲区,你可以把线程1需要发送给线程2的数据以邮件形式存入邮箱,线程2读取邮件,拿到数据进行处理。
现在一些新的业务场景正在淡化邮箱机制,转向消息队列,但其实邮箱是没有被取代的,它最擅长的地方还是高频写入 + 低频读取 + 只关心最新值的场景。只不过,它点对点,单次消费,这种通信特性不利于构建大型的特别是做群控需求的通信网络,现在是总线发布订阅以及FreeRTOS的任务通知机制更占风头。
邮箱的工作机制
邮箱开销比较低,效率较高。典型的邮箱也称作交换消息。
邮箱中的每一封邮件只能容纳固定的 4 字节内容(针对 32 位处理系统,指针的大小即为 4 个字节,所以一封邮件恰好能够容纳一个指针)。
这两个偏移量本质上就是环形缓冲区的“写指针”和“读 指针”。假设你创建了一个容量为 8 的邮箱。
msg_pool:就是这 8 个格子的起始地址,每个格子恰好放一封邮件(4字节)。
in_offset:“新邮件该投进哪个格子”的指针。每发一封邮件,就往这个格子写,然后指针移到下一格。
out_offset:“下一封该取哪封邮件”的指针。每收一封邮件,就从当前格子取,然后指针移到下一格。
entry:当前邮箱里躺着多少封未读邮件。
当指针移到 8(末尾)时,下一个动作会直接回到 0。这就是“环形”。
非阻塞方式的邮件发送过程能够安全的应用于中断服务中,是线程、中断服务、定时器向线程发送消息的有效手段。
通常来说,邮件收取过程可能是阻塞的,这取决于邮箱中是否有邮件,以及收取邮件时设置的超时时间。当邮箱中不存在邮件且超时时间不为 0 时,邮件收取过程将变成阻塞方式。在这类情况下,只能由线程进行邮件的收取。
当一个线程向邮箱发送邮件时,如果邮箱没满,将把邮件复制到邮箱中。
如果邮箱已经满了,发送线程可以设置超时时间,选择等待挂起或直接返回 - RT_EFULL。如果发送线程选择挂起等待,那么当邮箱中的邮件被收取而空出空间来时,等待挂起的发送线程将被唤醒继续发送。
当一个线程从 邮箱中接收邮件时,如果邮箱是空的,接收线程可以选择是否等待挂起直到收到新的邮件而唤醒,或可以设置超时时间。
当达到设置的超时时间,邮箱依然未收到邮件时,这个选择超时等待的线程将被唤醒并返回 - RT_ETIMEOUT。如果邮箱中存在邮件,那么接收线程将复制邮箱中的 4 个字节邮件到接收缓存中。
邮箱的管理方式
邮箱控制块(struct rt_mailbox)是 RT-Thread 用于管理邮箱的数据结构,由 IPC 容器管理。邮箱采用32 位消息传递方式,效率高于信号量队列,适用于小数据量的实时通信。
邮箱控制块结构定义如下:
struct rt_mailbox
{
struct rt_ipc_object parent; /* 继承自 IPC 对象 */
rt_uint32_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;
创建和删除邮箱(动态)
动态创建一个邮箱对象,系统会分配邮箱控制块和邮件缓冲池。
/**
* @brief 创建并初始化一个邮箱。
*
* @param name 邮箱的名称,用于调试和对象查找。
* @param size 邮箱的容量,即最多可容纳的邮件数目。
* @param flag 邮箱的等待队列排序方式:
* - RT_IPC_FLAG_FIFO (0x00):先进先出,非实时调度;
* - RT_IPC_FLAG_PRIO (0x01):优先级等待,高优先级线程优先唤醒(推荐)。
*
* @return 成功时返回指向邮箱控制块的指针(rt_mailbox_t 类型);
* 失败时返回 RT_NULL,通常由于系统内存不足或 flag 非法导致。
*
* @note 1. 该函数不能在中断服务例程(ISR)中调用。
* 2. 创建的邮箱对象在系统堆中动态分配内存,邮件缓冲池大小 = size * 4 字节。
* 3. 不再使用时必须通过 rt_mb_delete() 删除并释放内存。
* 4. RT_IPC_FLAG_FIFO 属于非实时调度方式,除非应用程序非常在意先来后到,
* 并且你清楚地明白所有涉及到该邮箱的线程都将会变为非实时线程,
* 方可使用,否则建议采用 RT_IPC_FLAG_PRIO。
*
* @see rt_mb_delete()
*/
rt_mailbox_t rt_mb_create(const char *name, rt_size_t size, rt_uint8_t flag);
删除动态创建的邮箱,释放系统资源:
rt_err_t rt_mb_delete(rt_mailbox_t mb);
初始化和脱离邮箱(静态)
静态邮箱对象的内存由用户在编译时分配,使用前需进行初始化。
/**
* @brief 初始化静态邮箱对象。
*
* @param mb 邮箱对象的句柄,指向用户已分配的邮箱控制块内存。
* @param name 邮箱的名称。
* @param msgpool 指向用户分配的邮件缓冲池,缓冲池大小必须为 size * 4 字节。
* @param size 邮箱容量(邮件个数),即缓冲池能存放的邮件数目。
* @param flag 邮箱的等待队列排序方式,取值同 rt_mb_create()。
*
* @return rt_err_t 执行结果:
* @retval RT_EOK (0) : 初始化成功
* @retval -RT_ERROR (1) : 初始化失败(参数为空或非法)
*
* @note 1. 该函数不能在中断服务例程(ISR)中调用。
* 2. 静态邮箱不使用堆内存,必须由用户提供邮箱控制块和邮件缓冲池的内存空间。
* 3. 不再使用时,需通过 rt_mb_detach() 将其从内核对象管理器中脱离。
*
* @see rt_mb_detach()
*/
rt_err_t rt_mb_init(rt_mailbox_t mb,
const char *name,
void *msgpool,
rt_size_t size,
rt_uint8_t flag);
脱离静态初始化的邮箱,将其从内核对象管理器中移除 :
rt_err_t rt_mb_detach(rt_mailbox_t mb);
发送邮件
线程或中断服务程序可以通过邮箱给其他线程发送邮件,邮件内容为 32 位任意格式的数据(整型或指针)。
/**
* @brief 发送一封邮件到邮箱尾部。
*
* @param mb 邮箱对象的句柄。
* @param value 邮件内容(32 位)。
*
* @return rt_err_t 执行结果:
* @retval RT_EOK (0) : 发送成功
* @retval -RT_EFULL (-4) : 邮箱已满,发送失败
*
* @note 1. 该函数**可以**在中断服务例程(ISR)中调用。
* 2. 当邮箱已满时,函数立即返回 -RT_EFULL,不会阻塞。
* 3. 成功发送后,邮件放入缓冲区尾部,并唤醒等待接收邮件的第一个线程。
*
* @see rt_mb_send_wait()
* @see rt_mb_urgent()
*/
rt_err_t rt_mb_send(rt_mailbox_t mb, rt_uint32_t value);
等待方式发送邮件
当邮箱满时,发送线程可选择等待直到缓冲区有空闲位置。
/**
* @brief 以等待方式发送邮件。
*
* @param mb 邮箱对象的句柄。
* @param value 邮件内容(32 位)。
* @param timeout 超时时间(单位:系统时钟节拍):
* - RT_WAITING_FOREVER : 永久等待
* - RT_WAITING_NO : 不等待,立即返回
* - 其他正整数 : 最大等待节拍数
*
* @return rt_err_t 执行结果:
* @retval RT_EOK (0) : 发送成功
* @retval -RT_ETIMEOUT (2) : 超时未空出空间
* @retval -RT_ERROR (1) : 发送失败(如邮箱被删除)
*
* @note 1. 该函数**不能**在中断服务例程(ISR)中调用(可能引起阻塞)。
* 2. 与 rt_mb_send() 的区别:当邮箱已满时,发送线程将根据 timeout 参数
* 挂起等待,直到邮箱有空闲位置或超时。
*
* @see rt_mb_send()
*/
rt_err_t rt_mb_send_wait(rt_mailbox_t mb,
rt_uint32_t value,
rt_int32_t timeout);
发送紧急邮件
紧急邮件将被直接插入到邮箱缓冲区的队首,从而被接收者优先处理。
/**
* @brief 发送一封紧急邮件到邮箱队首。
*
* @param mb 邮箱对象的句柄。
* @param value 邮件内容(32 位)。
*
* @return rt_err_t 执行结果:
* @retval RT_EOK (0) : 发送成功
* @retval -RT_EFULL (-4) : 邮箱已满,发送失败
*
* @note 1. 该函数**可以**在中断服务例程(ISR)中调用。
* 2. 与 rt_mb_send() 的唯一区别:邮件被放入缓冲区的**队首**,
* 因此等待接收的线程会优先获取这封紧急邮件。
* 3. 当邮箱已满时,同样立即返回 -RT_EFULL,不阻塞。
*
* @see rt_mb_send()
*/
rt_err_t rt_mb_urgent(rt_mailbox_t mb, rt_ubase_t value);
接收邮件
线程从指定邮箱中接收一封邮件,若邮箱为空则根据超时设置挂起或立即返回。
/**
* @brief 接收一封邮件。
*
* @param mb 邮箱对象的句柄。
* @param value 指向接收邮件内容的指针,函数将把邮件值写入该地址。
* @param timeout 超时时间(单位:系统时钟节拍):
* - RT_WAITING_FOREVER : 永久等待
* - RT_WAITING_NO : 不等待,立即返回
* - 其他正整数 : 最大等待节拍数
*
* @return rt_err_t 执行结果:
* @retval RT_EOK (0) : 接收成功
* @retval -RT_ETIMEOUT (2) : 超时未收到邮件
* @retval -RT_ERROR (1) : 接收失败(如邮箱被删除或参数无效)
*
* @note 1. 该函数**不能**在中断服务例程(ISR)中调用(可能引起阻塞)。
* 2. 接收成功时,邮件从缓冲区队首移除,entry 计数减 1。
* 3. 若 此时有挂起的发送线程在等待空闲位置,会唤醒其中一个。
*
* @see rt_mb_send()
*/
rt_err_t rt_mb_recv(rt_mailbox_t mb, rt_uint32_t *value, rt_int32_t timeout);
消息队列
消息队列是一 种异步的通信方式, 是邮箱的扩展。可以应用在多种场合:线程间的消息交换、使用串口接收不定长数据等。
消息队列的工作机制
消息队列遵循先进先出原则 (FIFO)。
想象消息队列是一个外卖柜。
-
创建队列 = 放置了一个有 10 个格子的外卖柜。
-
发送消息 = 往任意一个空格子里塞一份外卖。
-
接收消息 = 从格子里取走外卖。
-
挂起/唤醒 = 外卖柜是空的,你来取外卖,没取到,你就坐在外卖柜旁边等着(挂起)。一会儿有人塞了外卖,你立刻醒来拿走(唤醒)。
-
先进先出 = 塞进去的第1份外卖,会最早被取走。(这一点可能不太符合外卖柜这个概念,是队列,能理解就好)
假设你创建了一个消息队列,长度是 4(有 4 个格子)。
-
初始状态:4 个格子 全在空闲区。待取区是空的。
-
线程A 发消息“1”:从空闲区拿 1 号格子,写进“1”,挂到待取区队尾。此时:待取区 [1],空闲区 [2,3,4]。
-
线程B 发消息“2”:从空闲区拿 2 号格子,写进“2”,挂到待取区队尾。此时:待取区 [1,2],空闲区 [3,4]。
-
线程C 收消息:从待取区队头取 1 号格子,读走“1”,清空,把 1 号格子放回空闲区队尾。此时:待取区 [2],空闲区 [3,4,1]。
-
线程D 收消息:队列不为空,直接取走 2 号格子。此时:待取区 [],空闲区 [3,4,1,2]。
所有消息队列中的消息框总数即是消息队列的长度,就是这个意思。
发消息的线程,也就是外卖员不需要在场,他塞了外卖之后只需要继续做自己的事情,你来拿就好。
RT-Thread 操作系统的消息队列对象由多个元素组成,当消息队列被创建时,它就被分配了消息队列控制块:消息队列名称、内存缓冲区、消息大小以及队列长度等。同时每个消息队列对象中包含着多个消息框,每个消息框可以存放一条消息;消息队列中的第一个和最后一个消息框被分别称为消息链表头和消息链表尾,对应于消息队列控制块中的 msg_queue_head 和 msg_queue_tail;有些消息框可能是空的,它们通过 msg_queue_free 形成一个空闲消息框链表。所有消息队列中的消息框总数即是消息队列的长度,这个长度可在消息队列创建时指定。