FreeRtos源码分析之消息队列工作原理(九)

一、消息队列API函数

xQueueCreate( uxQueueLength, uxItemSize );
xQueueSend( xQueue, pvItemToQueue, xTicksToWait );
BaseType_t xQueueReceive( QueueHandle_t xQueue,void * const pvBuffer,TickType_t xTicksToWait );
  • xQueueCreate 创建一个消息队列,队列中有uxQueueLength个元素,每个元素大小为uxItemSize,该函数返回指向队列结构体的句柄;
  • xQueueSend 发送消息到指定的队列中,xQueue表示消息队列的句柄,pvItemToQueue表示要放到消息队列中的元素,xTicksToWait表示需要等待的时间;
  • xQueueReceive 用于接收消息,xQueue表示需要接收消息队列的句柄,pvBuffer用于存储接收到的数据,xTicksToWait表示阻塞接收的时间;

二、消息队列的创建

我们先来看下消息队列的结构体:

typedef struct QueueDefinition /* The old naming convention is used to prevent breaking kernel aware debuggers. */
{
     
    int8_t * pcHead;           /*< 指向队列存储的起始位置。*/
    int8_t * pcWriteTo;        /*< 指向队列下一块空闲位置。 */

    union
    {
     
        QueuePointers_t xQueue;     /*< 当结构体作为一个队列时使用. */
        SemaphoreData_t xSemaphore; /*< 当队列作为信号量时使用. */
    } u;

    List_t xTasksWaitingToSend;             /*< 阻塞等待向队列中放入元素的任务链表。按照优先级顺序存储。 */
    List_t xTasksWaitingToReceive;          /*< 阻塞等待从队列中读取元素的任务链表,按照优先级顺序存储。 */

    volatile UBaseType_t uxMessagesWaiting; /*<当前队列中的元素个数. */
    UBaseType_t uxLength;                   /*< 队列能够存储的元素个数。 */
    UBaseType_t uxItemSize;                 /*< 每一个元素的大小,如果这个值为0表示Mutex*/

    volatile int8_t cRxLock;                /*< Stores the number of items received from the queue (removed from the queue) while the queue was locked.  Set to queueUNLOCKED when the queue is not locked. */
    volatile int8_t cTxLock;                /*< Stores the number of items transmitted to the queue (added to the queue) while the queue was locked.  Set to queueUNLOCKED when the queue is not locked. */

    #if ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
        uint8_t ucStaticallyAllocated; /*< Set to pdTRUE if the memory used by the queue was statically allocated to ensure no attempt is made to free the memory. */
    #endif

    #if ( configUSE_QUEUE_SETS == 1 )
        struct QueueDefinition * pxQueueSetContainer;
    #endif

    #if ( configUSE_TRACE_FACILITY == 1 )
        UBaseType_t uxQueueNumber;
        uint8_t ucQueueType;        //队列类型
    #endif
} xQUEUE;

/* The old xQUEUE name is maintained above then typedefed to the new Queue_t
 * name below to enable the use of older kernel aware debuggers. */
typedef xQUEUE Queue_t;

接下来我们看下队列是如何创建的。xQueueCreate函数的原型是xQueueGenericCreate函数。

#define xQueueCreate( uxQueueLength, uxItemSize )    xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) )

QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength,//队列长度
                                                   const UBaseType_t uxItemSize,//对列每个元素的大小
                                                   const uint8_t ucQueueType )//队列的类型

FreeRtos的消息队列、信号量、互斥锁、递归锁都是利用队列来实现的,它们都使用Queue_t结构体。队列的类型ucQueueType的取值如下:

#define queueQUEUE_TYPE_BASE                  ( ( uint8_t ) 0U )//队列类型为基础队列
#define queueQUEUE_TYPE_SET                   ( ( uint8_t ) 0U )
#define queueQUEUE_TYPE_MUTEX                 ( ( uint8_t ) 1U )//队列类型为互斥锁
#define queueQUEUE_TYPE_COUNTING_SEMAPHORE    ( ( uint8_t ) 2U )//队列类型为计数信号量
#define queueQUEUE_TYPE_BINARY_SEMAPHORE      ( ( uint8_t ) 3U )//队列类型为二值信号量
#define queueQUEUE_TYPE_RECURSIVE_MUTEX       ( ( uint8_t ) 4U )//队列类型为递归锁

