【RTOS学习】源码分析(通用队列 && 队列 && 队列集)

🐱作者:一只大喵咪1201
🐱专栏:《RTOS学习》
🔥格言:你只管努力,剩下的交给时间!
图

前面本喵讲解了和任务相关的FreeRTOS源码,进行再来介绍一下用于任务间通信的几种数据结构源码。

🍓通用队列

队列(Queue)、队列集(Queue Set)、信号量(Semaphore)、互斥量(Mutex)、递归互斥量,这5种机制的核心都是通用队列(xQueueGenericCreate)

tu
上面函数都调用了xQueueGenericCreate,创建一个通用队列。这个函数原型为:

QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength,
                                   const UBaseType_t uxItemSize,
                                   const uint8_t ucQueueType );

参数含有如下:

参数含义
uxQueueLength队列长度,对于信号量、互斥量,这个参数为1
uxItemSize队列中数据长度,
对于队列:这个参数由用户设置;
对于队列集:这个参数是sizeof(Queue_t *)
对于信号量、互斥量:这个参数是0
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

根据类型创建不同的类型结构。

tu
如上图所示队列结构体xQUEUE定义。这个结构体可以用来实现队列、队列集、信号量、互斥量:

  • pcWriteTo、xQueue里的pcReadFrom用来维护环形缓冲区:队列/队列集用来读写数据。
  • xTasksWaitingToSend:用来管理"想发送数据,但是没有空间,因此阻塞"的任务,信号量、互斥量不会用到它。
  • xTasksWaitingToReceive:用来管理"想读取数据,但是没有数据,因此阻塞"的任务。
  • uxMessagesWaiting:队列/队列集用来记录有多少个有效数据,信号量/互斥量用来记录数值。

图
如上图所示,创建不同结构的函数虽然不同,但是最终调用的都是xQueueGenericCreate函数,只是传入的参数不同。

🍓队列

这里创建的是可以存放数据的队列。

🍅创建

图
如上图代码所示,使用xQueueCreate创建队列,在调用时传入队列长度和每一项大小两个参数,该函数又是xQueueGenericCreate的封装,本质上就是在创建一个通用队列,只是它的类型是queueQUEUE_TYPE_BASE表明这是一个用来存放数据的队列。

tu
如上图创建通用队列xQueueGenericCreate函数,会使用pvPortMalloc在堆区上开辟一段空间,这段空间包括通用队列结构体Queue_tuxQueueLength * uxItemSize个字节用来存放数据的的空间。

然后再让pucQueueStroage跳过sizeof(Queue_t)个字节,指向存放数据的起始位置,再调用prvInitialiseNewQueue来初始新化队列:

tu
如上图所示prvInitialiseNewQueue函数,先让Queue_t中的pcHead环形缓冲区头指针指向存放数据的起始位置,然后再给uxLengthuxItemSize成员赋值,再调用xQueueGenericReset来复位通用队列。

图

如上图所示xQueueGenericReset函数,关闭中断后进入临界区,让环形缓冲区的尾指针pcTail指向存放数据的末尾位置,让pcWriteTo写指针指向存放数据的起始位置,并且给记录队列中有效数据个数的uxMessageWaiting变量赋值为0。

最重要的时,让读环形缓冲区的指针pcReadFrom指向存放数据的最后一个数据所在位置,并不指向头,而是指向上一次读数据的位置。

然后就是调用vListInitialise来初始化队列结构体Queue_t中的两个链表,一个用来管理因写数据而阻塞的任务,另一个用来管理因读数据而阻塞的任务。最后恢复中断出临界区。


图
如上图所示便是队列创建好后的示意图,总的来说,创建过程分为如下几步:

  1. 在堆区上开辟一段空间,用来存放Queue_t队列头和环形缓冲区。
  2. 让头指针pcHead和写指针pcWrite指向存放数据空间的起始位置。
  3. 让联合体QueuePointers_t中的尾指针pcTail指向数据存满后的最后位置,读指针pcReadFrom指向存放最后一个数据的位置。
  4. 初始化xTasksWaitingToSendxTasksWaitingToReceive两个链表。
  5. Queue_t中其他成员赋予合适的值。

