深入理解FreeRTOS的队列,理解原理并面向面试
如果想要深入理解FreeRTOS的队列,那么在看下面的文章之前,你需要带着这么几个问题,并且尝试在阅读文章的时候,回答出下面几个问题,如果你看完后,能够根据内容总结出你的回答,相信你在今后使用和向别人讲解的时候也能够得心应手,当然,在文章的最后,我也会给出我的回答。
问题:
1.什么是队列,为什么要使用队列?
2.队列有什么特点?
3.队列的内部实现机制是什么样的?
一、队列的基本概念
队列概念:
队列是一种常用于任务间通信的数据结构,队列可以在任务与任务间、中断和任务间传递信息,实现了任务接收来自其他任务或中断的不固定长度的消息(而这个消息可以是任意类型的数据),任务能够从队列里面读取消息,也能够向队列发送消息。
如果我一直bb文字,那肯定看着没有兴趣,所以放出一张来自韦神经典的队列图:如下图所示,左边是向队列写数据的任务,也就是我们生活中所理解的生产者,在我和你的关系中,我就是写文章的人,也就是这个生产者,在实际产品中,生产者多为上位机发来的控制信号或者传感器采集到的数据;而你是阅读文章的人,也就是消费者,处于队列的右边,在实际产品中,消费者就是根据这个队列里控制信号进行一系列实际操作的函数或者将队列中的传感器的数据进行二次加工的函数。
一个队列可以有很多任务来写队列也可以很多任务来读队列。但是并不能两个队列同时来写或读队列。队列是通过关中断的方式来保证队列同一时间只能一个任务进行读写。这是因为关中断后,任务无法进行切换,因此其它的任务或者中断无法得到执行,也就不会有任务来对这个队列进行读写操作了。(后面会有体现)
用一个生活中的例子解释就是:领导想要我们搬砖,把我们叫去办公室安排任务,就相当于领导向我们的脑子(队列)写入了一个控制信号,控制我们按照要求把事情办好,但是一只领导一次只向一个人安排任务,一个人一次也只能听进去一个领导的话,不然就成了领导在吵架。例子可能举得并不恰当,因为没有完美的体现生产者和消费者模型,但是意思就是这么个意思。
对于队列有了一个简单的介绍,下面对队列的特点进行说明
队列的特点:
1.一般情况下队列消息是先进先出方式排队(当有新的数据被写入队列中时,永远都是写入到队列的尾部,而从队列中读取数据时,永远都是读取队列的头部数据),但同时 FreeRTOS的队列也支持将数据写入到队列的头部,并且还可以指定是否覆盖先前已经在队列头部的数据。
2. 队列传输数据时有两种方法:1. 直接拷贝数据 2.拷贝数据的地址,然后根据地址读取数据。 第二种方法适合传输大数据比如一个大数组, 或者一个结构体变量。
3.队列不属于某个特定的任务,可以在任何的任务或中断中往队列中写入消息,或者从队列中读取消息。
因为同一个队列可以被多个任务读取,因此可能会有多个任务因等待同一个队列,而被阻塞,在这种情况下,如果队列中有可用的消息,那么也只有一个任务会被解除阻塞并读取到消息,并且会按照阻塞的先后和任务的优先级,决定应该解除哪一个队列读取阻塞任务;
4.读写队列均支持阻塞机制
以读队列为例:在任务从队列读取消息时,可以指定一个阻塞超时时间。如果队列不为空则会读取队列中第一个消息(通过拷贝的方式: memcpy函数),如果队列为空,则看我们自己设置阻塞时间;设置不等待,则返回错误,设置等待多少ms,比如20ms,则如果20ms内,读取不到数据还是返回错误;
5.当在中断中读写队列时,如果队列空或满,不会进行阻塞,直接返回队列空或队列满错误,因为中断要的就是快进快出。
这些是参考这个博客FreeRTOS-消息队列详解_freertos消息队列-CSDN博客,后面的内容也会参考这个博客,但是会加入我自己的理解,以及划上重点,方便理解。
二、队列的内部实现
想要知道队列的一个具体的运行过程,以及为什么队列会有这些特点,那还是得看源码,先别急,我会把源码拆成一块块的,保证你能够理解。
首先先放出源码,如果你不看就想要退出或者看了一遍,什么也看不懂,那么你可以先不用看源码,而是看后面整理的图。
typedef struct QueueDefinition /* The old naming convention is used to prevent breaking kernel aware debuggers. */{ int8_t * pcHead; /*< 指向队列存储区域的开始。*/ int8_t * pcWriteTo; /*< 指向存储区域的下一个空闲位置。 *//* 当用于队列时,使用联合体中的 xQueue 当用于信号量时,使用联合体中的 xSemaphore */ union { QueuePointers_t xQueue; SemaphoreData_t xSemaphore; } u; List_t xTasksWaitingToSend; /*< 因为等待入队而阻塞的任务列表。 按优先级顺序存储。*/ List_t xTasksWaitingToReceive; /*< 因为等待出队而阻塞的任务列表。按优先级顺序存储。 */ volatile UBaseType_t uxMessagesWaiting; /*< 当前队列的队列项数目。 */ UBaseType_t uxLength; /*< 队列的总队列项数。 */ UBaseType_t uxItemSize; /*< 队列将保存的每个队列项的大小(单位为字节)。*/ volatile int8_t cRxLock; /*< 存储队列锁定时,从队列接收(从队列中删除)的出队项目数。 如果队列没有上锁,设置为queueUNLOCKED。 */ volatile int8_t cTxLock; /*< 存储队列锁定时,传输到队列(添加到队列)的入队项目数。 如果队列没有上锁,设置为queueUNLOCKED。 */ #if ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) ) uint8_t ucStaticallyAllocated; /*< 如果队列使用的内存是静态分配的,则设置为 pdTRUE,以确保不尝试释放内存。*/ #endif/* 此宏用于使能启用队列集 */ #if ( configUSE_QUEUE_SETS == 1 ) struct QueueDefinition * pxQueueSetContainer; /* 指向队列所在队列集 */ #endif/* 此宏用于使能可视化跟踪调试 */ #if ( configUSE_TRACE_FACILITY == 1 ) UBaseType_t uxQueueNumber;/* 队列的类型 0: 队列或队列集 1: 互斥信号量 2: 计数型信号量 3: 二值信号量 4: 可递归信号量 */ uint8_t ucQueueType; #endif} xQUEUE;/* 重定义成 Queue_t */typedef xQUEUE Queue_t;
下图就是将上面的源码转换为示意图之后的样子,现在再来解释每一个成员的作用。
图中可以看出,
1.pcHead(图中左上角部分):指向队列存储区域的开始位置;
2.pcWriteTo :指向队列消息存储区下一个可用消息空间。图中指向的和pcHead是同一个位置,初始化完成后,一般是指向队列的最末尾处的;
3.联合体变量,也就是图中的pcWriteTo 下面的:左边是用作互斥量时的样子,右边是用作消费队列时的样子,这里只讲队列:
pcTail:队列存储区域的结束地址,与pcHead一样一个指向开始地址一个指向结束地址,他们只是一个一头一尾的标识,在入队出队的时候他们并不会改变。
pcReadFrom:最后一次读取队列的位置。
4.xTasksWaitingToSend :发送消息阻塞列表,看英文意思也知道:等待发送的任务,也就是队列已满,任务想要发送消息到队列(入队),如果设定了阻塞时间,任务就会挂入该列表,表示任务已阻塞,任务会按照优先级进行排序(后面解除阻塞就是按照任务的优先级:当队列不为满了,xTasksWaitingToSend 列表中优先级高的就会先被唤醒)。
5.xTasksWaitingToReceive:等待消息阻塞列表,看英文意思也知道:等待接收的任务,也就是队列已空,任务想要从队列中读取消息(出队),如果设定了阻塞时间,任务就会挂入该列表,表示任务已阻塞,任务会按照优先级进行排序(后面解除阻塞就是按照任务的优先级:当队列不为空了,xTasksWaitingToReceive列表中优先级高的就会先被唤醒)。
然后到了右边这张图,最上面开始
6.uxMessagesWaiting:用于记录当前消息队列的消息个数,如果消息
队列被用于信号量的时候,这个值就表示有效信号量个数。
7.uxLength:表示队列的长度,表示一共能存放多少消息。
8.uxItemSize:表示单个消息的大小(单位为字节)。
9.cRxLock:队列上锁后,从队列接收(从队列中删除)的出队项目数。 如果队列没有上锁,设置为queueUNLOCKED。
10.cTxLock:队列上锁后,传输到队列(添加到队列)的入队项目数。 如果队列没有上锁,设置为queueUNLOCKED。
11.队列空间:最后的蓝色的地方也就是实际的消息存放的位置了,可见一个消息队列中,消息所占的空间并不算大,尤其是你自己消息个数和大小很少的情况下,大部分的空间其实都是被前面的指针和链表所占用了。其实黄色的部分都是对于消息队列本身的一种描述,蓝色的部分才算是真正的消息,但是如果没有了黄色的部分,消息队列也只能称为消息,而不能称为消息队列了。
到了这里,你已经对于队列的构成有了一个初步的认知了,接下来的部分其实都是在对这里面的变量进行一些操作,下面会演示队列实际使用过程中,队列里面的成员的一个变化过程。
三、队列的创建
对于队列的使用,第一个步骤就是根据你的需求创建一个队列;因此首先我们来看队列是怎么样被创建的。
队列创建函数 xQueueCreate()
队列创建与任务创建都分为动态与静态创建,所谓静态创建就是队列所需要的内存需要自己来分配,而动态创建则由FreeRTOS动态分配。在队列这里就不讲静态创建了因为也基本不用;
如图所示,队列所需要的内存分为两部分:1.队列结构体变量 2.队列的存储区域(环形缓存区),动态创建时会自动分配这两块内存,而且是连续的。
队列的创建使用的参数是xQueueCreate(uxQueueLength,uxItemSize)
xQueueCreate函数有两个参数uxQueueLength,uxItemSize。
uxQueueLength:队列能够存储的最大消息数目,即队列长度。
uxItemSize:队列中消息的大小,以字节为单位
返回值:
如果创建成功则返回一个队列句柄(就是队列结构体的地址),用于访问创建的队列。如果创建不成功则返回NULL,大概率一个原因是创建队列需要的 RAM 无法分配成功。
接下来就是创建队列的源码分析:如果你不想看,直接看后面的对于核心代码的分析
#if ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength, const UBaseType_t uxItemSize, const uint8_t ucQueueType ) { Queue_t * pxNewQueue = NULL; size_t xQueueSizeInBytes; uint8_t * pucQueueStorage; if( ( uxQueueLength > ( UBaseType_t ) 0 ) && /* 检查乘法溢出 */ ( ( SIZE_MAX / uxQueueLength ) >= uxItemSize ) && /* 检查是否溢出 */ ( ( SIZE_MAX - sizeof( Queue_t ) ) >= ( uxQueueLength * uxItemSize ) ) ) { /* 计算队列环形存储空间需要的字节大小,在队列用作信号量的情况下,uxItemSize 为零是有效的。 */ xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize ); /* 为队列申请内存空间:队列结构体+队列环形存储区域 */ pxNewQueue = ( Queue_t * ) pvPortMalloc( sizeof( Queue_t ) + xQueueSizeInBytes );/* 内存申请成功 */ if( pxNewQueue != NULL ) { /* 获取队列环形存储区域的起始地址(跳过队列结构体) */ pucQueueStorage = ( uint8_t * ) pxNewQueue; pucQueueStorage += sizeof( Queue_t ); /* 此宏用于启用支持静态内存管理 */ #if ( configSUPPORT_STATIC_ALLOCATION == 1 ) { /* 此宏用于启用支持静态内存管理,以防静态地理以后被删除 */ pxNewQueue->ucStaticallyAllocated = pdFALSE; } #endif /* configSUPPORT_STATIC_ALLOCATION */ /* 初始化队列 */ prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, ucQueueType, pxNewQueue ); } else { /* 用于调试,不用理会 */ traceQUEUE_CREATE_FAILED( ucQueueType ); mtCOVERAGE_TEST_MARKER(); } } else { configASSERT( pxNewQueue ); mtCOVERAGE_TEST_MARKER(); } /* 返回队列句柄(队列结构体地址) */ return pxNewQueue; }#endif /* configSUPPORT_STATIC_ALLOCATION */
首先是一些安全方面的,也就是是否会产生溢出的检测,这部分暂时不提,然后是关键的部分
1.计算队列环形存储空间需要的字节大小,环形存储区大小=消息的总个数*每个消息的大小(字节)。
2.为队列申请内存空间,队列所需内存就是队列结构体的大小+环形存储区的大小,然后一起分配出空间(前面作为队列结构体)。
3.当内存申请成功,获取存储区的起始地址,标记队列为动态申请,最后去调用prvInitialiseNewQueue()函数初始化队列(队列结构体成员的初始化)。
static void prvInitialiseNewQueue( const UBaseType_t uxQueueLength,队列长度
const UBaseType_t uxItemSize, 队列大小
uint8_t * pucQueueStorage, 队列存储空间的起始地址
const uint8_t ucQueueType, 队列类型
Queue_t * pxNewQueue ) 队列结构体
这里面的代码很简单,读者能看到这里,这里面主要做了什么看懂肯定没问题;
static void prvInitialiseNewQueue( const UBaseType_t uxQueueLength, const UBaseType_t uxItemSize, uint8_t * pucQueueStorage, const uint8_t ucQueueType, Queue_t * pxNewQueue ){ /* 防止编译器警告(可能用不到这个ucQueueType参数) */ ( void ) ucQueueType; if( uxItemSize == ( UBaseType_t ) 0 ) { /* 如果队列项目大小为 0(类型为信号量),那么就不需要存储空间 让pcHead指向一个有效区域就行,这里指向队列结构体起始地址 */ pxNewQueue->pcHead = ( int8_t * ) pxNewQueue; } else { /* 将pcHead设置为队列环形存储区域的起始地址. */ pxNewQueue->pcHead = ( int8_t * ) pucQueueStorage; } /* 初始化队列长度 */ pxNewQueue->uxLength = uxQueueLength; /* 队列消息的大小 */ pxNewQueue->uxItemSize = uxItemSize; /* 重置队列 */ ( void ) xQueueGenericReset( pxNewQueue, pdTRUE ); /* 此宏用于启用可视化跟踪调试 */ #if ( configUSE_TRACE_FACILITY == 1 ) { /* 队列的类型 */ pxNewQueue->ucQueueType = ucQueueType; } #endif /* configUSE_TRACE_FACILITY *//* 此宏用于使能使用队列集 */ #if ( configUSE_QUEUE_SETS == 1 ) { /* 队列所在队列集设为空 */ pxNewQueue->pxQueueSetContainer = NULL; } #endif /* configUSE_QUEUE_SETS *//* 用于调试 */ traceQUEUE_CREATE( pxNewQueue );}
看完了之后,你可能会发现,这里面又没有真正的初始化好这个队列,他又调用了xQueueGenericReset()函数,真是令人头大,好的代码总是不会一口气把功能给你讲完,总是要掉你的胃口,一步步深入,因此,我们又得进入到xQueueGenericReset()函数;但这是最后一个了,马上结束了,所以不要着急。
xQueueGenericReset()函数解析:
BaseType_t xQueueGenericReset( QueueHandle_t xQueue, BaseType_t xNewQueue ){ BaseType_t xReturn = pdPASS; Queue_t * const pxQueue = xQueue; configASSERT( pxQueue ); if( ( pxQueue != NULL ) && ( pxQueue->uxLength >= 1U ) && /* Check for multiplication overflow. */ ( ( SIZE_MAX / pxQueue->uxLength ) >= pxQueue->uxItemSize ) ) { taskENTER_CRITICAL(); /* pcTail:队列存储区域的结束地址 */ pxQueue->u.xQueue.pcTail = pxQueue->pcHead + ( pxQueue->uxLength * pxQueue->uxItemSize ); /* uxMessagesWaiting:队列中现有消息数量 */ pxQueue->uxMessagesWaiting = ( UBaseType_t ) 0U; /* pcWriteTo:下一个写入的位置 */ pxQueue->pcWriteTo = pxQueue->pcHead; /* pcReadFrom:最后一次读取的位置 */ pxQueue->u.xQueue.pcReadFrom = pxQueue->pcHead + ( ( pxQueue->uxLength - 1U ) * pxQueue->uxItemSize ); /* 消息队列没有上锁,设置为 queueUNLOCKED=-1 */ pxQueue->cRxLock = queueUNLOCKED; pxQueue->cTxLock = queueUNLOCKED; /* xNewQueue为pdFALSE表示队列为旧队列 */ if( xNewQueue == pdFALSE ) { /* 如果有任务被阻止等待从队列中读取,则任务将保持阻塞状态,因为在此函数退出后,队列仍将为空。 如果有任务被阻止等待写入队列,那么应该取消阻止,因为在此函数退出后可以写入它。*/ /* 唤醒写入阻塞任务列表的优先级最高的一个任务 */ if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE ) { if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE ) { /* 如果取消阻塞的任务优先级为就绪态任务中的最高优先级 则需要进行任务切换 */ queueYIELD_IF_USING_PREEMPTION(); } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } } else { /* 队列为新创建的队列,则初始化两个阻塞链表 */ vListInitialise( &( pxQueue->xTasksWaitingToSend ) ); vListInitialise( &( pxQueue->xTasksWaitingToReceive ) ); } taskEXIT_CRITICAL(); } else { xReturn = pdFAIL; } configASSERT( xReturn != pdFAIL ); return xReturn;}
这个函数就是实现了最终的一个队列的初始化工作
pcTail:pcHead存储区起始位置加上存储区的大小(总消息个数*一个消息大小),指向存储区结束位置。
uxMessagesWaiting:刚创建的队列里面无消息,即队列为空,则消息数量为0。
pcWriteTo:下一个入队的位置(从尾部入队),这里的尾部与pcTail是不一样的pcTail是始终指向存储区结束位置,而pcWriteTo是从队首的第一个空位,即尾部入队;
pcReadFrom:最后一次读取的位置,最开始是在队尾第一个消息的位置,后面出队源码分析的时候再来讲为什么在这个位置。
cRxLock,cTxLock设置为queueUNLOCKED等于-1,表示一开始队列不会上锁。(后面再来讲关于队列上锁)。
到这里队列结构体成员变量基本初始化完毕, 假设我们申请了4个队列项,每个队列项占用16字节存储空间(即uxLength=4、uxItemSize=16),则队列内存分布如下图所示。
四.向队列发送消息(入队)
一、任务级
FreeRTOS的队列入队提供很多接口函数,其实主要是xQueueGenericSend()与xQueueGenericSendFromISR()函数为实际执行函数,一个是在任务中调用的,一个是在中断中调用的,其中又因为入队的方式又分成几个函数(尾部入队,头部入队),最后还有一个特殊情况,覆写入队,这种方式下队列只能有一个消息,每次写入都会覆盖上一个消息的内容。
最主要的还是任务中的队列写入函数,后面的其实都是一些特殊情况。在任务中往队列写入消息的函数有函数 xQueueSend() 、 xQueueSendToBack() 、xQueueSendToFront()、xQueueOverwrite(),虽然有四个,但是这些函数实际上都是宏定义,四个函数最终调用的是xQueueGenericSend()函数,只不过传入的参数不一样即入队的方式会有所差异而已,所以任务中入队我们只需要掌握一个函数xQueueGenericSend()即可。
BaseType_t xQueueGenericSend( QueueHandle_t xQueue, const void * const pvItemToQueue, TickType_t xTicksToWait, const BaseType_t xCopyPosition )
函数参数:
1.xQueue:要写入的队列
2.pvItemToQueue:要写入的消息(数据的地址)
3.xTicksToWait:阻塞超时时间(当队列为满,是否需要进行阻塞等待)
4.xCopyPosition:要写入的位置:(尾部入队、头部入队、覆写入队)
函数返回值:
1.pdTRUE:写入成功
2.errQUEUE_FULL:队列满,写入失败
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; configASSERT( pxQueue );/* 检查参数的合法性:要写入数据地址不为NULL,消息的大小uxItemSize不为0 */ configASSERT( !( ( pvItemToQueue == NULL ) && ( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) );/* 限制:当队列写入方式是覆写入队,队列的长度必须为1 */ configASSERT( !( ( xCopyPosition == queueOVERWRITE ) && ( pxQueue->uxLength != 1 ) ) ); #if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || ( configUSE_TIMERS == 1 ) ) { configASSERT( !( ( xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED ) && ( xTicksToWait != 0 ) ) ); } #endif for( ; ; ) { /* 关中断 */ taskENTER_CRITICAL(); { /* 只有队列有空闲位置或者为覆写入队,队列才能被写入消息 */ if( ( pxQueue->uxMessagesWaiting uxLength ) || ( xCopyPosition == queueOVERWRITE ) ) { traceQUEUE_SEND( pxQueue );/* 此宏用于使能启用队列集 */ #if ( configUSE_QUEUE_SETS == 1 ) { /* 关于队列集的代码省略 */ } else /* configUSE_QUEUE_SETS */ {/* 将消息拷贝到队列的环形存储区的指定位置(即消息入队) */xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );/* 如果队列有阻塞的读取任务,请立马唤醒它 */if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE ){/* 将读取阻塞任务从队列读取任务阻塞列表中移除, 因为此时,队列中已经有消息可读取了(可出队) */if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE ){/* 如果被唤醒的任务比当前任务的优先级高,应立即切换任务 */queueYIELD_IF_USING_PREEMPTION();}else{mtCOVERAGE_TEST_MARKER();}}else if( xYieldRequired != pdFALSE ){/* 在互斥信号量释放完且任务优先级恢复后,需要进行任务切换 (这是关于信号量的暂且不要管) */queueYIELD_IF_USING_PREEMPTION();}else{ mtCOVERAGE_TEST_MARKER();} } #endif /* configUSE_QUEUE_SETS *//* 开中断(退出临界区) */taskEXIT_CRITICAL();return pdPASS; } else {/* 此时不能写入消息,则需要判断是否设置的阻塞时间 */if( xTicksToWait == ( TickType_t ) 0 ){/* 队列已满,未指定阻止时间(或阻止时间已过期),立即返回入队失败。 */taskEXIT_CRITICAL();/* 用于调试,不用理会 */traceQUEUE_SEND_FAILED( pxQueue );/* 入队失败,返回队列满错误 */return errQUEUE_FULL;}else if( xEntryTimeSet == pdFALSE ){/* 队列满,并指定了阻塞时间(则任务需要阻塞),所以需要记录下此时系统节拍计数器的值和溢出次数用于下面对阻塞时间进行补偿 */vTaskInternalSetTimeOutState( &xTimeOut );xEntryTimeSet = pdTRUE;}else{/* Entry time was already set. */mtCOVERAGE_TEST_MARKER();} }}/* 开中断(退出临界区) */taskEXIT_CRITICAL(); /* 中断和其他任务现在可以向队列发送和接收 *//* 挂起任务调度器 */ vTaskSuspendAll();/* 队列上锁 */ prvLockQueue( pxQueue ); /* 判断阻塞时间补偿后,是否还需要阻塞 */ if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ) { /* 阻塞时间补偿后,还需要进行阻塞 再次确认队列是否为满 */ if( prvIsQueueFull( pxQueue ) != pdFALSE ) { /* 用于调试,不用理会 */ traceBLOCKING_ON_QUEUE_SEND( pxQueue );/* 将任务添加到队列写入阻塞任务列表中进行阻塞 */ vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToSend ), xTicksToWait ); /* 解锁队列 */ prvUnlockQueue( pxQueue ); /* 恢复任务调度器,判断是否需要的进行任务切换 */ if( xTaskResumeAll() == pdFALSE ) { portYIELD_WITHIN_API(); } } else { /* 解锁队列 */ prvUnlockQueue( pxQueue );/* 恢复任务调度器 */ ( void ) xTaskResumeAll(); } } else { /* 已超时,解锁队列 */ prvUnlockQueue( pxQueue );/* 恢复任务调度器 */ ( void ) xTaskResumeAll();/* 用于调试,不用理会 */ traceQUEUE_SEND_FAILED( pxQueue );/* 返回队列满错误 */ return errQUEUE_FULL; } } /*lint -restore */}
又是好长的一串代码,但实际上我们要关注的并不多,不想看上述代码的可以直接看下面一段一段的解析。
1.首先是参数的合法性检查,这里我们跳过;
2.一进函数先关中断
为什么要关中断(保护临界资源)?
因为队列可以很多任务或者中断都可以读写,但是同一时间不能两个读写队列,就跟全局变量一样,同时读或写就会出问题,所以FreeRTOS通过关中断来解决这个问题:关中断可以防止任务切换、中断的发生,这样一来正在读或写队列的任务或中断就可以独占队列(没有其他任务或中断来打扰)。
3.判断是否能写队列
如果队列有空闲位置肯定可以写队列。
没有的话就要等待或者返回错误了,我们先讲有空闲位置的分支,再讲没有空闲位置的分支。
4.如果可以写队列,则拷贝消息到队列(包括: 尾部入队、头部入队、覆写入队),调用的函数为prvCopyDataToQueue()
static BaseType_t prvCopyDataToQueue( Queue_t * const pxQueue,
const void * pvItemToQueue,
const BaseType t xPosition)
这一步完成后,实际上已经完成了写队列的操作,接下来的工作就是把链表里面等待接收队列里面信息的任务唤醒了。
当然写队列有不同的方式,xPosition代表写入位置的区别,因此就介绍一下尾部入队、头部入队的区别,覆写入队用的比较少,可以理解为一个队列里面只能容许一个消息存在的情况下,向这个队列进行重复的写入,其实也没啥特殊的。
尾部入队的操作流程是:
else if( xPosition == queueSEND_TO_BACK ){( void ) memcpy( ( void * ) pxQueue->pcWriteTo, pvItemToQueue, ( size_t ) pxQueue->uxItemSize ); pxQueue->pcWriteTo += pxQueue->uxItemSize;if( pxQueue->pcWriteTo >= pxQueue->pcTail ) pxQueue->pcWriteTo = pxQueue->pcHead;}else{mtCOVERAGE_TEST_MARKER();}}
直接看流程图
然后是头部入队
( void ) memcpy( ( void * ) pxQueue->u.pcReadFrom, pvItemToQueue, ( size_t ) pxQueue->uxItemSize ); pxQueue->u.pcReadFrom -= pxQueue->uxItemSize;if( pxQueue->u.pcReadFrom pcHead ) {pxQueue->u.pcReadFrom = ( pxQueue->pcTail - pxQueue->uxItemSize );}else{mtCOVERAGE_TEST_MARKER();}
注意看,头部入队和尾部入队操作的指针是不一样的,尾部入队操作的是pcWriteTo,但是头部入队操作的指针是pcReadFrom ,这正是为什么头部入队的消息可以先被读出来的原因。以及使用头部入队的时候,pcReadFrom是减去uxItemSize的,但是尾部入队的时候。pcWriteTo是加上uxItemSize的,这就是两者的区别。
5.入队后,队列消息数加1
6.判断等待接收列表是否有任务?
判断等待接收列表是否有任务,如果列表不为空,说明有任务在等待读取队列的消息,此时需要将任务唤醒,因为上一步操作已经向队列中写入了消息,所以队列有消息可以出队(所以需要唤醒在等待读取队列的任务)。前面操作的都是存储空间,也就是环形缓冲区,现在要操作的就是链表了。
唤醒任务:依靠xTaskRemoveFromEventList()函数将任务唤醒,
唤醒任务函数xTaskRemoveFromEventList()源码如下所示:
BaseType_t xTaskRemoveFromEventList( const List_t * const pxEventList ){ TCB_t * pxUnblockedTCB; BaseType_t xReturn; /* 事件列表按优先级顺序排序,因此可以删除列表中的第一个,因为它已知优先级最高。 * 从延迟列表中删除TCB,并将其添加到就绪列表中 * 如果事件是针对已锁定的队列,则此函数将永远不会被调用-队列上的锁定计数将被修改。 * 这意味着此处保证对事件列表的独占访问权限。 */ configASSERT( pxUnblockedTCB );/* 在阻塞事件列表中移除该任务控制块 */ listREMOVE_ITEM( &( pxUnblockedTCB->xEventListItem ) ); /* 调度器未被挂起 */ if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ) { /* 将任务移出延时链表 */ listREMOVE_ITEM( &( pxUnblockedTCB->xStateListItem ) );/* 将任务添加到就绪链表 */ prvAddTaskToReadyList( pxUnblockedTCB ); /* 关于低功耗先不管 */ #if ( configUSE_TICKLESS_IDLE != 0 ) { prvResetNextTaskUnblockTime(); } #endif } else { /* 无法访问延迟列表和就绪列表,因此请保持此任务挂起,直到恢复调度程序 */ listINSERT_END( &( xPendingReadyList ), &( pxUnblockedTCB->xEventListItem ) ); } if( pxUnblockedTCB->uxPriority > pxCurrentTCB->uxPriority ) { /* 如果从事件列表中删除的任务的优先级高于调用任务,则返回 true 表明需要进行一次任务切换*/ xReturn = pdTRUE; /* 标记收益处于挂起状态, 以防用户未将“xHigherPriorityTaskWoken”参数用于 ISR 安全 FreeRTOS 函数 */ xYieldPending = pdTRUE; } else { xReturn = pdFALSE; } return xReturn;}
这里面又涉及到很多的链表操作的知识了,如果这里面又要深入探讨,内容不知道要增加多少,本次只是想讲清楚队列的知识,因此不再深入讲解, 读者需要了解的就是
唤醒任务xTaskRemoveFromEventList()函数:
1.首先将任务从事件列表(xTasksWaitingToSend或xTasksWaitingToReceive)中移除,该任务是列表中优先级最高的。因为事件列表是按优先级大小排序的(优先级最高的排第一个)。
2.然后将该任务从延时链表中移除,然后将任务添加到就绪链表,此时任务就被真正被唤醒。
如果被唤醒的任务比当前正在运行的任务的优先级更高则需要立即进行一次任务切换。
到此为止都为写队列成功的场景,没有遇到什么阻碍,但实际上如果当前队列已满,则我们不能进行队列的写入,,则需要判断任务是否需要阻塞?
如果发生阻塞,本质上就是将其挂入等待发送列表xTasksWaitingToSend和阻塞列表以及延时列表中去,这里我们重点还是放在队列上面。
到了这里,其实我们已经完成了队列的写入操作,因此做一个小小的总结:
1.首先要入队判断能否可以入队
2.若可以入队,则拷贝要入队的消息到指定的队列存储区的位置(入队方式不同,入队位置略有差异),然后判断是否有任务在等待读队列,如果有则立马唤醒它,如果唤醒的任务的优先级大于当前任务则立即切换任务,最后返回入队成功。
3.若不能入队(队列已满(不为覆写入队)),则需要判断任务是否需要阻塞,不阻塞返回失败,阻塞则挂入相应的列表当中去。
二、中断级
在中断中使用入队函数也有4个,xQueueSendFromISR() 、
xQueueSendToBackFromISR()、xQueueSendToFrontFromISR()、xQueueOverwriteFromISR(),与任务级一样他们也都是宏定义,真正执行的的xQueueGenericSendFromISR()函数,不过是入队方式的区别罢了。
xQueueGenericSendFromISR()函数原型:
BaseType_t xQueueGenericSendFromISR( QueueHandle_t xQueue,
const void * const pvItemToQueue,
BaseType_t * const pxHigherPriorityTaskWoken,
const BaseType_t xCopyPosition )
函数参数:
xQueue:待写入的队列
pvItemToQueue:待写入的消息
pxHigherPriorityTaskWoken:是否需要任务切换标记,为pdTRUE表示需要任务切换,为pdFALSE表示不需要任务切换。
xCopyPosition:队列写入方式(尾部入队、头部入队、覆写入队)
函数返回值:
pdTRUE:入队成功
errQUEUE_FULL:队列满,入队失败
BaseType_t xQueueGenericSendFromISR( QueueHandle_t xQueue, const void * const pvItemToQueue, BaseType_t * const pxHigherPriorityTaskWoken, const BaseType_t xCopyPosition ){ BaseType_t xReturn; UBaseType_t uxSavedInterruptStatus; Queue_t * const pxQueue = xQueue; configASSERT( pxQueue ); configASSERT( !( ( pvItemToQueue == NULL ) && ( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) );/* 这里限制了只有在队列长度为 1 时,才能使用覆写 */ configASSERT( !( ( xCopyPosition == queueOVERWRITE ) && ( pxQueue->uxLength != 1 ) ) );/* 只有受 FreeRTOS 管理的中断才能调用该函数 */ portASSERT_IF_INTERRUPT_PRIORITY_INVALID(); /* 队列没有空闲位置时不会阻塞在这里也不会被阻塞 而且返回错误, 也不要直接唤醒在阻塞的等待接收的任务 而是返回一个标志来说明是否需要上下文切换 */ /* 屏蔽受 FreeRTOS 管理的中断,并保存,屏蔽前的状态,用于恢复 */ uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR(); { /* 队列有空闲的写入位置,或为覆写才允许入队 */ if( ( pxQueue->uxMessagesWaiting uxLength ) || ( xCopyPosition == queueOVERWRITE ) ) { /* 获取任务的写入上锁计数器 */ const int8_t cTxLock = pxQueue->cTxLock;/* 获取队列中非空闲位置的数量 */ const UBaseType_t uxPreviousMessagesWaiting = pxQueue->uxMessagesWaiting; /* 用于调试,不用理会 */ traceQUEUE_SEND_FROM_ISR( pxQueue ); /* 将消息以指定的写入方式(尾入、头入、覆写)拷贝到队列存储区中 */ ( void ) prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition ); /* 如果队列已锁定,则不会更改事件列表。 这将 稍后在队列解锁时完成。*//* 判断队列的写入是否上锁 */ if( cTxLock == queueUNLOCKED ) { #if ( configUSE_QUEUE_SETS == 1 ) { /* 关于队列集的代码省略 */ } #else /* configUSE_QUEUE_SETS */ { /* 队列有阻塞的读取任务 */ if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE ) { /* 将读取阻塞任务从队列读取任务阻塞列表中移除, 因为此时,队列中已经有非空闲的项目了*/ if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE ) { /* 如果解除阻塞的任务优先级比当前任务更高 则需要标记需要进行切换任务 */ if( pxHigherPriorityTaskWoken != NULL ) { *pxHigherPriorityTaskWoken = pdTRUE; } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } /* 未其中队列集时未使用,防止编译器警告 */ ( void ) uxPreviousMessagesWaiting; } #endif /* configUSE_QUEUE_SETS */ }/* 队列写入已被上锁 */ else { /* 增加锁定计数,以便解锁队列的任务 知道数据是在锁定时发布的 */ configASSERT( cTxLock != queueINT8_MAX );/* 上锁次数加 1 **/ pxQueue->cTxLock = ( int8_t ) ( cTxLock + 1 ); } xReturn = pdPASS; }/* 无空闲的写入位置,且不覆写 */ else { /* 用于调试,不用理会 */ traceQUEUE_SEND_FROM_ISR_FAILED( pxQueue );/* 不需要阻塞,返回队列满错误 */ xReturn = errQUEUE_FULL; } }/* 恢复屏蔽中断前的中断状态 */ portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus ); return xReturn;}/*-----------------------------------------------------------*/
又是一长串的代码,但是到了现在,任务级队列的写入你已经了解了,那么中断级别的只是有一些小小的区别,相信你很快就可以弄懂。
xQueueGenericSendFromISR()函数代码分析:
中断级的入队函数基本与任务级差不多,大体流程不变,只是中断中不能阻塞,不能立马切换任务。
1.当可以入队的时候 ,中断入队与任务入队没有区别。
2.紧接着就会判断队列是否上锁,如果队列上锁则只是增加cTxLock的值,等到队列解锁再来操作事件列表,如果队列没有上锁,则判断是否有等待接收的任务,有的话就将其进行唤醒。
3.当需要任务切换时,并不会立马切换任务,只是将pxHigherPriorityTaskWoken表示需要切换任务,等快退出中断服务函数时,再调用有关任务切换的函数。
4.任务与中断中入队的区别
在FreeRTOS中,在任务与在中断中会分别实现一套API函数,中断中使用的函数的函数名含有后缀\"FromISR\"字样,表示\"从 ISR 中调用该函数\",这样做的目的是
1.中断是快进快出,执行的代码越少越好(不然会造成用户系统卡顿)。
2.在中断中是不能进入阻塞态,不然还怎么实现快进快出。
3.在中断中不能立马切换任务,因为任务切换是依靠PendSV中断来实现的,而PendSV中断是优先级最低的中断,因此在中断中只会标记需要任务切换,等到中断退出后才能进行真正的任务切换。
这里面提到了队列锁,其实在最开始就出现了这个队列锁,在这里对队列锁进行一个说明:
1.队列锁是什么
队列锁是 FreeRTOS 内部用于保护队列数据结构的一种互斥机制。当一个任务(或中断服务程序 ISR)需要访问队列(如发送数据、接收数据、查询状态)时,它会先尝试获取该队列的锁。如果获取成功,该任务就获得了对该队列的独占访问权,在此期间其他任务或中断尝试访问同一个队列的操作会被阻塞(任务)或延迟(ISR,通过使用带 FromISR
后缀的 API 和适当的同步机制),直到持有锁的任务释放该锁。这确保了在任何时刻,最多只有一个执行上下文能修改队列的内部状态(如头指针、尾指针、计数器等)。
2.为什么要使用队列锁
使用队列锁的核心目的是防止并发访问导致的数据损坏和不一致。队列是共享资源,如果多个任务或中断在没有保护的情况下同时尝试修改队列(例如一个任务正在写入队列尾部,而另一个任务同时也在写入或被抢占后另一个任务写入,或者一个任务在读队列头时另一个任务在修改队列头),很容易发生数据被覆盖、指针错乱、计数不准等问题。队列锁通过强制对队列的访问串行化(一次只允许一个操作完成),保证了每个队列操作(发送、接收等)的原子性和队列内部状态的一致性,是确保队列作为可靠通信机制的基础。
3.什么时候会使用队列锁?
每当一个任务调用 xQueueSend()
, xQueueReceive()
, uxQueueMessagesWaiting()
等标准队列 API 时,这些函数内部在执行核心操作(访问队列结构)之前会先尝试获取该队列的锁,操作完成后再释放锁。
同样,当中断服务程序 (ISR) 调用 xQueueSendFromISR()
, xQueueReceiveFromISR()
, uxQueueMessagesWaitingFromISR()
等带 FromISR
后缀的 API 时,这些函数内部也会使用特定的同步机制(通常是通过暂时提升中断优先级或屏蔽中断来实现一种轻量级的“锁”,因为 ISR 不能阻塞)来保证对队列访问的互斥性。
4.队列上锁如何起作用?
当中断服务程序操作队列并且导致阻塞的任务解除阻塞时(比如入队成功,如果有等待接收的任务则需要解除该任务的阻塞),会首先判断该队列是否上锁,如果没有上锁,则解除被阻塞的任务,还会根据需要设置任务切换的请求标志,如果队列已经上锁,则不会解除被阻塞的任务,而是将xRxLock或xTxLock加1,表示队列上锁期间出队或入队的数目,也表示有任务可以解除阻塞了,等待最后队列解锁再来处理这些需要解除阻塞的任务。
五.向队列读出消息(出队)
关于队列出队有下列四个函数,与写队列不同,读队列只能从队列头部读取消息,且读取完消息可以选择删除消息,也可以选择只读取消息而不删除。
xQueueReceive() xQueuePeek() xQueueReceiveFromISR() xQueuePeekFromISR()
首先来看任务级的出队,这里就讲解xQueueReceive()
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait )
函数参数:
xQueue:待读取的队列
pvBuffer:存储读取到的消息的缓冲区
xTicksToWait:阻塞超时时间
函数返回值:
pdTRUE:读取成功
pdFALSE:读取失败
BaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait ){ BaseType_t xEntryTimeSet = pdFALSE; TimeOut_t xTimeOut; Queue_t * const pxQueue = xQueue; /* Check the pointer is not NULL. */ configASSERT( ( pxQueue ) ); /* 仅当数据大小为零时,接收数据的缓冲区才能为 NULL(因此不会将数据复制到缓冲区中)). */ configASSERT( !( ( ( pvBuffer ) == NULL ) && ( ( pxQueue )->uxItemSize != ( UBaseType_t ) 0U ) ) ); /* Cannot block if the scheduler is suspended. */ #if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || ( configUSE_TIMERS == 1 ) ) { configASSERT( !( ( xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED ) && ( xTicksToWait != 0 ) ) ); } #endif for( ; ; ) { /* 关中断,进入临界区 */ taskENTER_CRITICAL(); { /* 记录当前队列中的消息数 */ const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting; /* 判断队列中是否有消息 */ if( uxMessagesWaiting > ( UBaseType_t ) 0 ) { /* 队列有消息,则读取消息(出队) */ prvCopyDataFromQueue( pxQueue, pvBuffer );/* 用于调试,不用理会 */ traceQUEUE_RECEIVE( pxQueue );/* 删除队列中一个消息 */ pxQueue->uxMessagesWaiting = uxMessagesWaiting - ( UBaseType_t ) 1; /* 读取完消息则队列有空闲可以入队,则判断是否有任务在等待发送消息给队列 如果有则解除xTasksWaitingToSend中优先级最高的任务的阻塞 */ if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE ) { /* 解除xTasksWaitingToSend列表中优先级最高的任务的阻塞 */ if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE ) { /* 若解除阻塞的任务的优先级高于当前任务 则立即切换任务*/ queueYIELD_IF_USING_PREEMPTION(); } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } /* 开中断,退出临界区 */ taskEXIT_CRITICAL();/* 返回出队成功 */ return pdPASS; } else { /* 若队列为空,不能出队,则需要判断任务是否需要阻塞 */ if( xTicksToWait == ( TickType_t ) 0 ) { /* 当我们设置的超时时间xTicksToWait为0 则任务不需要阻塞,立即返回队列为空错误*/ taskEXIT_CRITICAL(); traceQUEUE_RECEIVE_FAILED( pxQueue ); return errQUEUE_EMPTY; } else if( xEntryTimeSet == pdFALSE ) { /* 队列空,并指定了阻塞时间(则任务需要阻塞),所以需要记录下此时系统节拍计数器的值和溢出次数用于下面对阻塞时间进行补偿 */ vTaskInternalSetTimeOutState( &xTimeOut ); xEntryTimeSet = pdTRUE; } else { /* Entry time was already set. */ mtCOVERAGE_TEST_MARKER(); } } }/* 开中断(退出临界区) */ taskEXIT_CRITICAL(); /* 此时中断和其他任务现在可以向队列发送和接收,因为已退出临界区 */ /* 挂起任务调度器 */ vTaskSuspendAll();/* 队列上锁 */ prvLockQueue( pxQueue ); /* 判断阻塞时间补偿后,是否还需要阻塞 */ if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ) { /* 阻塞时间补偿后,还需要进行阻塞(未超时) 再次确认队列是否为空 */ if( prvIsQueueEmpty( pxQueue ) != pdFALSE ) { /* 用于调试,不用理会 */ traceBLOCKING_ON_QUEUE_RECEIVE( pxQueue );/* 将任务添加到队列读取阻塞任务列表中进行阻塞 */ vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait ); /* 解锁队列 */ prvUnlockQueue( pxQueue );/* 恢复任务调度器,判断是否需要的进行任务切换 */ if( xTaskResumeAll() == pdFALSE ) { portYIELD_WITHIN_API(); } else { mtCOVERAGE_TEST_MARKER(); } } else { /* 解锁队列 */ prvUnlockQueue( pxQueue );/* 恢复任务调度器 */ ( void ) xTaskResumeAll(); } } else { /* 已超时,解锁队列 */ prvUnlockQueue( pxQueue );/* 恢复任务调度器 */ ( void ) xTaskResumeAll(); if( prvIsQueueEmpty( pxQueue ) != pdFALSE ) { /* 用于调试,不用理会 */ traceQUEUE_RECEIVE_FAILED( pxQueue );/* 返回队列满错误 */ return errQUEUE_EMPTY; } else { mtCOVERAGE_TEST_MARKER(); } } } /*lint -restore */}/*-----------------------------------------------------------*/
最后的代码分析:
出队函数基本与入队函数是对称的,它们两个最主要的区别就是一个是判断队列满一个是判断队列空,一个是操作xTasksWaitingToReceive列表一个是操作xTasksWaitingToSend列表,一个是往队列里放东西,一个是往队列里面拿出东西。
具体需要说明的只有是怎么往队列里面拿出东西的呢,是用左手呢还是右手呢,拿当然用的是pcReadFrom指针啦
static void prvCopyDataFromQueue( Queue_t * const pxQueue, void * const pvBuffer ){if( pxQueue->uxItemSize != ( UBaseType_t ) 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,尾部入队就是应该放在队列项4,但是到了这里应该能理解头部入队和尾部入队的机制了吧。简单来说,要实现头部入队,就是要把消息放在里pcReadFrom指针下一个要读的位置。
至此,队列的读取也完成了,至于中断的队列读取过程则交给读者自己,相信经过上面的学习读者已经可以完成对于中断函数的分析了。
BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue, void * const pvBuffer, BaseType_t * const pxHigherPriorityTaskWoken ){ BaseType_t xReturn; UBaseType_t uxSavedInterruptStatus; Queue_t * const pxQueue = xQueue; configASSERT( pxQueue ); configASSERT( !( ( pvBuffer == NULL ) && ( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) ); /* 只有受 FreeRTOS 管理的中断才能调用该函数 */ portASSERT_IF_INTERRUPT_PRIORITY_INVALID(); /* 屏蔽受 FreeRTOS 管理的中断,并保存,屏蔽前的状态,用于恢复 */ uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR(); { /* 记录队列中消息数 */ const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting; /* 判断队列有消息可以嘛 */ if( uxMessagesWaiting > ( UBaseType_t ) 0 ) { /* 获取任务的读取上锁计数器 */ const int8_t cRxLock = pxQueue->cRxLock; traceQUEUE_RECEIVE_FROM_ISR( pxQueue ); /* 消息出队 */ prvCopyDataFromQueue( pxQueue, pvBuffer );/* 队列消息数减1 */ pxQueue->uxMessagesWaiting = uxMessagesWaiting - ( UBaseType_t ) 1; /* 如果队列被锁定,则不会修改事件列表。而是更新锁定计数, 以便解锁队列的任务知道 ISR 在队列锁定时已删除数据。 *//* 判断队列的读取是否上锁 */ if( cRxLock == queueUNLOCKED ) { /* 队列有阻塞的发送任务 */ if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE ) { /* 将写入阻塞任务从队列读取任务阻塞列表中移除, 因为此时,队列中已经有空闲的位置*/ if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE ) { /* 如果解除阻塞的任务优先级比当前任务更高 则需要标记需要进行切换任务 */ if( pxHigherPriorityTaskWoken != NULL ) { *pxHigherPriorityTaskWoken = pdTRUE; } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } } else { /* 增加锁定计数,以便解锁队列的任务 知道数据是在锁定时发布的 */ configASSERT( cRxLock != queueINT8_MAX ); pxQueue->cRxLock = ( int8_t ) ( cRxLock + 1 ); } xReturn = pdPASS; }/* 队列无消息可读,队列为空 */ else { /* 不需要阻塞,返回队列空错误 */ xReturn = pdFAIL; traceQUEUE_RECEIVE_FROM_ISR_FAILED( pxQueue ); } } portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus ); return xReturn;}/*-----------------------------------------------------------*/
六、总结
相信大家看到这里,已经对于什么是队列,以及队列的内部实现有了一个不错的了解,那么最后就是要回答文章开头提到的问题,从而在向他人解释,以及向面试官解释时有一个良好的措辞。
1.什么是队列,为什么要使用队列?
在 FreeRTOS 中,队列是一种任务之间的通信机制,本质是一个遵循先入先出原则的缓冲区(首先回答队列是什么)。任务和中断服务程序可以通过队列安全地传递固定大小的数据项(作用)。使用队列的核心目的是实现任务之间、任务与中断之间的可靠数据传递。它消除了直接共享全局变量带来的数据竞争风险,例如数据被意外覆盖或损坏,是构建稳定多任务系统的关键组件。
2.队列有什么特点?
队列的特点其实文章中已经进行说明了,这里再写一次
1.FIFO 行为:一般情况下队列消息是先进先出方式,但也支持后进先出 LIFO 的“覆盖写入”和紧急发送,即头部写入方式。
2.阻塞机制:当任务尝试从空队列读取或向满队列写入时,可选择阻塞等待直到条件满足,这能高效利用 CPU 时间。
3.多任务访问:队列不属于某个特定的任务,可以在任何的任务或中断中往队列中写入消息,或者从队列中读取消息。
因为同一个队列可以被多个任务读取,因此可能会有多个任务因等待同一个队列,而被阻塞,在这种情况下,如果队列中有可用的消息,那么也只有一个任务会被解除阻塞并读取到消息,并且会按照阻塞的先后和任务的优先级,决定应该解除哪一个队列读取阻塞任务;
4.传递数据或指针(可直接传递数据副本,也可传递指向数据的指针,后者效率高但需谨慎管理内存和生命周期)。
5.当在中断中读写队列时,如果队列空或满,不会进行阻塞,直接返回队列空或队列满错误,因为中断要的就是快进快出。
3.队列的内部实现机制是什么样的?
FreeRTOS队列的内部实现是一个精巧的机制,其核心在于通过一个预分配的固定大小缓冲区作为环形队列(FIFO)来存储数据项,每个数据项的大小和队列长度在创建时确定。当任务调用xQueueSend()
或xQueueReceive()
等API时,内核通过复制的方式将数据移入或移出队列缓冲区,确保数据所有权清晰,避免共享内存问题。队列结构体(Queue_t
)不仅管理缓冲区的读写指针(pcHead
, pcWriteTo
, u.xQueue.pcReadFrom
),还维护着两个关键的任务等待列表(xTasksWaitingToSend
和 xTasksWaitingToReceive
):当任务尝试写入满队列或读取空队列时,内核会根据阻塞时间将其挂起到相应列表并触发调度器切换任务;反之,当写入操作使空队列变为非空或读取操作释放队列空间时,内核会从等待列表中唤醒优先级最高的挂起任务(或标记调度需求)。
队列操作通过关闭中断或使用互斥信号量(对互斥队列)实现原子性,确保多任务并发访问的安全性;中断服务程序(ISR)则使用带FromISR
后缀的专用API,这些API通过检查pxHigherPriorityTaskWoken
变量避免在中断上下文中直接切换任务。