[020] [RT-Thread学习笔记] 消息队列

RT-Thread
学习笔记
消息队列控制块
消息队列工作机制
传输数据
阻塞访问
消息队列函数接口
创建与删除
初始化与脱离
发送消息
接收消息
应用示例
总结

RT-Thread版本:4.0.5
MCU型号:STM32F103RCT6(ARM Cortex-M3 内核)

1 消息队列控制块

struct rt_messagequeue
{
    
    
    struct rt_ipc_object parent;                        /* 继承ipc对象 */

    void                *msg_pool;                      /*指向存放消息的缓冲区的指针 */

    rt_uint16_t          msg_size;                      /* 每条消息大小, 单位为字节 */
    rt_uint16_t          max_msgs;                      /* 最大消息数量 */

    rt_uint16_t          entry;                         /* 队列中已有的消息数 */

    void                *msg_queue_head;                /* 消息链表头:指向第一个有数据的消息块(读数据) */
    void                *msg_queue_tail;                /* 消息链表尾:指向最后一个有数据的消息块(写数据) */
    void                *msg_queue_free;                /* 指向空闲的消息块(初始化时指向最后一个消息块) */

    rt_list_t            suspend_sender_thread;         /* 发送线程的挂起等待队列 */
};
typedef struct rt_messagequeue *rt_mq_t;

数据结构:队列

存储方式:链式

因此,需要有指向自身的指针结构:

struct rt_mq_message
{
    
    
    struct rt_mq_message *next;
};

2 消息队列工作机制

2.1 传输数据