🍅写数据

图
如上图,使用xQueueSned函数向队列中写数据,最后会调用通用写数据函数xQueueGenericSend,在调用时会指定写数据的位置queueSEND_TO_BACK

队列有空:

tu

如上图代码所示,当队列中有空位置时,也就是uxMessageWaiting < uxLength,调用prvCopyDataToQueue将数据复制到环形缓冲区中。


tu
如上图prvCopyDataToQueue函数,使用memcpy将要写入队列的数据复制到队列中,然后更新pcWriteTo写指针,如果和尾指针pcTail相同,则让其指向pcHead来维持环状。最后再让有效数据个数uxMessageWiting加一。


然后再使用listLIST_IS_EMPTY来判断一下管理读取数据链表中是否有任务在等待,如果有,则调用xTaskRemoveFromEventList将其移除。然后写数据成功返回。


图
如上图xTaskRemoveFromEventList函数,首先从等待读取数据的链表中选出要唤醒任务的TCB,然后将TCB从该链表(事件链表)中移除。

如果此时调度器是开着的,则将唤醒的任务放入到就绪链表中,如果是调度器是关着的,则将唤醒的任务放入到xPendingReadyList链表中,待调度器打开后从该链表中将TCB放入就绪链表中。

如果唤醒的任务优先级高于现在正在执行的任务,则发起调度。


队列没空:

tu
如上图xQueueSend函数中队列没空位置的处理代码,如果时间xTicksToWait为0,说明不愿意等待,则立刻返回errQUEUE_FULL表示队列满了,无法写入数据。

如果愿意等待,则调用vTaskInternalSetTimeOutState设置一下时间:

图
如上图所示,就是记录一下当前的系统时间,方便后面进行超时唤醒。


图
在记录完时间以后立刻恢复中断,然后再关闭调度器,因为关闭中断的代价太大了,能关闭调度器就不关中断。

检查一下该任务等待是否超时,如果没有超时则再确认一下队列中真的没空。

  • 因为在恢复中断后,虽然调度器关了,但是该函数xQueueSend随时可能被中断打断,如果打断了,中断函数执行一定时间后再次轮到该函数执行,很有可能就超时了,也有可能中断函数会从队列中读取数据,此时队列就不空了。

然后再调用vTaskPlaceOnEventList将这个写数据的任务放入到等待写数据的链表中。


图
如上图vTaskPlaceOnEventList函数所示,在该函数中,除了将放入到等待写入数据的事件链表中外,还要将自己放入到等待超时时间到来的延时状态链表中。


重新开启调度器。


总的来说,向队列中写数据分为如下几步:

  1. 如果队列不满,则将要写的数据复制到队列中,并且从等待读数据的链表xTasksWaitingToReceive唤醒一个任务(如果有任务在等待的话)。

  2. 如果队列满了,则看该任务是否愿意等待:

    • 不愿意等待,直接错误返回,表示队列满了无法写入。
    • 愿意等待,则将其放入等待写数据的链表xTasksWaitingToSend中,并且根据超时时间也将其放入延时链表xDelayList中。

🍅读数据

队列中有数据:

图
如上图xQueueReceive读取数据的函数,先得到该队列中有效数据个数,如果大于0说明队列中有数据,此时调用prvCopyDataFromQueue函数从队列中复制一个数据到目标地址。

然后调用listLIST_IS_EMPTY判断等待写数据的链表xTasksWaitingTosend中是否有任务,如果有,则此时队列中有空位置,调用xTaskRemoveFromEventList唤醒一个任务。

最后读数据成功返回。

队列没有数据:

图
如上图xQueueReceive函数中部分代码所示,当队列中没有数据时,会先判断该任务是否愿意等待数据到来,如果不愿意,则直接错误返回,表示队列为空。

如果愿意等待,则设置一下超时时间,在恢复中断,关闭调度器后,再次确认是否超时和队列是否为空,原因和前面写数据时一样。

然后将自己的TCB放入到等待读取数据的链表xTasksWaitingToReceive中,然后主动发起一次调度。


