1. 简介
对于多任务的系统,对于某个共享资源(全局变量,外设等)的并发访问容易引起数据的不一致性,这将会导致一些意外的结果。而共享资源的同步访问则是为了解决这个问题而提供的一种同步机制,例如临界区、互斥锁等,他们的基本思想就是互斥访问。本文将介绍FreeRTOS中的临界区、互斥锁与递归互斥锁。
2. 临界区和挂起调度器
2.1. 基本临界区(Basic Critical Sections)
基本临界区:被taskENTER_CRITICAL()
和taskEXIT_CRITICAL()
包围的代码区域。在临界区内的代码不会被其他任务和中断打断。
FreeRTOS也提供了ISR版本的临界区:taskENTER_CRITICAL_FROM_ISR()
和taskEXIT_CRITICAL_FROM_ISR()
。
具体的实现如下:
-
进入临界区(vPortEnterCritical()
):
{
首先屏蔽小于系统最大优先级的所有中断。(`ulPortSetInterruptMask()`):
{
屏蔽CPU的中断(`portCPU_IRQ_DISABLE()`)。
设置中断优先级屏蔽系统寄存器为`configMAX_API_CALL_INTERRUPT_PRIORITY << portPRIORITY_SHIFT`。
恢复CPU的中断屏蔽(`portCPU_IRQ_ENABLE()`)。
}
增加中断嵌套计数(`ulCriticalNesting++`)。
如果调用者处于中断上下文,却调用了该接口,则会发生assert。
}
中断上下文的版本临界区进入流程:实际上只是做了任务上下文中的第一步,即只调用了ulPortSetInterruptMask()
。
-
退出临界区(vPortExitCritical()
):
{
先检查当前临界区嵌套是否大于0,如果不是,则直接返回;否则需要执行下面的操作:
减少临界区嵌套数(`ulCriticalNesting--`)。
检查是否已经退出所有嵌套的临界区,即当前的嵌套数是否已减为0,如果是则恢复中断(portCLEAR_INTERRUPT_MASK()),即将中断屏蔽系统寄存器的值恢复为`portUNMASK_VALUE`。
}
中断上下文的版本临界区退出流程:是任务上下文中的简化版本,调用了vPortClearInterruptMask(ulNewMaskValue)
,即如果ulNewMaskValue
为0,则恢复所有中断。
从实现可以看出,临界区是支持嵌套的,只有当嵌套深度为0时,才认为退出了临界区。需要注意的是,这个临界区深度是任务的上下文之一,也就是说,若发生了任务切换,临界区深度也会变化。
2.2. 挂起(锁定)调度器
临界区也可以通过挂起(或称锁定)调度器来实现。与基本临界区不同的是,它只能保护共享数据不会被多个任务并发访问,即中断依然可以打断任务执行而影响共享资源。
vTaskSuspendAll()
可挂起调度器,挂起后将不发生上下文切换。
xTaskResumeAll()
恢复调度器。具体的实现可参看挂起和恢复调度器。
3. 互斥锁和二值信号量
互斥锁(MUTual EXclution,缩写为MUTEX)是一种特殊的二值信号量,用于保护在多个任务之间共享的资源。
在FreeRTOS中,如果希望开启互斥锁模块,则需要设置configUSE_MUTEXES = 1
。
3.1. 优先级继承
在使用互斥锁进行共享资源保护时,通常会出现一种**优先级翻转(Priority Inversion)**的现象,如下图所示,即高优先级的任务等待低优先级任务所持有的互斥锁的时,被中间优先级的任务抢占的现象。因为总是存在低优先级的任务A先获取了互斥锁的可能性,此时,在高优先级的任务B申请该锁必然会进入Blocked状态,假设这时一个优先级介于任务A和任务B之间的任务C进入了Ready队列,那么任务C势必会导致任务A被抢占,间接地,原本应该属于任务B的时间片被任务C抢占了。从结果来看,一个高优先级的任务被低优先级的任务抢占了,这显然不是FreeRTOS的本意。
为了避免优先级翻转导致的问题,通常会使用优先级继承的方案。其基本思路是:当有高优先级的任务等待低优先级任务所持有的互斥锁时,低优先级的任务继承高优先级任务的优先级,防止被中间优先级的任务中断。引入了优先级继承后,示意图如下:
由此,便可以看出二值信号量与互斥锁的区别:互斥锁支持优先级继承机制,而二值信号量不支持。
同时,与二值信号量不同的是,互斥锁存在锁的拥有者的概念,所以必须在同一个任务中申请和释放。显然,这也是符合常理的。
3.2.死锁
死锁是使用互斥锁时非常常见的问题。通常是通过软件设计来避免死锁。FreeRTOS应对死锁的方法就是鸵鸟策略,即不做任何处理。
3.3. 递归互斥锁
对于普通的互斥锁,如果任务尝试连续两次申请同一个锁时(前一次申请并没有释放),则会导致死锁。为了避免这种问题,FreeRTOS提供了递归互斥锁机制。在使用递归互斥锁时,可以多次申请同一个锁,释放时,也需要释放对应的次数,即xSemaphoreTakeRecursive()
和xSemaphoreGiveRecursive()
应该成对出现。
3.4. 互斥锁与任务调度
当有高优先级的任务等待低优先级的任务所持有的互斥锁,一旦高优先级的任务获取到互斥锁,则会立即发生一次调度;
而如果持有锁的任务与等待该锁的任务拥有相同的优先级,当锁释放后,需要等待下一个Tick才会发生调度。
4. 互斥锁的使用
互斥锁是基于信号量实现的。因此,与信号量一样,这里也基于FreeRTOSv10的实现进行介绍。
4.1. 创建互斥锁
函数原型:需要设置configUSE_MUTEXES = 1
#define xSemaphoreCreateMutex() xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )
QueueHandle_t xQueueCreateMutex( const uint8_t ucQueueType );
该函数创建互斥锁,并返回互斥锁的句柄。
下面来看看其具体实现,活动图如下:
可以看到,互斥锁在创建的时候,已经入队了一个消息,所以任务可以正常地获取刚创建的互斥锁。
4.2. 获取和释放互斥锁
互斥锁与二值信号量不同之处在于,互斥锁需要实现任务优先级继承机制。此外,互斥锁还有持有者的概念。下面则主要介绍一下这些行为的实现。其中关于获取互斥锁时包括如下操作:
-
递增任务获取互斥锁的计数(void *pvTaskIncrementMutexHeldCount()
):将任务当前占用的互斥锁计数增1,并返回任务的TCB。
{
如果当前有正在运行的任务(`pxCurrentTCB != NULL`),增加该任务的持有锁计数(`pxCurrentTCB->uxMutexesHeld++`)。
返回当前任务的TCB(`pxCurrentTCB`)。
}
-
任务优先级继承(BaseType xTaskPriorityInherit( TaskHandle_t const pxMutexHolder )
):锁的所有者任务将继承所有申请该锁的任务中,最高优先级的任务的优先级。FreeRTOSv10后,为了实现超时后恢复原来任务的优先级,该函数还需要返回是否发生了优先级继承。。活动图如下所示:
-
因为任务等待超时而导致优先级继承的恢复(void vTaskPriorityDisinheritAfterTimeout( TaskHandle_t const pxMutexHolder, UBaseType_t uxHighestPriorityWaitingTask )
):更新锁拥有者任务当前继承的优先级。
-
获取因为某个等待信号量的任务超时后,目前所有等待该信号量的任务中最高的优先级(static UBaseType_t prvGetDisinheritPriorityAfterTimeout( const Queue_t * const pxQueue )
):
{
如果当前仍有其他任务在等待该信号量(`listCURRENT_LIST_LENGTH( &( pxQueue->xTasksWaitingToReceive ) ) > 0`),则获取目前最高的优先级(`uxHighestPriorityOfWaitingTasks = configMAX_PRIORITIES - listGET_ITEM_VALUE_OF_HEAD_ENTRY( &( pxQueue->xTasksWaitingToReceive ) )`)。
否则,设置优先级为最低(`uxHighestPriorityOfWaitingTasks = tskIDLE_PRIORITY`)。
返回`uxHighestPriorityOfWaitingTasks`。
}
在释放互斥锁时,包含如下操作:
-
优先级的恢复(BaseType_t xTaskPriorityDisinherit( TaskHandle_t const pxMutexHolder )
):结束优先级继承,并返回是否需要触发调度。
-
互斥锁的释放发生在static BaseType_t prvCopyDataToQueue( Queue_t * const pxQueue, const void *pvItemToQueue, const BaseType_t xPosition )
中。
获取和释放互斥锁的API与信号量一致,详情请参看二值信号量小节。
4.3. 获取互斥锁的所有者
函数原型:
#define xSemaphoreGetMutexHolder( xSemaphore ) xQeueueGetMutexHolder( ( xSemaphore ) )
#define xSemaphroeGetMutexHolderFromISR( xSemaphore ) xQueueGetMutexHolderFromISR( ( xSemaphore ) )
这两个函数都会返回互斥锁的拥有者任务。
4.4. 删除互斥锁
删除互斥锁的API与信号量的一致,详情请参考删除信号量小节。
5. 递归互斥锁的使用
5.1. 创建递归互斥锁
函数原型:需要设置configUSE_RECURSIZE_MUTEXES = 1
#define xSemaphoreCreateRecursiveMutex() xQueueCreateMutex( queuQUEUE_TYPE_RECURSIVE_MUTEX )
该函数创建递归互斥锁,并返回递归互斥锁的句柄。
具体实现可参看创建互斥锁小节。
5.2. 获取递归互斥锁
函数原型:需要设置configUSE_RECURSIZE_MUTEXES = 1
#define xSemaphoreTakeRecursive( xMutex, xBlockTime ) xQueueTakeMutexRecursive( ( xMutex ), ( xBlockTime ) )
BaseType_t xQueueTakeMutexRecursive( QueueHandle_t xMutex, TickType_t xTicksToWait );
具体的实现如下:
{
初始化`pxMutex = ( Queue_t * ) xMutex`。
参数检查:`pxMutex != NULL`。
如果该任务获取了该锁(`pxMutex->pxMutexHolder == ( void * ) xTaskGetCurrentTaskHandle()`):
{
将该锁的递归深度增一(`pxMutex->u.uxRecursiveCallCount++`)。
设置返回值`xReturn = pdPASS`。
}
否则,即任务第一次获取锁:
{
和普通互斥锁一样获取锁(`xReturn = xQueueSemaphoreTake( pxMutex, xTicksToWait )`)。
如果申请成功(`xReturn != pdFAIL`),则增加递归数(`pvMutex->u.uxRecursiveCallCount++`)。
}
返回`xReturn`。
}
从实现可以看出,递归互斥锁与正常互斥锁不同的是,当任务已经获取了该锁之后,它仅仅增加了递归计数,而不会使得任务进入Blocked状态。
5.3. 释放递归锁
函数原型:需要设置configUSE_RECURSIVE_MUTEXES = 1
#define xSemaphoreGiveRecursive( xMutex ) xQueueGiveMutexRecursive( ( xMutex ) )
BaseType_t xQueueGiveMutexRecursive( QueueHandle_t xMutex );
下面看看其具体的实现:
{
初始化`pxMutex = ( Queue_t * ) xMutex`。
参数检查:Assert `pxMutex != NULL`。
如果当前任务是递归锁的所有者(`pxMutex->pxMutexHolder == ( void * ) xTaskGetCurrentTaskHandle()`):
{
将锁的递归深度减一(`pxMutex->u.uxRecursiveCallCount--`)。
如果锁的递归深度为0(`pxMutex->u.uxRecursiveCallCount == 0`):
{
正常释放该锁(`xQueueGenericSend( pxMutex, NULL, queueMUTEX_GIVE_BLOCK_TIME, queueSEND_TO_BACK )`)。
}
设置返回值`xReturn = pdPASS`。
}
否则,即当前任务不是递归锁的所有者:
{
设置返回值`xReturn = pdFAIL`。
}
返回`xReturn`。
}
从实现可以看到,只有当递归锁的递归深度递减为0时,才会真正的释放锁。
6. Gatekeeper Task(管程)
Gatekeeper task:是一个拥有和管理共享资源的任务。只有它自身能直接访问这个共享资源。其他的任务则需要通过与该任务进行通信来间接访问共享资源。
这是一种设计理念,在FreeRTOS中并没有给出直接的支持,需要用户编写自定义的任务实现。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)