消息队列能够接收来自线程或中断服务例程中不固定长度的消息,并把消息缓存在自己的内存空间中,用于线程间的消息交换、中断服务程序给线程发送数据(中断服务程序不能接收消息

RT-Thread 中使用队列数据结构实现线程异步通信工作,具有如下特性:

  • 可以允许不同长度(不超过队列节点最大值)的任意类型消息

  • 最大消息数量max_msgs = 消息缓冲区msg_pool大小/(每条消息msg_size大小+4)[sizeof(struct rt_mq_message) = 4]

  • 每条消息可容纳的数据大小是一样的(消息队列创建时确定)

  • 数据的操作采用先进先出的方法(FIFO):队尾写数据,队头读数据

  • 消息链表头msg_queue_head指向第一个有数据的消息块(用于读数据),消息链表尾msg_queue_tail指向最后一个有数据的消息块(用于写数据)

  • 从消息缓冲区的末尾开始读写数据(消息缓冲区低地址作为队尾,高地址作为队头),msg_queue_free 指向空闲的消息块

  • 紧急消息数据直接写到消息队列的头部

  • mq->msg_queue_free == NULL表示消息队列已满,mq->entry == 0表示消息队列已空

使用消息队列传输数据时有两种方法:

  • 拷贝:把数据、把变量的值复制进消息队列里
  • 引用:把数据、把变量的地址复制进消息队列里

一般使用拷贝方式传输数据。

消息队列操作流程举例如下:
image-20220326172857775
最后线程B取出第一个消息后,该消息块变为空闲消息块,将插入到msg_queue_free位置,然后修改msg_queue_free指向此内存块:

msg->next = (struct rt_mq_message *)mq->msg_queue_free;
mq->msg_queue_free = msg;

2.2 阻塞访问

image-20220326160439381

  • 发送线程阻塞挂在mq->suspend_sender_thread链表上,接收线程阻塞挂在mq->parent->suspend_thread链表上
  • 对于接收线程,当它取消息时消息队列为空:
    • 超时时间为0:直接返回
    • 超时时间不为0则挂起:
      • 从就绪链表移除
      • 放入mq->parent->suspend_thread链表上(按优先级PRIO或先进先出FIFO顺序插入)
      • 启动线程内置定时器,计数超时时间
      • 发起线程调度
    • 被唤醒:
      • 其他线程发送消息,且此接收线程位于mq->parent->suspend_thread链表头部,则唤醒之(从挂起链表移除,插入到就绪链表中,并停止线程内置定时器)
      • 定时器超时,唤醒之,并返回-TIMEOUT
  • 对于发送线程,当它发送消息时消息队列已满时,阻塞访问机制与接收线程相同。

3 消息队列函数接口

image-20220327023033585

3.1 创建与删除

3.1.1 创建消息队列

/**
 * @param    name mq对象指针
 * @param    msg_size 消息队列中消息的最大长度 (Unit: Byte).
 * @param    max_msgs 消息队列中的最大消息数
 * @param    RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
 * @return   成功: rt_mq_t对象, 失败: RT_NULL
 * @warning  不能在中断上下文中使用
 */
rt_mq_t rt_mq_create(const char *name,
                     rt_size_t   msg_size,
                     rt_size_t   max_msgs,
                     rt_uint8_t  flag)
{
    
    
    struct rt_messagequeue *mq;
    struct rt_mq_message *head;
    register rt_base_t temp;

    mq = (rt_mq_t)rt_object_allocate(RT_Object_Class_MessageQueue, name);
    if (mq == RT_NULL)
        return mq;

    mq->parent.parent.flag = flag;

    /* 即初始化接收线程挂起链表ipc->suspend_thread */
    _ipc_object_init(&(mq->parent));

    /* 每条消息大小对齐到RT_ALIGN_SIZE整数倍(默认为4) */
    mq->msg_size = RT_ALIGN(msg_size, RT_ALIGN_SIZE);
    mq->max_msgs = max_msgs;

    /* 分配内存大小 = [消息大小 + 消息头大小] * 消息队列容量 */
    mq->msg_pool = RT_KERNEL_MALLOC((mq->msg_size + sizeof(struct rt_mq_message)) * mq->max_msgs);
    if (mq->msg_pool == RT_NULL)
    {
    
    
        rt_object_delete(&(mq->parent.parent));

        return RT_NULL;
    }
    mq->msg_queue_head = RT_NULL;
    mq->msg_queue_tail = RT_NULL;

    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 = (struct rt_mq_message *)mq->msg_queue_free;
        mq->msg_queue_free = head;
    }

    /* 队列中已有的消息数为0 */
    mq->entry = 0;

    /* 初始化发送线程挂起链表 */
    rt_list_init(&(mq->suspend_sender_thread));

    return mq;
}
  • rt_object_allocate函数分配消息队列对象大小,并将其插入到内核对象管理链表中
  • 初始化接收/发送线程的挂起链表,即prev与next均指向自身
  • 每条消息大小mq->msg_size必须对齐到RT_ALIGN_SIZE整数倍(默认为4)
  • 分配消息缓冲区大小mq->msg_pool = [每条消息大小 + 消息头大小(指针结构大小)] * 消息队列最大容量
  • mq->msg_queue_headmq->msg_queue_tail均初始化为0
  • 按 消息大小+指针结构大小 分割分配的消息缓冲区,后面的消息块指向前面的消息块,mq->msg_queue_free指向最后一个消息块,即从后往前链接消息块
  • 队列中已有的消息数mq->entry初始化为0

3.1.2 删除消息队列

rt_mq_create创建的消息队列对象大小和消息缓冲区大小,可调用rt_mq_delete函数删除并释放内存:

/**
 * @brief    这个函数将删除一个messagequeue对象并释放内存
 * @param    mq 指向消息队列对象的指针.
 * @return   成功: RT_EOK, 失败: 其他值
 * @warning  不能在中断上下文中调用
 */
rt_err_t rt_mq_delete(rt_mq_t mq)
{
    
    
    /* 恢复所有挂在此链表上的接收线程 */
    _ipc_list_resume_all(&(mq->parent.suspend_thread));
    /* 恢复所有挂在此链表上的发送线程 */
    _ipc_list_resume_all(&(mq->suspend_sender_thread));

    /* 释放消息队列内存 */
    RT_KERNEL_FREE(mq->msg_pool);

    /* 删除消息队列对象并释放消息队列内核对象的内存 */
    rt_object_delete(&(mq->parent.parent));

    return RT_EOK;
}
  • 释放消息队列缓冲区内存&消息队列内核对象的内存
  • 唤醒挂在此消息队列链表上的所有线程,将线程错误码设为-RT_ERROR