总的来说,读数据和写数据非常类似,主要分为如下几步:

  1. 如果队列中有数据,则从队列中将数据复制出去,并且从等待写数据的链表中唤醒任务(如果有任务在等待)。
  2. 如果队列中没有数据:
    • 如果不愿意等待,则直接错误返回。
    • 如果愿意等待,则将自己放入到等待读数据的事件链表中,并且根据超时时间将自己也放入到延时链表中。

🍅被唤醒

回答一个问题,为什么前面本喵讲解的这些代码都在一个for循环中呢?
图
如上图所示,无论是xQueueSend还是xQueueReceive,所有对队列的操作和判断都是在这个死循环for中。

图
如上图代码所示,在将任务放入到事件链表中后会发起一次调度,此时任务本身就处于阻塞状态了。

当被唤醒时,有两种可能:

  • 超时唤醒
  • 可以写数据/读数据

无论哪种情况下被唤醒,该任务都是从阻塞处恢复执行。因为是for循环,所以该任务会重新对队列进行一遍前面的判断和操作,在重新判断和操作的过程中:

  • 被其他任务唤醒:可以写/读数据

图
如上图,在重新判断和操作的过程中,在正常读取数据或者写入数据后,执行return pdPASS成功返回

  • 超时唤醒

图

如上图所示,超时唤醒后,执行return errQUEUE_XXX错误返回

🍓队列集

队列集的核心,就是队列。

🍅创建

图
如上图,调用xQueueCreateSet创建队列集的本质就是调用xQueueGenericCreate来创建通用队列,只是队列类型是queueQUEUE_TYPE_SET,表示这是一个队列集。

图
如上图,其他和创建普通队列一样,只是在prvInitialiseNewQueue中初始化新队列时,将属于队列集的pxQueueSetContainer设置为NULL

图
如上图所示,队列集创建好后,和队列结构几乎相同,只是队列集中每个数据存放的都是Queue_t*队列指针,而且多了一个struct QueueDefinition * pxQueueSetContainer成员,且其初始值是NULL

  • 如果使用队列集的话,普通队列中也会多出pxQueueSetContainer成员,且初始值是NULL

🍅操作

将队列添加到队列集中:

图
如上图xQueueAddToSet函数所示,只是让要添加到队列集中的队列里的pxQueueSetContainer成员指向队列集xQueueSet

也就是说,被添加到的队列集中的队列,都可以通过pxQueueSetContainer指针找到队列集xQueueSet

写队列集:

并没有专门的任务来写队列集,写队列集只是写队列时顺带手的事:

tu
上图所示,在xQueueGenericSend中向队列写数据时,如果使用了队列集,则在调用prvCopyDataToQueue将要写的数据复制到队列中后,再调用prvNotifyQueueSetContainer将队列的地址写入到pxQueueSetContainer指向的队列集中。

图

如上图prvNotifyQueueSetContainer函数,当队列集中有空位置时,将本次写队列的队列地址复制到队列集中,然后判断一下队列集中等待读取数据的链表中是否有任务在等待,如果有则唤醒。

  • 队列集中存放的是队列的句柄。
  • 每当有任务向队列中写数据时,顺手会将要写的队列句柄写到队列集中。
  • 所以队列集的大小就是被添加到队列集中所有队列大小的总和。

只有这样才能在向队列集中写数据时有足够的空间,FreeRTOS对于写队列集也没有阻塞的机制。

对队列集:

tu
如上图,调用xQueueSelectFromSetFromISR读取队列集时,会调用xQueueReceive来读取。

传参时也没有特别传什么参数,所以和普通队列的读取是一样的:

  • 如果队列集中有数据,则读取成功并返回。
  • 如果队列集中没有数据:
    • 不愿意等待则直接错误返回
    • 愿意等待则将自己放入队列集的等待读取数据链表xTasksWaitingToReceive中阻塞等待。

读取队列集成功后返回的是队列集中存放的某个队列句柄:

图
如上图,再使用xQueueReceive从返回的队列句柄指向的队列中读取具体的数据。

🍓总结

无论是普通队列,还是队列集,都是对通用队列的进一步封装,所以说,通用队列才是核心。

  • 39
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 21
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只大喵咪1201

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值