• 【freertos】010-消息队列概念及其实现细节


    前言

    消息队列是任务间通信系列介绍的首篇笔记,因为学习完消息队列的源码实现后,信号量、互斥量这些任务间通信机制也相当于学完了,只剩下概念性的内容了。

    参考:

    重点问题

    这个只是阅读源码时发现的,并未验证,有空再填这个坑。

    vQueueDelete()就知道,这玩意只是释放内存,没做别的操作。

    那么我们思考一个问题,如果此时有任务阻塞在这个队列对象里面,我们调用vQueueDelete()释放内存后,如果阻塞在这个队列对象的任务被唤醒了,需要解除阻塞,解除任务事件节点值,那岂不是会对内存的乱踩?

    唤醒参考阻塞超时唤醒后调用xTaskIncrementTick()里面的代码段:

    freertos的这个问题确实会存在,所以我们删除消息队列时必须确保没有任务阻塞在这个队列里面。

    10.1 消息队列概念

    消息队列实任务间通信机制中的一种。

    其它还有二值信号量、计数信号量、互斥量和递归互斥量等等。

    一个或多个任务往一个消息容器里面发消息,其它一个或多个任务从这个消息容器里面获取消息,这样实现通信。
    /* 该图源自野火 */

    freertos的消息队列:

    • 支持FIFO、支持LIFO也支持异步读写工作方式。
    • 支持超时机制。
    • 支持不同长度(在节点长度范围内)、任意类型的消息。
    • 一个任务可对一个消息队列读、写。
    • 一个消息队列支持被多个任务读、写。
    • 队列使用一次后自动从消息队列中移除。

    10.2 消息队列的数据传输机制

    队列传输数据有两种方式:

    1. 拷贝:把数据、把变量的值复制进队列里。
    2. 引用:把数据、把变量的地址复制进队列里。

    而freertos的消息队列机制就是拷贝,拷贝的方式有以下优点:

    • 局部变量的值可以发送到队列中,后续即使函数退出、局部变量被回收,也不会影响队列中的数据。
    • 无需分配buffer来保存数据,队列中有buffer。
    • 发送任务、接收任务解耦:接收任务不需要知道这数据是谁的、也不需要发送任务来释放数据。
    • 如果数据实在太大,可以选择传输地址(即是拷贝地址),依然能实现传输引用的效果。
    • 队列的空间有FreeRTOS内核分配,无需上层应用维护。
    • 无需考虑内存保护功能,因为拷贝的方式新数据的存储区是由队列组件提供的,无需担心获取消息的任务需要权限访问。

    当然对比引用的方式也有劣势:

    1. 拷贝数据相对拷贝引用来说要耗时。
    2. 需要更多内存,因为需要存储数据副本。

    10.3 消息队列的阻塞访问机制

    只要拿到队列句柄,任务和中断都有权限访问消息队列,但是也有阻塞限制。

    写消息时,如果消息队列已满,则无法写入(覆盖写入除外),如果用户设置的阻塞时间不为0,则任务会进入阻塞,直到该队列有空闲空间给当前任务写入消息或阻塞时间超时才解除阻塞。

    上面说的“该队列有空闲空间给当前任务写入消息”是因为就算当前队列有空间空间,也会优先安排阻塞在等待写链表中的最高优先级任务先写入。如果任务优先级相同,则先安排给最早开始等待的那个任务先写。

    读消息时,机制和写消息一样,只是阻塞的条件是队列里面没有消息。

    数据传输和阻塞访问机制都会在分析源码时阐述

    10.4 消息队列使用场景

    消息队列可以应用于发送不定长消息的场合,包括任务与任务间的消息交换。

    队列是FreeRTOS主要的任务间通讯方式,可以在任务与任务间、中断和任务间传送信息。

    发送到队列的消息是通过拷贝方式实现的,这意味着队列存储的数据是原数据,而不是原数据的引用。

    10.5 消息队列控制块

    消息队列的控制块也是队列控制块,这个控制块的数据结构除了被消息队列使用,还被使用到二值信号量、计数信号量、互斥量和递归互斥量。

    FreeRTOS的消息队列控制块由多个元素组成,当消息队列被创建时,系统会为控制块分配对应的内存空间,用于保存消息队列的一些信息,包括数据区位置、队列状态等等。

    10.5.1 队列控制块源码

    队列控制块struct QueueDefinition源码:

    10.5.2 队列控制块成员剖析

    在成员剖析时默认按消息队列的作用去剖析。

    int8_t *pcHead;:

    int8_t *pcTail;:

    int8_t *pcWriteTo;:

    int8_t *pcReadFrom;:

    UBaseType_t uxRecursiveCallCount;:

    List_t xTasksWaitingToSend;:

    List_t xTasksWaitingToReceive;:

    volatile UBaseType_t uxMessagesWaiting;:

    UBaseType_t uxLength;:

    UBaseType_t uxItemSize;:

    volatile int8_t cRxLock;:

    volatile int8_t cTxLock;:

    10.5.3 cRxLock 和 cTxLock

    当中断服务程序操作队列并且导致阻塞的任务解除阻塞时。
    首先判断该队列是否上锁:

    • 如果没有上锁,则解除被阻塞的任务,还会根据需要设置上下文切换请求标志;
    • 如果队列已经上锁,则不会解除被阻塞的任务,取而代之的是,将xRxLock或xTxLock加1,表示队列上锁期间出队或入队的数目,也表示有任务可以解除阻塞了。

    cRxLock 对应待出队的个数。
    cTxLock 对应待入队的个数。

    10.5.4 队列控制块数据结构图

    10.6 创建消息队列

    创建消息队列是在系统上新建一个消息队列,申请资源并初始化后返回句柄给用户,用户可以使用该队列句柄访问、操作该队列。

    10.6.1 创建消息队列API说明

    队列的创建有两种方法:静态分配内存、动态分配内存。其区别就是队列的内存来源是用户提供的还是内核分配的。

    主要分析动态分配内存。

    函数原型:

    参数说明:

    • uxQueueLength:队列长度,最多能存放多少个数据(item)。

    • uxItemSize:每个数据(item)的大小:以字节为单位。

    • 返回值:

      • 非0:成功,返回句柄,以后使用句柄来操作队列。
      • NULL:失败,因为内存不足。

    10.6.2 创建消息队列简要步骤

    1. 参数校验。
    2. 计算本次队列需要的总内存。
    3. 分配队列内存空间。
    4. 初始化队列控制块。
    5. 格式化队列数据区。
    6. 返回队列句柄。

    10.6.3 创建消息队列源码

    创建消息队列这个API其实就是封装了创建队列xQueueGenericCreate()这个通用API,类型为queueQUEUE_TYPE_BASE

    其中队列的类型有多种:

    创建队列函数源码xQueueGenericCreateStatic()

    初始化队列函数源码prvInitialiseNewQueue()

    • 小笔记:初始化队列,看源码实现就知道控制块和数据区物理内存是可以分开的,但是在创建消息队列这个API里面实现是连续的。

    重置队列函数xQueueGenericReset()

    • 专门用于函数据区的队列,如消息队列。

    10.6.4 消息队列数据结构图

    10.7 发送消息

    任务或者中断服务程序都可以给消息队列发送消息。

    中断中发送消息不可阻塞。要么直接返回,要么覆盖写入。

    任务发送消息时,如果队列未满或者允许覆盖入队,FreeRTOS会将消息拷贝到消息队列队尾或队列头,否则,会根据用户指定的阻塞超时时间进行阻塞。直到该队列有空闲空间给当前任务写入消息或阻塞时间超时才解除阻塞。

    发送消息的API分任务和中断专属,中断专用的API都带FromISR后缀。

    因为本系列笔记主要记录源码实现,API的使用不会详细列举。

    10.7.1 发送消息API

    参数说明:

    • xQueue:队列句柄。

    • pvItemToQueue:数据指针,这些数据的值会被复制进队列。

    • xTicksToWait:最大阻塞时间,单位Tick Count。

      • 如果被设为0,无法写入数据时函数会立刻返回;
      • 如果被设为portMAX_DELAY,则会一直阻塞直到有空间可写
    • 返回值:

      • pdPASS:数据成功写入了队列
      • errQUEUE_FULL:写入失败,因为队列满了。

    10.7.2 发送消息实现简要步骤

    1. 参数校验。

    2. 检查当前队列是否有空闲空间可写入。

      1. 进入临界。

      2. 有空间可写入:

        1. 直接写入。
        2. 检查下是否有任务阻塞在当前队列写阻塞链表中,有就解锁一个最高优先级、最早开始等待的任务。
        3. 退出临界。
      3. 没空间可写入:进入阻塞处理。

        1. 不需要阻塞,就退出临界并返回。
        2. 开始阻塞超时计时。
        3. 退出临界。(可能会切到其它任务或中断)
        4. 挂起调度器。
        5. 再次检查下是否有空间可写,是否超时。
        6. 需要阻塞就计算下当前任务的唤醒时间,记录到任务事件状态节点信息中,把当前任务从就绪链表抽离,插入到延时链表和当前队列的写阻塞任务链表中。
        7. 切走任务,等待唤醒。

    10.7.3 发送消息源码分析

    往队列里发消息的API(中断专用除外),都是封装xQueueGenericSend()函数而来的,所以我们直接分析该函数实现即可。

    需要注意的是,如果发送消息前,调度器被挂起了,则这个消息不能配置为阻塞式的,因为如果挂起调度器后使用阻塞式写入队列,会触发断言。

    在这里可以拓展下,如果没有这个断言校验,队列已满,则会在当前任务一直死循环,直至有中断服务恢复调度器或读取当前队列的消息,当前任务才能跑出这个坑。

    xQueueGenericSend()

    写入队列的API源码prvCopyDataToQueue():(临界中调用

    优先级继承机制概念、实现原理及其源码在互斥量章节的笔记讲解。

    10.7.5 中断专用的发送消息API

    中断专用的发送消息API比普通的发送消息API佛系了。

    区别就是:中断专用的没有阻塞机制。

    如果队列有空闲空间,或本次是强制写入,就把数据写入。

    • 写入后如果队列没有上锁,就更新当前队列信息,解锁阻塞在读阻塞队列的最高优先级、最早等待的一个任务。

    • 如果队列上锁了,就用队列中的pxQueue->cTxLock记录当前队列入队了一个数据,在调用prvUnlockQueue()解锁时更新当前队列信息,解锁阻塞在读阻塞队列的最高优先级、最早等待的一个任务。

    如果队列没有空闲空间,又不是强制写入,就直接退出。

    10.8 接收消息

    当任务从队列中读取消息时,如果队列中有消息,可以读取并返回。

    如果队列中没有消息,需要进入阻塞处理,在阻塞超时前,有其他任务或中断服务往这个队列里面写消息了,且当前任务时这个队列中阻塞在读阻塞链表中的最高优先级、最先等待的任务,就解锁该任务,否则还会一直阻塞到超时才唤醒当前任务。

    10.8.1 接收消息API

    参数说明:

    • xQueue:队列句柄。

    • pvBuffer:存储接收数据的指针,其有效空间需要按照当前队列属性设定。

    • xTicksToWait:如果队列空则无法读出数据,可以让任务进入阻塞状态,xTicksToWait表示阻塞的最大时间,单位:Tick Count。

      • 如果被设为0,无法读出数据时函数会立刻返回;
      • 如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写。
    • 返回值:

      • pdPASS:从队列读出数据入;
      • errQUEUE_EMPTY:读取失败,因为队列空了。

    10.8.2 接收消息简要步骤

    10.8.3 接收消息源码

    xQueueReceive()

    出队函数prvCopyDataFromQueue()

    10.9 窥探消息

    就是只读取数据,不删除该数据。

    其源码和xQueueReceive()差不多,只是数据不删除,读指针也不偏移,有效个数也不减少。

    10.10 队列查询

    队列查询主要是操作队列控制块中的信息。

    10.10.1 查询队列当前有效数据个数

    10.10.2 查询队列当前可以空间个数

    10.11 删除消息队列

    队列删除函数是根据消息队列句柄直接删除的,删除之后这个消息队列的所有信息都会被系统回收清空,而且不能再次使用这个消息队列了。

    直接上源码:

    10.12 消息队列使用注意

    在使用freertos提供的消息队列组件时,需要注意以下几点:

    1. 使用xQueueSend()、xQueueSendFromISR()、xQueueReceive()等这些函数之前应先创建需消息队列,并根据队列句柄进行操作。
    2. 要明白写入队列采用的逻辑时FIFO还是LIFO,使用对应的API。
    3. 在获取队列中的消息时候,必须要定义一个存储读取数据的地方,并且该数据区域大小不小于消息大小,否则,很可能引发地址非法的错误。
    4. freertos的数据流是拷贝方式实现的,如果消息过大,建议使用拷贝引用。
    5. 队列独立在内核中,不属于任何一个任务。

    小结

    学习,重在理解,懂得底层原理,上层特性、特点即可推理。


    __EOF__

  • 本文作者: 李柱明
  • 本文链接: https://www.cnblogs.com/lizhuming/p/16344076.html
  • 关于博主: 嵌入式从业者。RTOS、Linux ...
  • 版权声明: 版权归博主所有
  • 声援博主: 学习笔记分享
  • 相关阅读:
    2022.11.14-11.20 AI行业周刊(第124期):给自己的礼物
    别以为逃离大城市你就能舒适了,小城市可比你想象的内卷!
    线性表之顺序表
    51单片机入门_江协科技_27~28_OB记录的自学笔记_AT24C02数据存储&秒表
    delphi技术专题---获取网卡物理地址之NetBios网络编程接口LANA介绍
    基于训练和推理场景下的MindStudio高精度对比
    Spring cloud—Netflix
    SSM整合
    计算机视觉小项目—基于RGB颜色特征的火焰识别
    Apache Linkis 与 OceanBase 集成:实现数据分析速度提升
  • 原文地址:https://www.cnblogs.com/lizhuming/p/16344076.html