3.2 初始化与脱离

3.2.1 初始化消息队列

/**
 * @brief    初始化静态消息对象对象
 * @param    mq         指向要初始化的消息队列对象的指针。
 * @param    name       消息对象名称
 * @param    msgpool    指向消息队列缓冲区起始地址的指针
 * @param    msg_size   每条消息大小 (Unit: Byte).
 * @param    pool_size  预先分配给消息队列的内存空间大小
 * @param    RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
 *
 * @return   成功: RT_EOK, 失败:  其他值
 *
 * @warning  仅能在线程中调用
 */
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)
{
    
    
    struct rt_mq_message *head;
    register rt_base_t temp;

    /* initialize object */
    rt_object_init(&(mq->parent.parent), RT_Object_Class_MessageQueue, name);

    mq->parent.parent.flag = flag;

    /* 初始化接收线程挂起链表ipc->suspend_thread */
    _ipc_object_init(&(mq->parent));

    mq->msg_pool = msgpool;

    /* 每条消息大小对齐到RT_ALIGN_SIZE整数倍(默认为4) */
    mq->msg_size = RT_ALIGN(msg_size, RT_ALIGN_SIZE);
    mq->max_msgs = pool_size / (mq->msg_size + sizeof(struct rt_mq_message));

    mq->msg_queue_head = RT_NULL;
    mq->msg_queue_tail = RT_NULL;

    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 = (struct rt_mq_message *)mq->msg_queue_free;
        mq->msg_queue_free = head;
    }

     /* 队列中已有的消息数为0 */
    mq->entry = 0;

    /* 初始化发送线程挂起链表 */
    rt_list_init(&(mq->suspend_sender_thread));

    return RT_EOK;
}
  • rt_object_init函数将消息队列插入到内核对象管理链表中,并标记为静态对象(type | RT_Object_Class_Static)
  • 消息队列最大容量mq->max_msgs = 分配消息缓冲区大小mq->msg_pool / [每条消息大小 + 消息头大小(指针结构大小)]
  • 消息队列缓冲区与消息队列对象是提前初始化好的,其他与动态创建函数无区别

3.2.2 脱离消息队列

rt_mq_init初始化的消息队列,可以调用rt_mq_detach函数将其从内核管理链表中脱离:

rt_err_t rt_mq_detach(rt_mq_t mq)
{
    
    
     /* 恢复所有挂在此链表上的接收线程 */
    _ipc_list_resume_all(&mq->parent.suspend_thread);
     /* 恢复所有挂在此链表上的发送线程 */
    _ipc_list_resume_all(&(mq->suspend_sender_thread));

    /* 将消息队列对象从内核对象管理链表中脱离 */
    rt_object_detach(&(mq->parent.parent));

    return RT_EOK;
}

与删除消息队列对象一样,也会唤醒挂在此消息队列链表上的所有线程,将线程错误码设为-RT_ERROR

3.3 发送消息

3.3.1 等待方式发送

消息队列对象先从空闲消息链表上取下一个空闲消息块,把线程或者中断服务程序发送的消息内容复制到消息块上,然后把该消息块挂到消息队列的尾部。当且仅当空闲消息链表上有可用的空闲消息块时,发送者才能
成功发送消息,否则根据超时时间决定错误返回还是阻塞挂起。

/**
 * @brief    这个函数将向消息队列对象发送一条消息。如果消息队列上挂起了一个线程,则该线程将被恢复。
 * @param    mq 	 指向要发送消息队列对象的指针
 * @param    buffer  消息内容
 * @param    size    消息长度(Unit: Byte).
 * @param    timeout 超时时间 (unit: an OS tick).
 * @return   成功: RT_EOK, 失败:  其他值
 * @warning  这个函数可以在中断上下文和线程中调用
 */