xQueueGenericCreate的主要作用有两个,一个是给队列分配内存,另外一个是初始化队列结构体的指针。
第一步,给队列分配内存,部分源代码如下:

Queue_t * pxNewQueue;
size_t xQueueSizeInBytes;
  /*分配足够的存储空间,保证队列能够放下最大数目的队列元素。如果uxItemSize为0表示队列作为信号量使用。*/
        xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize ); 
          pxNewQueue = ( Queue_t * ) pvPortMalloc( sizeof( Queue_t ) + xQueueSizeInBytes );

从上面的代码可以看出,队列的内存是动态分配的,分配的内存空间比实际需要的空间多出了一个队列结构体Queue_t的大小。这是必须的,因为每一个队列都需要使用Queue_t这个结构体对队列进行管理。

第二步,初始化队列中的各个指针,部分源代码如下(pxQueue->pcHead指向动态分配内存中的第一个元素地址):

        pxQueue->u.xQueue.pcTail = pxQueue->pcHead + ( pxQueue->uxLength * pxQueue->uxItemSize ); /*lint !e9016 Pointer arithmetic allowed on char types, especially when it assists conveying intent. */
        pxQueue->uxMessagesWaiting = ( UBaseType_t ) 0U;//队列中元素个数初始化为0
        pxQueue->pcWriteTo = pxQueue->pcHead;
        pxQueue->u.xQueue.pcReadFrom = pxQueue->pcHead + ( ( pxQueue->uxLength - 1U ) * pxQueue->uxItemSize ); /*指向队列最后一个元素的位置。 */
        pxQueue->cRxLock = queueUNLOCKED;
        pxQueue->cTxLock = queueUNLOCKED;

初始化完成后,队列的各个指针指向如图:
FreeRtos源码分析之消息队列工作原理(九)_第1张图片

pcHead和pcTail这两个指针用来记录队列的起始地址和结束地址,这两个值始终不变。数据的写入和读取分别使用pvWriteTo和pcReadFrom两个指针,pcReadFrom始终指向pvWriteTo指向元素的前一个元素的地址。

三、 消息队列元素的写入

FreeRtos提供了3种向队列写入数据的方式:

  • queueSEND_TO_BACK 队列的入队顺序从左(前)向右(后),当队列满时,xQueueSend会返回false,并且不会覆盖之前的数据。
  • queueSEND_TO_FRONT 队列的入队顺序从右(后)向左(前),FreeRtos10.4.3版本的sdk删除了这部分相关的api。
  • queueOVERWRITE 队列中只存储一个元素,当有新的元素入队时,旧的元素会被覆盖,这种队列只有两种状态,空或者满。
    FreeRtos通过prvCopyDataToQueue函数向队列中写入元素,其源码如下:
