🐱作者:一只大喵咪1201
🐱专栏:《RTOS学习》
🔥格言:你只管努力,剩下的交给时间!
目录
🍓信号量和互斥量
信号量和互斥量几乎一模一样:
创建:
如上图所示,创建时使用的都是xSemaphoreCreateXXX
函数,只是后面的XXX
不一样,其他都非常类似,而且本质上都是调用的xQueueGenericCreate
函数来创建通用队列,只是传入的参数不一样。
Give:
如上图所示,Give
时,使用的都是xSemaphoreGiveXXX
,本质上也是在调用xQueueGenericSend
向通用队列中写数据,只是参数不同。
Take:
如上图所示,Take
时,使用的都是xSemaphoreTakeXXX
,本质上也是在调用xQueueSemaphoreTake
这个函数,只是参数不同,注意,这里并不是调用xQueueGenericReceive
函数,和队列的操作不同。
可以看到,信号量和互斥量在创建,释放,申请信号量时的函数非常类似,所以本喵只需要讲解一种即可,就介绍一下最复杂的递归互斥量吧。
🍅创建
如上图代码所示,调用xSemaphoreCreateRecursiveMutex
创建递归互斥量,这是一个宏函数,会调用xQueueCreateMutex
,在该函数中会调用xQueueGenericCreate
创建通用队列。
但在这之前先让创建的队列长度为1,队列中每个数据的大小是0字节,然后再调用xQueueGenericCreate
。
根据前面创建队列时的分析我们知道,此时只会在堆区上创建一个队列结构体Queue_t
,并不会分配环形缓冲区。
然后再调用prvInitialiseMutex
函数初始化互斥量:
如上图所示prvInitialiseMutex
函数,将Queue_t
中联合体中xSemaphore
成员的xMutexHolder
用来表示锁的所有者设置为NULL
,再将uxRecursiveCallCount
递归锁计数次数设置为0。
最后再调用xQueueGenericSend
向队列中写数据,写入数据是NULL
,这里仅仅是让uxMessagesWaiting = 1
,好让互斥量有初始值。
如上图,所以此时得到的Queue_t
队列是这样的,没有存放数据的环形缓冲区,只有一个队列头,其中的成员也被赋予了合适的初始值。
🍅Take
如上图所示,使用xSemaphoreTakeRecursive
申请一把递归锁,该函数会调用xQueueTakeMutexRecursive
函数,在这个函数中,先判断锁的持有者身份xMutexHolder
,如果是当前任务pxCurrentTCB
,则说明此时是递归上锁,则将递归上锁次数uxRecursiveCallCount
加一。
如果持有者身份是NULL
或者不是当前任务,调用xQueueSemaphoreTake
申请锁,申请成功后让uxRecursiveCallCount
加一。
如上图xQueueSemaphoreTake
函数,先判断可否申请锁,也就是队列中的有效数据是否大于0,如果可以申请则将有效数据uxMessagesWaiting
的数值减一,然后通过pvTaskIncrementMutexHeldCount
函数记录持锁人身份到pxQueue->u.xSemaphore.xMutexHolder
成员中。
如上图所示,如果队列中的有效数据小于等于0,说明此时无法申请锁,如果该任务不愿意等待的话就直接错误返回,如果愿意等待,则调用vTaskInternalSetTimeOutState
记录当前时刻。
在确认要阻塞后,调用xTaskPriorityInherit
函数进行优先级继承,然后将该任务放入等待读取数据的xTasksWaitingToReceive
链表中,再主动发起调度,让当前任务阻塞。
如上图xTaskPriorityInherit
函数,如果被阻塞任务的优先级大于持锁者的优先级,并且持锁者在就绪链表中,则交换双方的位置,也就是将二者的优先级交换,并且放入对应的就绪链表中。如果持锁者不在就绪链表中,则直接将当前阻塞任务的优先级给它即可。
如果被阻塞任务的优先级小于等于持锁者的优先级,则不需要进行优先级继承。
如上图所示,当这个申请锁的任务再次被唤醒时,也是有两种情况,如果是有人释放了锁,那么for
循环中再次判断操作时会成功申请到锁,成功返回。
如果是超时被唤醒,则会先调用prvGetDisinheritPriorityAfterTimeout
将被继承的优先级恢复原样,然后错误返回。
🍅Give
如上图所示xSemaphoreGiveRecursive
,用来释放递归锁,最后会调用xQueueGiveMutexRecursive
函数,在该函数中,首先判断释放锁的是否是锁的持有者,如果是持有者,则先将递归次数减一,当该次数为0时,说明不是递归释放,则向队列中写数据,就是让有效数据uxMessagesWaiting
加一。
如果不是持有者,则直接错误返回,并不会阻塞。
- 在释放锁的过程中,并不会阻塞,如果失败就直接返回。
总的来说,使用锁的过程分为如下几步:
- 创建互斥锁并进行初始化,让锁有初始值,但是此时持锁者为
NULL
。 - 申请锁时:
- 如果不是持有者申请锁,则看有效数据个数
uxMessagesWaiting
是否大于0,如果大于0说明可以申请,如果小于等于0,说明不可以申请,此时会将申请者放入到锁的管理读取数据的事件链表中。 - 如果是锁的持有者,对于递归锁则会让递归次数增加,非递归锁和不是持有者的待遇一样。
- 如果不是持有者申请锁,则看有效数据个数
- 释放锁时:
- 如果不是持有者,直接错误返回,因为锁不能被其他任务随意释放。
- 如果是持有者,对于递归锁,则让递归次数减少,当递归次数减少为0时,则向队列中写数据,让有效数据的个数
uxMessagesWaiting
重新变为1。
🍓事件组
如上图所示事件组结构体EventGroup_t
的定义,包含两个成员:
uxEventBits
:这是一个32位的变量,用来存放事件,只是用低24位,每一个比特位代表一个事件。xTasksWaitingForBits
:这是一个链表头,该链表用来存放因等待事件就绪而阻塞的任务TCB。
如上图xEventGroupCreate
函数所示,在创建事件组时,先在堆区上开辟一块存放EventGroup_t
结构体的空间,然后将结构体中表示事件值的uxEventBits
清0,再初始化一下链表xTasksWaitingForBits
。
如上图所示,每个任务TCB里的xEventListItem
事件链表中,每个链表项中的xItemValue
,该32位的变量也可以用来表示事件。
- 高8位用来表示控制位:比如等待后是否清除事件等等。
- 低24位用来表示事件:每一位表示一个事件和
EventGroup_t
中的事件位对应。
🍅设置事件
如上图xEventGroupSetBits
函数所示,在该函数中,先获取事件组中的链表里的第一个链表项,这里可能管理着因正在等待事件就绪而处于阻塞状态的任务。
然后将要设置的事件值,使用或等 的方式赋值给EventGroup_t
中的uxEventBits
。
如上图所示代码,在将事件值设置好以后,需要判断是否有任务可以唤醒,使用while
循环遍历链表中的所有任务。
在判断过程中,先拿到事件控制位uxControlBits
,也就是在等待事件的任务对事件的态度,如果不是eventWAIT_FOR_ALL_BITS
,说明当前遍历的任务不要求所有等待是事件都就绪,只要uxBitsWaitedFor & pxEventBits->uxEventBits != 0
,说明只有一个或者多个事件就绪,此时将xMatchFound = pdTRUE
,表示可以唤醒链表中等待的任务。
如果控制位表示要等待所有事件就绪,则只有uxBitsWaitedFor & pxEventBits->uxEventBits == uxBitsWaitedFor
时,也就是所有等待事件都就绪时,才可以唤醒链表中等待的任务。
接下来如果有任务可以唤醒时,从正在遍历的当前任务中uxControlBits
拿到是否要清除已经就绪的事件所在的bit。
之后再调用vTaskRemoveFromUnorderedEventList
将要唤醒的任务从事件组的链表中移除,并且设置标志eventUNBLOCKED_DUE_TO_BIT_SET
,表示该任务被唤醒是由于等待事件成功。
最后返回事件组中的事件值pxEventBits->uxEventBits
。
在设置事件值时主要分为三大步:
- 设置要等待的事件值。
- 唤醒事件组链表中正在等待事件就绪的任务:
- 等待事件的任务有要求全部事件就绪时才会被唤醒。
- 等待事件的任务也有只要求有事件就绪时就可以被唤醒。
- 在退出函数时,根据控制位决定是否清除事件组中已经就绪的事件。
🍅等待事件
如上图xEventGroupWaitBits
函数,该函数是由某个任务调用的,用来等待事件组中要等待的事件。
首先就是获取事件组中的事件值uxCurrentEventBits
,然后判断其与等待任务要等待的事件uxBitsToWaitFor
是否相等,要根据xWaitForAllBits
决定是所有事件都就绪才符合等待要求还是有事件就绪就符合等待要求。
当符合等待要求时,根据控制位决定是否在退出该函数前将事件组中已经就绪的事件值清除,如果不符合等待要求,且该任务不愿意等待,则超时返回,如果愿意等待,则将该任务放入到事件组用来维护等待事件的任务链表中。
然后主动发起调度,当前任务就阻塞在这里了。
当该任务再次被唤醒时,有可能是等待的事件就绪了被唤醒,也有可能是因为超时而被唤醒:
如上图所示,当等待事件的任务再次被唤醒时,根据eventUNBLOCKED_DUE_TO_BIT_SET
判断一下是否因为事件就绪被唤醒,如果不是,则说明是超时,再判断一次事件是否就绪,没有就绪则超时返回。
如果是事件就绪而被唤醒,等待成功返回。
等待过程中注意分为三步:
- 判断要等待的事件和事件组中的事件值,根据任务指定的控制值决定是所有事件都就绪才算等待成功还是只要有事件就绪就算等待成功。
- 如果等待成功,则成功返回,等待不成功,则根据是否愿意等待决定是错误返回还是放入事件组的阻塞链表中。
- 处于阻塞状态再次被唤醒后,根据
eventUNBLOCKED_DUE_TO_BIT_SET
位判断是事件就绪被唤醒还是超时唤醒。
🍅同步点
如上图xEventGroupSync
函数,调用该函数可以实现同步点,在函数内部,首先将要等待的事件设置到事件组中:
- 如果在设置过程中,其他等待同步点的事件产生,则唤醒其他任务
- 并且判断自己要等待的事件也全部发生,自己不会被阻塞。
如果自己要等待的事件没有全部就绪,说明其他任务也没有被唤醒,此时将当前任务继续添加到事件组的阻塞等待链表中。
如果当前任务愿意等待,则主动发起调度,阻塞到这里。
再次被唤醒后,如果eventUNBLOCKED_DUE_TO_BIT_SET
不为1,说明是超时唤醒,则错误返回,如果该位为1,说明是事件被设置唤醒。
多个等待同步的任务都会从这里被唤醒,开始一起继续执行。
🍓任务通知
如上图所示,使用队列、信号量、事件组时,我们都要事先创建对应的结构体,双方通过中间的结构体通信。
如上图所示,使用任务通知时,任务结构体TCB中就包含了内部对象,可以直接接收别人发过来的"通知"。
使用任务通知时,可以明确指定:通知哪个任务。
如上图所示,每个任务都有一个结构体:TCB(Task Control Block),里面有2个成员:
- 一个是uint8_t类型的
ucNotifyState
数组,用来表示通知状态。 - 一个是uint32_t类型
ulNotifiedValue
数组,用来表示通知值。
数组的大小#define configTASK_NOTIFICATION_ARRAY_ENTRIES 1
为1,可以将数组看成一个变量。
通知状态有3种取值:
taskNOT_WAITING_NOTIFICATION
:任务没有在等待通知taskWAITING_NOTIFICATION
:任务在等待通知taskNOTIFICATION_RECEIVED
:任务接收到了通知,也被称为pending(有数据了,待处理)
🍅发通知
如上图所示xTaskNotify
,调用该函数项目标任务发出通知时,会调用xTaskGenericNotify
,在该函数中,首先获取被通知任务的状态值ucNotifyState
,然后将该值设置为taskNOTIFICATION_RECEIVED
,表示已经对该任务发出了通知。
根据eAction
对被通知的任务通知值ulNotIfiedValue
进行操作,可以赋值,可以加加,可以覆盖,也可以不做任何操作等等。
操作完毕后,判断一下被通知之前目标任务的状态,如果是taskWAITING_NOTIFICATION
,说明目标任务在等待通知而处于阻塞状态,所以此时将目标任务放入到就绪链表中,如果被通知任务的优先级更高,则主动发起调度。
向任务发起通知总的来说就三步:
- 设置通知状态为
taskNOTIFICATION_RECEIVED
。 - 根据
eACction
操作通知值ulNotIfiedValue
。 - 任务如果原本阻塞,则将其放入就绪链表中。
🍅等待通知
如上图xTaskNotifyWait
函数,任务调用该函数等待任务通知,最后会调用xTaskGenericNotifyWait
函数,在该函数内,首先判断当前任务的任务状态ucNotifyState
。
- 如果状态值不是
taskNOTIFICATION_RECEIVED
,说明没有接到通知:- 根据控制位决定是否在入口处清除指定事件。
- 将当前任务状态设置为
taskWAITING_NOTIFICATION
,表示正在等待通知。 - 如果愿意等待,则将当前任务放入到延时链表中,然后发起调度,当前任务阻塞。
- 被唤醒或者第一次调用就接收到任务通知:
- 再次判断任务状态,如果不是
taskNOTIFICATION_RECEIVED
,说明是超时唤醒,直接错误返回。 - 如果是
taskNOTIFICATION_RECEIVED
,说明接收到任务通知被唤醒,根据控制位决定是否在出口处清除事件。
- 再次判断任务状态,如果不是
🍓总结
虽然互斥量信号量,事件组名字中不包含队列,但其本质上还是使用的通用队列,只是该队列中不存放数据本身,只靠Queue_t
中的成员。
对于任务通知,不如说是通知任务,更是没有用于两个任务间通信的结构,直接从一个任务指定通知另一个任务,改变的是TCB中的任务通知状态和任务通知值。