rt_err_t rt_mq_send_wait(rt_mq_t     mq,
                         const void *buffer,
                         rt_size_t   size,
                         rt_int32_t  timeout)
{
    
    
    register rt_ubase_t temp;
    struct rt_mq_message *msg;
    rt_uint32_t tick_delta;
    struct rt_thread *thread;

    /* 判断消息的大小 */
    if (size > mq->msg_size)
        return -RT_ERROR;

    tick_delta = 0;

    thread = rt_thread_self();

    temp = rt_hw_interrupt_disable();

    /* 获取一个空闲消息链表,必须有一个空闲链表节点 */
    msg = (struct rt_mq_message *)mq->msg_queue_free;
    /* 非阻塞调用 */
    if (msg == RT_NULL && timeout == 0)
    {
    
    
        rt_hw_interrupt_enable(temp);

        return -RT_EFULL;
    }

    /* 消息队列已满 */
    while ((msg == RT_NULL)
    {
    
    
        /* 设置线程错误码 */
        thread->error = RT_EOK;

        /* 不等待,直接返回 */
        if (timeout == 0)
        {
    
    
            rt_hw_interrupt_enable(temp);

            return -RT_EFULL;
        }

        /* 挂起当前线程 */
        _ipc_list_suspend(&(mq->suspend_sender_thread),
                            thread,
                            mq->parent.parent.flag);

        /* 有超时时间,启动定时器 */
        if (timeout > 0)
        {
    
    
            /* 获取tick */
            tick_delta = rt_tick_get();

            /* 设置超时时间并启动定时器 */
            rt_timer_control(&(thread->thread_timer),
                             RT_TIMER_CTRL_SET_TIME,
                             &timeout);
            rt_timer_start(&(thread->thread_timer));
        }

        rt_hw_interrupt_enable(temp);

        rt_schedule();

        if (thread->error != RT_EOK)  // 超时返回-RT_TIMEOUT
        {
    
    
            return thread->error;
        }

        temp = rt_hw_interrupt_disable();

        /* 如果不是永久等待,则重新计算超时等待时间 */
        if (timeout > 0)
        {
    
    
            tick_delta = rt_tick_get() - tick_delta;
            timeout -= tick_delta;
            if (timeout < 0)
                timeout = 0;
        }
    }

    /* 移动空闲链表指针,指向下一个节点 */
    mq->msg_queue_free = msg->next;

    rt_hw_interrupt_enable(temp);

    /* 这个消息是新的链表尾部, 其next指针需要置为NULL */
    msg->next = RT_NULL;

    /* 拷贝数据, 因为空闲节点有消息头,所以其真正存放消息的地址是msg + 1,  即移动4个字节 */
    rt_memcpy(msg + 1, buffer, size);

    temp = rt_hw_interrupt_disable();
    /* 将消息挂载到消息队列尾部 */
    if (mq->msg_queue_tail != RT_NULL)
    {
    
    
        /* 若队尾非空,即已有数据,则直接将发送的消息挂载到尾部链表后面 */
        ((struct rt_mq_message *)mq->msg_queue_tail)->next = msg;
    }

    /* 重置消息队列尾指针指向新消息节点 */
    mq->msg_queue_tail = msg;
    /* 若头指针为空, 则将头指针指向新消息节点 */
    if (mq->msg_queue_head == RT_NULL)
        mq->msg_queue_head = msg;

    if(mq->entry < RT_MQ_ENTRY_MAX)
    {
    
    
        /* 消息数量+1 */
        mq->entry ++;
    }
    else
    {
    
    
        rt_hw_interrupt_enable(temp); 
        return -RT_EFULL; /* 溢出 */
    }

    /* 若有接收线程挂起,唤醒之 */
    if (!rt_list_isempty(&mq->parent.suspend_thread))
    {
    
    
        _ipc_list_resume(&(mq->parent.suspend_thread));

        rt_hw_interrupt_enable(temp);

        rt_schedule();

        return RT_EOK;
    }

    rt_hw_interrupt_enable(temp);

    return RT_EOK;
}
  • 若发送的消息大小>mq->msg_size直接返回-RT_ERROR;
  • 若消息队列已满(mq->msg_queue_free == NULL),根据超时时间 判断阻塞挂起或直接返回
  • 消息队列未满正常发送流程:
    • 新消息msg存放消息块为mq->msg_queue_free
    • mq->msg_queue_free指向新消息节点的下一节点msg->next
    • 将新消息节点msg的next指针置为NULL,因为新节点成为了尾节点
    • 拷贝数据, 因为消息节点都一个消息头(链队的指针域),所以其真正存放消息的地址是msg + 1, 即移动4个字节
    • mq->msg_queue_tail不为空,则说明消息队列中已有数据,则直接将新消息挂在尾指针的下一节点
    • 将尾指针mq->msg_queue_tail指向新的队尾msg
    • mq->msg_queue_head为空,则将头指针指向新节点(说明此消息为消息队列中第一条)
    • 当前消息数量mq->entry+1(若变量溢出则返回-RT_EFUL)
    • 若有接收线程挂起,则唤醒消息队列接收线程挂起链表中第一个线程

消息发送流程如下图所示(F: mq->msg_queue_free, t: mq->msg_queue_tail, h: mq->msg_queue_head):

image-20220326225518099

3.3.2 无等待发送

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);
}