static BaseType_t prvCopyDataToQueue( Queue_t * const pxQueue,   //队列句柄
                                      const void * pvItemToQueue,//写入队列的数据
                                      const BaseType_t xPosition )//写入位置
{
     
    BaseType_t xReturn = pdFALSE;
    UBaseType_t uxMessagesWaiting;

    /* This function is called from a critical section. */

    uxMessagesWaiting = pxQueue->uxMessagesWaiting;//记录队列中已有元素个数

    if( pxQueue->uxItemSize == ( UBaseType_t ) 0 )//如果元素大小为0表示互斥锁
    {
     
        #if ( configUSE_MUTEXES == 1 )
            {
     
                if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
                {
     
                    /* The mutex is no longer being held. */
                    xReturn = xTaskPriorityDisinherit( pxQueue->u.xSemaphore.xMutexHolder );
                    pxQueue->u.xSemaphore.xMutexHolder = NULL;
                }
                else
                {
     
                    mtCOVERAGE_TEST_MARKER();
                }
            }
        #endif /* configUSE_MUTEXES */
    }
    else if( xPosition == queueSEND_TO_BACK )
    {
     
        //将数据拷贝到队列中,每一个元素占用大小为pxQueue->uxItemSize
        ( void ) memcpy( ( void * ) pxQueue->pcWriteTo, pvItemToQueue, ( size_t ) pxQueue->uxItemSize ); 

        pxQueue->pcWriteTo += pxQueue->uxItemSize;        //更新队列空闲内存指针                                                       

        if( pxQueue->pcWriteTo >= pxQueue->u.xQueue.pcTail )      //如果队列满了,则将写指针指向队列首部(第一个元素的位置),因此是否需要覆盖队列中的数据需要在该函数外判断                                      
        {
     
            pxQueue->pcWriteTo = pxQueue->pcHead;
        }
        else
        {
     
            mtCOVERAGE_TEST_MARKER();
        }
    }
    else
    {
     
        ( void ) memcpy( ( void * ) pxQueue->u.xQueue.pcReadFrom, pvItemToQueue, ( size_t ) pxQueue->uxItemSize ); //将数据存储到队列最右边的内存中
        pxQueue->u.xQueue.pcReadFrom -= pxQueue->uxItemSize;//队列指针向左偏移一个元素的大小

        if( pxQueue->u.xQueue.pcReadFrom < pxQueue->pcHead ) //如果u.xQueue.pcReadFrom小于队列的其实地址,说明队列已经满了,将其指向队列最后一个元素的位置
        {
     
            pxQueue->u.xQueue.pcReadFrom = ( pxQueue->u.xQueue.pcTail - pxQueue->uxItemSize );
        }
        else
        {
     
            mtCOVERAGE_TEST_MARKER();
        }

        if( xPosition == queueOVERWRITE )//如果队列的写入方式是queueOVERWRITE则需要保证队列中时刻只有一个元素
        {
     
            if( uxMessagesWaiting > ( UBaseType_t ) 0 )
            {
     
                /* An item is not being added but overwritten, so subtract
                 * one from the recorded number of items in the queue so when
                 * one is added again below the number of recorded items remains
                 * correct. */
                --uxMessagesWaiting;
            }
            else
            {
     
                mtCOVERAGE_TEST_MARKER();
            }
        }
        else
        {
     
            mtCOVERAGE_TEST_MARKER();
        }
    }
    //更新队列中的元素个数
    pxQueue->uxMessagesWaiting = uxMessagesWaiting + ( UBaseType_t ) 1;

    return xReturn;
}

当xPosition的值为queueSEND_TO_BACK时,向队列中写入一个元素,各个指针的变化如下(只有写指针向右偏移一个元素的大小):
FreeRtos源码分析之消息队列工作原理(九)_第2张图片

当xPosition的值为queueSEND_TO_FRONT时,向队列中写入一个元素,各个指针的变化如下:
FreeRtos源码分析之消息队列工作原理(九)_第3张图片

FreeRtos使用xQueueSend函数向队列中写入数据,其定义如下:

#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait ) \
    xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )

xQueueGenericSend的源代码:

BaseType_t xQueueGenericSend( QueueHandle_t xQueue,             //队列句柄
                              const void * const pvItemToQueue,//放入队列中的元素
                              TickType_t xTicksToWait,          //等待时间
                              const BaseType_t xCopyPosition ) //写入位置
{
     
    BaseType_t xEntryTimeSet = pdFALSE, xYieldRequired;
    TimeOut_t xTimeOut;
    Queue_t * const pxQueue = xQueue;

   //此处省略断言部分代码

    for( ; ; )
    {
     
        taskENTER_CRITICAL();
        {
     
         /* 判断当前队列是否还有剩余空间。 如果写入类型是queueOVERWRITE则无需关心队列是否已满. */
            if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) )
            {
     
                #if ( configUSE_QUEUE_SETS == 1 )
                    {
     
                    }
                #else /* configUSE_QUEUE_SETS */
                    {
     
                        xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );

                        /* If there was a task waiting for data to arrive on the
                         * queue then unblock it now. */
                        if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
                        {
     
                            if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
                            {
     
                                /* The unblocked task has a priority higher than
                                 * our own so yield immediately.  Yes it is ok to do
                                 * this from within the critical section - the kernel
                                 * takes care of that. */
                                queueYIELD_IF_USING_PREEMPTION();
                            }
                            else
                            {
     
                                mtCOVERAGE_TEST_MARKER();
                            }
                        }
                        else if( xYieldRequired != pdFALSE )
                        {
     
                            /* This path is a special case that will only get
                             * executed if the task was holding multiple mutexes and
                             * the mutexes were given back in an order that is
                             * different to that in which they were taken. */
                            queueYIELD_IF_USING_PREEMPTION();
                        }
                        else
                        {
     
                            mtCOVERAGE_TEST_MARKER();
                        }
                    }
                #endif /* configUSE_QUEUE_SETS */

                taskEXIT_CRITICAL();
                return pdPASS;
            }
            else//如果队列已经满了
            {
     
                if( xTicksToWait == ( TickType_t ) 0 )
                {
     
                    /*如果队列已经满了,或者阻塞时间耗尽。*/
                    taskEXIT_CRITICAL();
                    return errQUEUE_FULL;
                }
                else if( xEntryTimeSet == pdFALSE )
                {
     
                    /*如果队列满了,并且阻塞时间不为0,则初始化阻塞时间。*/
                    vTaskInternalSetTimeOutState( &xTimeOut );
                    xEntryTimeSet = pdTRUE;
                }
                else
                {
     
                    /* Entry time was already set. */
                    mtCOVERAGE_TEST_MARKER();
                }
            }
        }
        taskEXIT_CRITICAL();

        /* Interrupts and other tasks can send to and receive from the queue
         * now the critical section has been exited. */
     
        vTaskSuspendAll();
        prvLockQueue( pxQueue );

        /* Update the timeout state to see if it has expired yet. */
        if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )//如果等待时间没有耗尽,尝试向队列中写入数据
        {
     
            if( prvIsQueueFull( pxQueue ) != pdFALSE )//如果队列没有满
            {
     
               //将任务放入等待列表
                vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToSend ), xTicksToWait );

                prvUnlockQueue( pxQueue );

                if( xTaskResumeAll() == pdFALSE )
                {
     
                    portYIELD_WITHIN_API();
                }
            }
            else
            {
     
                /* Try again. */
                prvUnlockQueue( pxQueue );
                ( void ) xTaskResumeAll();
            }
        }
        else
        {
     
            /* The timeout has expired. */
            prvUnlockQueue( pxQueue );
            ( void ) xTaskResumeAll();

            traceQUEUE_SEND_FAILED( pxQueue );
            return errQUEUE_FULL;
        }
    } /*lint -restore */
}

