文章目录
一、前言
- 本文源码摘自
v3.0.3,新版本丰富了不少功能,欢迎评论指出。
二、MQ 源码流程介绍
- rtthread 中 ipc 都继承自 rt_object,都以链表的形式呈现。
- mq 的管理结构体为 rt_messagequeue,msg_pool 即为分配的队列池。
- 在后面的内容中会了解到实际上有两个链表:rt_messagequeue 中
msg_queue_tail 和 msg_queue_tail表示的使用中链表,以及msg_pool 和 msg_queue_free表征的空闲队列链表。当需要发送消息时,从空闲链表取出一个节点然后放到使用中链表中;而在接收时,将节点重新放回空闲链表。
c
struct rt_messagequeue
{
struct rt_ipc_object parent; /**< inherit from ipc_object */
void *msg_pool; /**< start address of message queue */
rt_uint16_t msg_size; /**< message size of each message */
rt_uint16_t max_msgs; /**< max number of messages */
rt_uint16_t entry; /**< index of messages in the queue */
void *msg_queue_head; /**< list head */
void *msg_queue_tail; /**< list tail */
void *msg_queue_free; /**< pointer indicated the free node of queue */
};
typedef struct rt_messagequeue *rt_mq_t;
1、MQ 创建
- mq 的创建位于
rt_mq_create(), 首先申请管理结构体 rt_messagequeue, 然后初始化 ipc object 为循环链表,该链表用于存储等待该队列的线程。根据需求的 msg_size 和 max_msgs 进行队列池申请,需要留意的是,这里需要额外申请链表指针占用的空间。 - 初始化空闲消息队列链表,也就是 rt_mq_message:
将起始地址作为链表最后一个节点进行串联,free指针指向最后节点

c
rt_mq_t rt_mq_create(const char *name,
rt_size_t msg_size,
rt_size_t max_msgs,
rt_uint8_t flag)
{
/* allocate object */
mq = (rt_mq_t)rt_object_allocate(RT_Object_Class_MessageQueue, name);
...
/* init ipc object */
rt_ipc_object_init(&(mq->parent));
...
/* allocate message pool */
mq->msg_pool = RT_KERNEL_MALLOC((mq->msg_size + sizeof(struct rt_mq_message)) * mq->max_msgs);
...
/* init message empty list */
mq->msg_queue_free = RT_NULL;
for (temp = 0; temp < mq->max_msgs; temp ++)
{
head = (struct rt_mq_message *)((rt_uint8_t *)mq->msg_pool +
temp * (mq->msg_size + sizeof(struct rt_mq_message)));
head->next = mq->msg_queue_free;
mq->msg_queue_free = head;
}
}
2、发送消息
-
首先从空闲队列中获取 空闲的节点,更新相关指针,然后将 buf copy 到节点后边的地址中,这里需要注意,由于
rt_mq_message存储的是一个指针,即 4字节,因此(msg + 1)其实就是( (uint8_t*)msg + 4 )。另外,我们可以留意到,这里copy 时并没有相关参数用于描述 buf中的有效字节数(v5版本才增加了相关描述),这将会导致一些问题,在 recv时会讲。 -

-
接着将会检查是否有线程在等待该 ipc,如果有,将该线程设置为 就绪状态,并进行调度,保证实时性能。
c
rt_err_t rt_mq_send(rt_mq_t mq, void *buffer, rt_size_t size)
{
/* get a free list, there must be an empty item */
msg = (struct rt_mq_message *)mq->msg_queue_free;
/* move free list pointer */
mq->msg_queue_free = msg->next;
...
/* copy buffer */
rt_memcpy(msg + 1, buffer, size);
...
/* resume suspended thread */
if (!rt_list_isempty(&mq->parent.suspend_thread))
{
rt_ipc_list_resume(&(mq->parent.suspend_thread));
/* enable interrupt */
rt_hw_interrupt_enable(temp);
rt_schedule();
return RT_EOK;
}
...
}
3、接收消息
- 当设置了超时时,在没有消息的情况下,会将当前线程设置为 suspend 状态,绑定定时器并开启,然后开启调度,直到有消息或者超时。
- 当有消息时,从链表中获取头指针,并将数据 copy 到 buf,注意到没有?
size > mq->msg_size ? mq->msg_size : size,只是检查传入的 size是否超过 msg_size,因此如果你想当然的类似socket一样进行信息接收,例如rt_mq_recv(mq->msg_size),那么当我先 send 12345,再 send 67,第二次接收到的将是67345,也就是粘连历史信息。关于这个问题,rtt在 v5版本才增加了相关描述,在此之前,需要用户自己通过结构体传输相关字节,但这样存在操作数据量大的问题;或者使用不那么优雅的解决方法。
c
rt_err_t rt_mq_recv(rt_mq_t mq,
void *buffer,
rt_size_t size,
rt_int32_t timeout)
{
/* message queue is empty */
while (mq->entry == 0)
{
/* suspend current thread */
rt_ipc_list_suspend(&(mq->parent.suspend_thread),
thread,
mq->parent.parent.flag);
/* re-schedule */
rt_schedule();
}
/* get message from queue */
msg = (struct rt_mq_message *)mq->msg_queue_head;
/* move message queue head */
mq->msg_queue_head = msg->next;
/* copy message */
rt_memcpy(buffer, msg + 1, size > mq->msg_size ? mq->msg_size : size);
/* put message to free list */
msg->next = (struct rt_mq_message *)mq->msg_queue_free;
mq->msg_queue_free = msg;
}
三、总结
- 该版本的 rtt 消息队列实现非常简单,基本只是链表操作,关于消息队列没有记录发送数据大小的问题,在 v5版本也更新了相关内容,前面版本的使用则需要留意使用。