rt_mq_send_wait函数超时时间设为0

3.3.3 发送紧急消息

/**
 * @brief    这个函数将向消息队列对象发送一条紧急消息
 * @note     在发送紧急消息时,消息被放置在消息队列的头部,以便接收方可以首先收到紧急消息。
 * @param    mq     指向要发送消息队列对象的指针
 * @param    buffer 消息内容
 * @param    size    单条消息长度(Unit: Byte).
 * @return   成功: RT_EOK, 失败:  其他值
 */
rt_err_t rt_mq_urgent(rt_mq_t mq, const void *buffer, rt_size_t size)
{
    
    
    register rt_ubase_t temp;
    struct rt_mq_message *msg;

    /* 超过单条消息大小 */
    if (size > mq->msg_size)
        return -RT_ERROR;

    temp = rt_hw_interrupt_disable();

    /* 获取空闲消息块 */
    msg = (struct rt_mq_message *)mq->msg_queue_free;
    /* 消息队列已满 */
    if (msg == RT_NULL)
    {
    
    
        rt_hw_interrupt_enable(temp);

        return -RT_EFULL;
    }
    /* 移动空闲链表指针,指向下一个节点 */
    mq->msg_queue_free = msg->next;

    rt_hw_interrupt_enable(temp);

    /* 拷贝数据, 因为空闲节点有消息头,所以其真正存放消息的地址是msg + 1,  即移动4个字节 */
    rt_memcpy(msg + 1, buffer, size);

    temp = rt_hw_interrupt_disable();

    /* 将新消息挂在队头 */
    msg->next = (struct rt_mq_message *)mq->msg_queue_head;
    mq->msg_queue_head = msg;

    /* 队尾指针为空 */
    if (mq->msg_queue_tail == RT_NULL)
        mq->msg_queue_tail = msg;

    if(mq->entry < RT_MQ_ENTRY_MAX)
    {
    
    
        /* 消息数量+1 */
        mq->entry ++;
    }
    else
    {
    
    
        rt_hw_interrupt_enable(temp);
        return -RT_EFULL; /* 溢出 */
    }

    /* 唤醒挂起线程 */
    if (!rt_list_isempty(&mq->parent.suspend_thread))
    {
    
    
        _ipc_list_resume(&(mq->parent.suspend_thread));

        rt_hw_interrupt_enable(temp);

        rt_schedule();

        return RT_EOK;
    }

    rt_hw_interrupt_enable(temp);

    return RT_EOK;
}