上述代码主要做了两件事情,首先是判断队列是否已经满了,如果队列有剩余空间则向队列中写入元素,否则检查指定的等待时间是否耗尽,如果在时间耗尽时队列为满,则返回false,否则返回true。
如果队列已经满了,那么新的向队列中写入的元素会被丢弃,队列中已有的元素不会被覆盖。

三、 消息队列元素的取出

FreeRtos使用prvCopyDataFromQueue函数从队列中读取数据,其源码如下:

static void prvCopyDataFromQueue( Queue_t * const pxQueue,
                                  void * const pvBuffer )
{
     
    if( pxQueue->uxItemSize != ( UBaseType_t ) 0 )//如果队列中每个元素大小不为0
    {
     
        pxQueue->u.xQueue.pcReadFrom += pxQueue->uxItemSize;  //队列读指针向后偏移一个单位,这个指针始终指向队列第一个元素的前一个元素的位置       

        if( pxQueue->u.xQueue.pcReadFrom >= pxQueue->u.xQueue.pcTail ) //如果取出的元素位于队列尾部,则将指针指向队列首部
        {
     
            pxQueue->u.xQueue.pcReadFrom = pxQueue->pcHead;
        }
        else
        {
     
            mtCOVERAGE_TEST_MARKER();
        }
        //取出队列的内容
        ( void ) memcpy( ( void * ) pvBuffer, ( void * ) pxQueue->u.xQueue.pcReadFrom, ( size_t ) pxQueue->uxItemSize );
    }
}
  1. 假设队列的写入方式为queueSEND_TO_BACK,队列中有三个元素,写入前、写入队列各个指针的指向为:
    写入前:
    FreeRtos源码分析之消息队列工作原理(九)_第4张图片

写入3个元素后各个指针的指向:
FreeRtos源码分析之消息队列工作原理(九)_第5张图片

读取第一个元素后各个指针的指向:
FreeRtos源码分析之消息队列工作原理(九)_第6张图片

  1. 假设队列的写入方式为queueSEND_TO_FRONT,队列中有三个元素,写入前、写入队列各个指针的指向为:
    写入前:
    FreeRtos源码分析之消息队列工作原理(九)_第7张图片

写入后:
FreeRtos源码分析之消息队列工作原理(九)_第8张图片

取出一个元素:
FreeRtos源码分析之消息队列工作原理(九)_第9张图片

从上面的图解中可以很清晰的看出来,queueSEND_TO_BACK的入队方式是先进先出,而queueSEND_TO_FRONT的入队方式是先进后出。

你可能感兴趣的:(FreeRtos,队列,FreeRtos)