目录
消息队列的本质是链表:
- 空闲消息链表,往队列里写入消息时,先从空闲链表中得到消息块;从队列读出消息后,把消息块放入空闲链表中
- 消息块头部链表:消息写入消息块后,该消息块被放到尾部;从队列里读出消息时,从头部读
- 使用消息队列可以传递各类大小的消息,它使用memcpy的方式写入消息,读出消息,如果我们只是传递很小的数据,比如一些数值,可以使用邮箱;它的效率更高。
邮箱的基本概念和特性
邮箱的基本概念
邮箱在操作系统中是一种常用的IPC通信方式,邮箱可以在线程与线程之间,中断与线程之间进行消息的传递,此外邮箱相比于信号量与消息队列来说,其开销更低,效率更高。所以常用来做线程与线程之间、中断与线程之间的通信。邮箱中的每一封邮件只能容纳固定的4字节的内容(STM32是32位处理系统,一个指针的大小即为4字节,所以一封邮件恰好能够容纳一个指针)。当需要在线程间传递较大的消息时,可以把指向一个缓冲区的指针作为邮件发送到邮箱中。
线程能够从邮箱中读取消息,当邮箱中的邮件是空的时,根据用户自定义的阻塞时间决定是否挂起线程。当邮箱中有新邮件时,挂起的读取线程被唤醒,邮箱也是一种异步通信方式。通过邮箱,线程或中断服务函数可以将一个或多个邮件放入邮箱中,同样,一个或者多个线程可以从邮箱中获得邮件消息,当有多个邮件发送到邮箱时,通常应将先进入邮箱的邮件先传给线程,也就是说,线程先得到的是最先进入邮箱的消息,即先进先出原则(FIFO),同时RT-Thread中的邮箱支持优先级,也就是说在所有等待邮件的线程中优先级最高的会先获得邮件。
RT-Thread中使用邮箱实现异步通信工作,具有如下特性:
- 邮件支持先进先出方式排队与优先级排队方式,支持异步读写工作方式
- 发送与接收邮件均支持超时机制
- 一个线程能够从任意一个消息队列中接收和发送邮件
- 多个线程能够向同一个邮箱发送邮件和从中接收邮件
- 邮箱中的每一封邮件只能容纳固定的4字节长度内容(可以存放缓冲区的地址)
- 当队列使用结束后,需要通过删除邮箱以释放内存。
邮箱与消息队列很相似,消息队列中消息的长度是可以由用户配置的,但邮箱中邮件的大小却只能是固定容纳4字节的内容,所以使用邮箱的开销是很小的,因为传递的只能是4字节以内的内容,所以其效率会更高。
邮箱的特性
邮箱的本质是环形缓冲区,和消息队列的本质是不一样的,虽然说邮箱相当于是消息队列的一种特殊情形。
- 邮箱的每一封邮件,只能容纳4字节内容,对于32位系统指针刚好为4字节
- 邮件的 发送通常是非阻塞的,线程中断都可以发送邮件,也可使用阻塞方式发送。
- 邮件的接收通常是阻塞的,取决于邮箱中是否有邮件
- 当一个线程向邮箱发送邮件,如果邮箱没满,就把数值写入邮箱中,如果邮箱满了,发送线程可以直接返回-RT_EFULL,也可以挂起一段时间,在挂起期间,别的线程或者中断读了邮箱会唤醒挂起的线程;同理,接收邮件也是如此,可以不等待也可以等待阻塞
邮箱的运作机制
创建邮箱对象时会先创建一个邮箱对象控制块,然后给邮箱分配一块内存空间用来存放邮件,这块内存的大小等于邮件大小(4字节)与邮箱容量的乘积,接着初始化接收邮件和发送邮件在邮箱中的偏移量,然后在初始化消息队列,此时消息队列为空。
RT-Thread操作系统的邮箱对象由多个元素组成,当邮箱被创建时,它就被分配了邮箱控制块;邮箱名称;邮箱缓冲区起始地址;邮箱大小等。同时,每个邮箱对象中包含着多个邮件框,每个邮件框可以存放一封邮件,所有邮箱中的邮件框总数既是邮箱的大小,这个大小可在邮箱创建时指定。
发送邮件
线程或者中断服务程序都可以给邮箱发送邮件,非阻塞方式的邮件发送过程能够安全地应用于中断服务中,中断服务函数、定时器向线程发送消息的有效手段,而阻塞方式的邮件发送只能应用线程中,当发送邮件时,当且仅当邮箱中还没满邮件的时候才能进行发送,如果邮箱已满,可以根据用户设定的等待时间进行等待,当邮箱中的邮件被收取而空出空间来时,等待挂起的发送线程将被唤醒继续发送的过程,当等待时间到了,还没有完成发送邮件,或者未设定等待时间,此时发送邮件失败,发送邮件的线程或者中断程序会收到一个错误码(-RT_EFULL),线程发送邮件可以带阻塞,但在中断中不能采用任何带阻塞的方式发送邮件。
接收邮件
接收邮件时,根据邮箱控制块中的entry判断队列是否接收邮件,如果邮箱的邮件非空,那么可以根据out_offset找到最先发送的邮箱中的邮件进行接收,在接收时如果邮箱为空,如果用户设置了等待超时时间,系统会将当前线程挂起,当达到设置的超时时间,邮箱依然未收到邮件,那么线程将被唤醒并返回-RT_ETIMEOUT。如果邮箱中存在邮件,那么接收线程将复制邮箱中的4个字节邮件到接收线程中,通常来说,邮件收取过程中可能是阻塞的,这取决于邮箱中是否有邮件,以及收取邮件时设置的超时时间。
当邮箱不再被使用时,应该删除它以释放系统资源,一旦操作完成,邮箱将被永久性的删除。
邮箱的运作机制具体见邮箱的发送和接收示意图
邮箱的应用场景
RT-Thread操作系统的邮箱可存放固定条数的邮件,邮箱容量在创建/初始化邮箱时设定,每个邮件大小为4字节,当需要在线程间传递比较大的消息时,可以把指向一个缓冲区的指针作为邮件发送到邮箱中。
与系统其他通信方式相比,邮箱的通信开销更低,效率更高。无论是什么消息,传递的都是4个字节的邮件,所以经常应用在众多领域中,另外其实现的发送/接收阻塞机制,能很好应用于线程与线程、中断与线程之间的通讯。
其实邮箱中,每封邮件的大小为4字节,在32位系统中,刚好能存放一个指针,所以邮箱也特别适合那种仅传地址的情况。
邮箱的应用技巧
其实很简单的,只是一个指针的传递。
struct msg
{
rt_uint8_t *data_ptr;
rt_uint32_t data_size;
};
对于这样一个结构体,其中包含了指向数据的指针data_ptr和数据块长度的变量data_size。当一个线程需要把这个消息发送给另外一个线程时,可以采用如下的方式进行操作。
struct msg *msg_ptr;
msg_ptr = (struct msg*)rt_malloc(sizeof(struct msg));
msg_ptr->data_ptr = ...; /* 指向相应的数据块地址 */
msg_ptr->data_size = len; /* 数据块的长度 */
/* 发送这个消息指针给 mb 邮箱 */
rt_mb_send(mb, (rt_uint32_t)msg_ptr);
申请结构体大小的内存空间,返回的指针指向了结构体,当结构体中的信息处理完,那么可以将指向结构体的指针作为邮件发送到邮箱中,而在接收邮件的线程中完成对结构体信息的读取操作,在完成操作后应当释放内存,因为收取过来的是指针,而msg_str是一个新分配出来的内存块,所以在接收线程处理完毕之后,需要释放相应的内存块。
struct msg* msg_ptr;
if (rt_mb_recv(mb, (rt_uint32_t*)&msg_ptr) == RT_EOK)
{
/* 在接收线程处理完毕后,需要释放相应的内存块 */
rt_free(msg_ptr);
}
邮箱控制块
struct rt_mailbox
{
struct rt_ipc_object parent; /**< inherit from ipc_object */
rt_ubase_t *msg_pool; /**< start address of message buffer */
rt_uint16_t size; /**< size of message pool */
rt_uint16_t entry; /**< index of messages in msg_pool */
rt_uint16_t in_offset; /**< input offset of the message buffer */
rt_uint16_t out_offset; /**< output offset of the message buffer */
rt_list_t suspend_sender_thread; /**< sender thread suspended on this mailbox */
};
邮箱相关接口
创建邮箱:邮箱的创建 有两种方法
- 动态分配内存:rt_mb_create();邮箱的内存在函数内部动态分配,分配的内存大小为邮件大小乘以邮箱容量
- 静态分配内存:rt_mb_init();邮箱的内存事先分配好,比如可以是数组
rt_mailbox_t rt_mb_create(const char *name, rt_size_t size, rt_uint8_t flag)
参数 | 说明 |
name | 邮箱名称 |
size | 邮箱容量 |
flag | 邮箱采用的等待方式,RT_IPC_FLAG_FIFO或者RT_IPC_FLAG_PRIO |
返回值 | 邮箱对象的句柄,成功:返回句柄,以后使用句柄来操作邮箱,RT_NULL:失败 |
rt_err_t rt_mb_init(rt_mailbox_t mb,
const char *name,
void *msgpool,
rt_size_t size,
rt_uint8_t flag);
参数 | 说明 |
mb | 邮箱对象的句柄 |
name | 邮箱名称 |
msgpool | 缓冲区指针 |
szie | 邮箱容量 |
flag | 邮箱采用的等待方式,RT_IPC_FLAG_FIFO或者RT_IPC_FLAG_PRIO |
返回值 | RT_EOK:成功,RT_NULL:失败 |
删除邮箱接口:删除和脱离
- 删除它:rt_mb_delete(),只能删除使用rt_mb_create()创建的邮箱
- 脱离它:rt_mb_detach(),只能脱离使用rt_mb_init()初始化的邮箱
删除邮箱时,如果有线程在等待该邮箱,则内核先唤醒这些线程(线程返回值是 -RT_ERROR),然后再释放邮箱使用的内存,最后删除邮箱对象。
rt_err_t rt_mb_detach(rt_mailbox_t mb)
rt_err_t rt_mb_delete(rt_mailbox_t mb)
(注意:个人觉得二者直接的差异就是一个删除之后,会释放内存,一个只是不能使用但是内存没有释放)
发送邮件接口:有三个发送邮件的函数(我在源码里只找到两个)
- rt_mb_send():发送邮件
- rt_mb_send_wait():等待方式发送邮件
- rt_mb_urgent():发送紧急邮件(没找到源码)
rt_err_t rt_mb_send(rt_mailbox_t mb, rt_ubase_t value)
rt_err_t rt_mb_send_wait(rt_mailbox_t mb,
rt_ubase_t value,
rt_int32_t timeout)
接收邮件接口:
rt_err_t rt_mb_recv(rt_mailbox_t mb, rt_ubase_t *value, rt_int32_t timeout)