rt_mq_urgent属于无阻塞调用,不会阻塞当前发送线程,它会将新消息插入到队列头部,这样接收线程从队头取数据时,可以直接获取新消息,具体流程:

  • 消息队列已满,返回-RT_EFULL
  • 消息队列未满:
    • mq->msg_queue_free指向下一个消息块
    • 拷贝数据
    • 将新消息插入到队头,同时修改mq->msg_queue_head指向新消息节点
    • 若此时队尾指针为空(mq->msg_queue_tail == RT_NULL),将队尾指针指向新消息(说明此消息为消息队列中第一条)
    • 当前消息数量mq->entry+1(若变量溢出则返回-RT_EFUL)
    • 若有接收线程挂起,则唤醒消息队列接收线程挂起链表中第一个线程

紧急消息发送示例(续上图):

image-20220326231943663

3.4 接收消息

/**
 * @brief    这个函数将从消息队列对象接收消息,如果消息队列对象中没有消息,线程将等待指定的时间。
 * @param    mq       指向要发送消息队列对象的指针
 * @param    buffer   接收消息缓冲区
 * @param    size     单条消息长度(Unit: Byte).
 * @param    timeout  超时时间 (unit: an OS tick).
 * @return   成功: RT_EOK, 失败:  其他值
 */
rt_err_t rt_mq_recv(rt_mq_t    mq,
                    void      *buffer,
                    rt_size_t  size,
                    rt_int32_t timeout)
{
    
    
    struct rt_thread *thread;
    register rt_ubase_t temp;
    struct rt_mq_message *msg;
    rt_uint32_t tick_delta;

    tick_delta = 0;

    thread = rt_thread_self();

    temp = rt_hw_interrupt_disable();

    /* 对于无阻塞调用 */
    if (mq->entry == 0 && timeout == 0)
    {
    
    
        rt_hw_interrupt_enable(temp);

        return -RT_ETIMEOUT;
    }

    /* 消息队列为空 */
    while (mq->entry == 0)
    {
    
    
        RT_DEBUG_IN_THREAD_CONTEXT;

        /* 重置线程中的错误码 */
        thread->error = RT_EOK;

        /* 不等待 */
        if (timeout == 0)
        {
    
    
            rt_hw_interrupt_enable(temp);

            thread->error = -RT_ETIMEOUT;

            return -RT_ETIMEOUT;
        }

        /* 挂起当前线程 */
        _ipc_list_suspend(&(mq->parent.suspend_thread),
                            thread,
                            mq->parent.parent.flag);

        /* 有等待时间,启动线程计时器 */
        if (timeout > 0)
        {
    
    
            /* 获取systick 定时器时间 */
            tick_delta = rt_tick_get();

            /* 重置线程计时器的超时时间并启动它 */
            rt_timer_control(&(thread->thread_timer),
                             RT_TIMER_CTRL_SET_TIME,
                             &timeout);
            rt_timer_start(&(thread->thread_timer));
        }

        rt_hw_interrupt_enable(temp);

        rt_schedule();

        if (thread->error != RT_EOK)
        {
    
    
            return thread->error;
        }

        temp = rt_hw_interrupt_disable();

        /* *如果它不是永远等待,然后重新计算超时时间 */
        if (timeout > 0)
        {
    
    
            tick_delta = rt_tick_get() - tick_delta;
            timeout -= tick_delta;
            if (timeout < 0)
                timeout = 0;
        }
    }

    /* 获取消息 */
    msg = (struct rt_mq_message *)mq->msg_queue_head;

    /* 将队头指针移动到下一节点 */
    mq->msg_queue_head = msg->next;
    /* 到达队尾,队尾指针设为NULL */
    if (mq->msg_queue_tail == msg)
        mq->msg_queue_tail = RT_NULL;

    /* 消息数量-1 */
    if(mq->entry > 0)
    {
    
    
        mq->entry --;
    }

    rt_hw_interrupt_enable(temp);

    /* 拷贝消息到指定存储地址 */
    rt_memcpy(buffer, msg + 1, size > mq->msg_size ? mq->msg_size : size);

    temp = rt_hw_interrupt_disable();
    /* 移到空闲链表指向已被读取的消息块 */
    msg->next = (struct rt_mq_message *)mq->msg_queue_free;
    mq->msg_queue_free = msg;

    /* 唤醒被挂起的发送线程 */
    if (!rt_list_isempty(&(mq->suspend_sender_thread)))
    {
    
    
        _ipc_list_resume(&(mq->suspend_sender_thread));

        rt_hw_interrupt_enable(temp);

        rt_schedule();

        return RT_EOK;
    }

    rt_hw_interrupt_enable(temp);

    return RT_EOK;
}
  • 若消息队列已空,根据超时时间 判断阻塞挂起或直接返回
  • 若消息队列非空:
    • 从队头mq->msg_queue_head获取消息
    • 将队头指针移动到下一节点
    • 若当前消息已到达队尾,将队尾指针置空(说明此消息为消息队列中最后一条)
    • 消息数量mq->entry-1
    • 拷贝消息到指定存储地址
    • 移到空闲链表mq->msg_queue_free指向已被读取的消息块,这样可以保证消息队列的循环利用,而不会导致头链表指针移动到队列尾部时没有可用的消息节点。
    • 若有发生线程被阻塞,则唤醒发送线程挂起链表中的第一个线程

消息接收示例(续上图):

image-20220326235936315

F:mq->msg_queue_free指向的即为被读取的消息块,该消息块next指针指向的位置是mq->msg_queue_free先前指向的位置。

同时需要注意被读取的消息块虽然被视为空闲消息块,但其中的数据并没有被清除,下次写消息时会将其直接覆盖。

4 应用示例

创建两个线程,thread1用于接收数据,thread2用于发送数据;同时创建一个静态消息队列对象,用于发送和接收消息。

#define my_printf(fmt, ...)         rt_kprintf("[%u]"fmt"\n", rt_tick_get(), ##__VA_ARGS__)

#define THREAD_STACK_SIZE     512
#define THREAD_PRIORITY       20
#define THREAD_TIMESLICE      5

typedef enum
{
    
    
    common_msg,
    urgent_msg,
}id_t;

typedef struct
{
    
    
    id_t id;
    char* str;
}msg_data_t;

msg_data_t msg_send_data[4] =
{
    
    
    {
    
    common_msg, "hello!"},
    {
    
    common_msg, "how are you?"},
    {
    
    common_msg, "send over!"},
    {
    
    urgent_msg, "SOS!"}
};

static struct rt_messagequeue mq;    // 静态消息队列对象
static char msg_pool[512];           // 静态消息队列缓冲区

static rt_thread_t thread1;
static rt_thread_t thread2;

static void thread1_entry(void* param)
{
    
    
    msg_data_t buf;
    while(1)
    {
    
    
        if (RT_EOK == rt_mq_recv(&mq, &buf, sizeof(msg_data_t), RT_WAITING_FOREVER))
        {
    
    
            if (buf.id == common_msg)
            {
    
    
                my_printf("[%s] common_msg: %s", thread1->name, buf.str);
                if (strcmp(buf.str, "send over!") == 0)
                    break;
            }
            else if (buf.id == urgent_msg)
                my_printf("[%s] urgent_msg: %s", thread1->name, buf.str);
        }
        rt_thread_mdelay(200);
    }

    my_printf("[%s] detach mq", thread1->name);

    rt_mq_detach(&mq);  // 将消息队列对象脱离
}

static void thread2_entry(void* param)
{
    
    
    rt_uint8_t cnt = 0;
    rt_err_t result;
    while(cnt++ < 10)
    {
    
    
       if (cnt & 0x1)  //  奇数
           result = rt_mq_send(&mq, &msg_send_data[0], sizeof(msg_data_t));
       else            //  偶数
           result = rt_mq_send(&mq, &msg_send_data[1], sizeof(msg_data_t));
       if (result != RT_EOK)
           my_printf("[%s] msg send error!", thread2->name);
       else
           my_printf("[%s] send message: %s", thread2->name,
                   ((msg_data_t *)((int *)mq.msg_queue_tail + 1))->str);

       rt_thread_mdelay(100);
    }

    rt_mq_urgent(&mq, &msg_send_data[3], sizeof(msg_data_t));   // 发送紧急消息
    rt_thread_mdelay(300);                                      // 等待线程1收到紧急消息

    rt_mq_send(&mq, &msg_send_data[2], sizeof(msg_data_t));     // 发送结束消息
    my_printf("[%s] message queue stop send, thread2 quit", thread2->name);
}

int msgq_sample(void){
    
    
    rt_err_t result;
    /* 静态初始化邮箱 */
    result = rt_mq_init(&mq,
                        "test_mq",
                        &msg_pool[0],
                        sizeof(msg_data_t),
                        sizeof(msg_pool),
                        RT_IPC_FLAG_FIFO);

    if (result != RT_EOK)
        return -RT_ERROR;

    thread1 = rt_thread_create("thread1",
                                thread1_entry,
                                RT_NULL,
                                THREAD_STACK_SIZE,
                                THREAD_PRIORITY,
                                THREAD_TIMESLICE - 1);
    if (thread1 == RT_NULL)
        return -RT_ERROR;
    else
        rt_thread_startup(thread1);


    thread2 = rt_thread_create("thread2",
                                thread2_entry,
                                RT_NULL,
                                THREAD_STACK_SIZE,
                                THREAD_PRIORITY,
                                THREAD_TIMESLICE);
    if (thread2 == RT_NULL)
        return -RT_ERROR;
    else
        rt_thread_startup(thread2);

    return RT_EOK;
}

每次发送数据,队尾指针都会指向新发送的数据,即可通过(msg_data_t *)((int *)mq.msg_queue_tail + 1))->str获取新发送数据的字符串,这里将mq.msg_queue_tail 转换成int*,是因为struct rt_mq_message *也恰好是指向4字节变量的指针,最后+1是因为每条消息还需要存储包含自身结构的指针,如下图所示:

image-20220327021927616

因此(int *)mq.msg_queue_tail + 1才是指向消息的数据域。

打印log信息如下:

[3][thread2] send message: hello!
[6][thread1] common_msg: hello!
[106][thread2] send message: how are you?
[209][thread1] common_msg: how are you?
[213][thread2] send message: hello!
[316][thread2] send message: how are you?
[413][thread1] common_msg: hello!
[420][thread2] send message: hello!
[523][thread2] send message: how are you?
[616][thread1] common_msg: how are you?
[627][thread2] send message: hello!
[730][thread2] send message: how are you?
[820][thread1] common_msg: hello!
[834][thread2] send message: hello!
[937][thread2] send message: how are you?
[1023][thread1] common_msg: how are you?
[1227][thread1] urgent_msg: SOS!
[1341][thread2] message queue stop send, thread2 quit
[1430][thread1] common_msg: hello!
[1633][thread1] common_msg: how are you?
[1837][thread1] common_msg: hello!
[2040][thread1] common_msg: how are you?
[2244][thread1] common_msg: send over!
[2247][thread1] detach mq

可以看到最后发送的一条紧急消息SOS,优先打印了出来。

5 总结

  • 消息队列可以发送任意字节的数据(最大65535),但是每条消息的大小在初始化就已确定,之后不可以更改
  • 消息队列采用链式存储的队列结构,将队列缓冲区切割成mq->max_msgs个大小为mq->msg_size + sizeof(struct rt_mq_message)字节的节点,其中struct rt_mq_message为指向自身的结构,即链队的next指针,最后一个节点(最高地址)作为队头,从后往前链接消息节点
  • 每条消息前4个字节存储指针自身结构的指针(指针域),后面才开始存储数据(数据域)
  • 发送消息的线程阻塞挂起在mq->suspend_sender_thread链表上,接收消息的线程阻塞挂起在mq->parent.suspend_thread链表上
  • 发送紧急消息可以直接在队头插入数据,同时修改队头指针指向此紧急数据,下次从队头读取数据时,先取走它

END

猜你喜欢

转载自blog.csdn.net/kouxi1/article/details/123767528