FreeRTOS内核详解----信号量

时间:2021-07-18 15:11:52

FreeRTOS内核详解—-信号量


信号量主要用来保护共享资源、任务之间或者任务与中断之间用来同步等。FreeRTOS有二值信号量、计数信号量、互斥锁和递归互斥锁。

  • 信号量实现总览
  • 二值信号量
  • 计数信号量
  • 互斥锁的创建
  • 递归互斥锁的创建
  • 通用发送信号量函数
  • 通用接收信号量函数
  • 递归信号量发送函数
  • 递归信号量接收函数

1.信号量实现总览

#define vSemaphoreCreateBinary( xSemaphore ) \
    {                                                                                                                                           \
        ( xSemaphore ) = xQueueGenericCreate( ( unsigned portBASE_TYPE ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE ); \
        if( ( xSemaphore ) != NULL )                                                                                                            \
        {                                                                                                                                       \
            xSemaphoreGive( ( xSemaphore ) );                                                                                                   \
        }                                                                                                                                       \
    }

#define xSemaphoreTake( xSemaphore, xBlockTime ) xQueueGenericReceive( ( xQueueHandle ) ( xSemaphore ), NULL, ( xBlockTime ), pdFALSE )
#define xSemaphoreGive( xSemaphore ) xQueueGenericSend( ( xQueueHandle ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )
#define xSemaphoreGiveRecursive( xMutex ) xQueueGiveMutexRecursive( ( xMutex ) )
#define xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken ) xQueueGenericSendFromISR( ( xQueueHandle ) ( xSemaphore ), NULL, ( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK )
#define xSemaphoreTakeRecursive( xMutex, xBlockTime ) xQueueTakeMutexRecursive( ( xMutex ), ( xBlockTime ) )
#define xSemaphoreTakeFromISR( xSemaphore, pxHigherPriorityTaskWoken ) xQueueReceiveFromISR( ( xQueueHandle ) ( xSemaphore ), NULL, ( pxHigherPriorityTaskWoken ) )
#define xSemaphoreCreateMutex() xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )
#define xSemaphoreCreateRecursiveMutex() xQueueCreateMutex( queueQUEUE_TYPE_RECURSIVE_MUTEX )
#define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ) xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) )
#define vSemaphoreDelete( xSemaphore ) vQueueDelete( ( xQueueHandle ) ( xSemaphore ) )
#define xSemaphoreGetMutexHolder( xSemaphore ) xQueueGetMutexHolder( ( xSemaphore ) )

我们可以看到信号量都是通过对已有几个特定函数通过宏包装实现的,下面我们分别讲述一下。

2.二值信号量

二值信号量是一个队列项个数为1的队列,使用的创建函数队列的通用方法进行创建、发送和获取数据的,创建代码如下:

#define vSemaphoreCreateBinary( xSemaphore ) \
    {                                                                                                                                            \
        ( xSemaphore ) = xQueueGenericCreate( ( unsigned portBASE_TYPE ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE );    \
        if( ( xSemaphore ) != NULL )                                                                                                            \
        {                                                                                                                                        \
            xSemaphoreGive( ( xSemaphore ) );                                                                                                    \
        }                                                                                                                                        \
    }

我们可以看到队列长度为0,也就是二值信号量要不是满列,要不就是空列,而且不会存储数据。这也是二值信号量名字来源。同时我们可以看到二值信号量创建成功之后,会立即进行一次GIVE操作,也就是二值信号量创建完成之后,就有数据,就是一个满列,不明白这样做的意义是什么。

3.计数信号量

#define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ) xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) )

计数信号量创建由定义在queue中的函数 xQueueCreateCountingSemaphore ,函数如下:


    xQueueHandle xQueueCreateCountingSemaphore( unsigned portBASE_TYPE uxCountValue, unsigned portBASE_TYPE uxInitialCount )
    {
    xQueueHandle xHandle;

        // 队列长度等于计数值的最大值 队列项大小为0
        xHandle = xQueueGenericCreate( ( unsigned portBASE_TYPE ) uxCountValue, queueSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_COUNTING_SEMAPHORE );

        if( xHandle != NULL )
        {
            ( ( xQUEUE * ) xHandle )->uxMessagesWaiting = uxInitialCount;

            traceCREATE_COUNTING_SEMAPHORE();
        }
        else
        {
            traceCREATE_COUNTING_SEMAPHORE_FAILED();
        }

        configASSERT( xHandle );
        return xHandle;
    }

计数信号量是一个只存储当前有多少个数据不存储当前数据具体值的队列。我们可以看到计数信号量与二值信号量的区别就是,二值信号量只有两种状态 。而计数信号量也有两种状态 有数据 (包括满) 。这有什么作用呢?让我们看一个例子,假如有一个函数需要记录外部中断的个数。实现方法是在中断使用信号量发送函数,每进一次中断,发送一次;创建一个任务,每次收到信号量,就将计数值加1。这时候就可以使用计数信号量,因为假如外部中断发生频率非常高的时候,如果使用二值信号量,会出现任务还没有来的及更新计数值,又来了中断向信号量发送数据,导致统计最终不准确。

4.互斥锁的创建

#define xSemaphoreCreateMutex() xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )

计数信号量创建由定义在queue中的函数 xQueueCreateMutex ,函数如下:


xQueueHandle xQueueCreateMutex( unsigned char ucQueueType )
    {
    xQUEUE *pxNewQueue;

        ( void ) ucQueueType;

        /* Allocate the new queue structure. */
        pxNewQueue = ( xQUEUE * ) pvPortMalloc( sizeof( xQUEUE ) );
        if( pxNewQueue != NULL )
        {
            pxNewQueue->pxMutexHolder = NULL;
            pxNewQueue->uxQueueType = queueQUEUE_IS_MUTEX;

            // 不需要真的存储数据
            pxNewQueue->pcWriteTo = NULL;
            pxNewQueue->pcReadFrom = NULL;

            pxNewQueue->uxMessagesWaiting = ( unsigned portBASE_TYPE ) 0U;
            pxNewQueue->uxLength = ( unsigned portBASE_TYPE ) 1U;   //队列项个数为1
            pxNewQueue->uxItemSize = ( unsigned portBASE_TYPE ) 0U; //队列项长度为0
            pxNewQueue->xRxLock = queueUNLOCKED;
            pxNewQueue->xTxLock = queueUNLOCKED;

            #if ( configUSE_TRACE_FACILITY == 1 )
            {
                pxNewQueue->ucQueueType = ucQueueType;
            }
            #endif

            #if ( configUSE_QUEUE_SETS == 1 )
            {
                pxNewQueue->pxQueueSetContainer = NULL;
            }
            #endif

            /* Ensure the event queues start with the correct state. */
            vListInitialise( &( pxNewQueue->xTasksWaitingToSend ) );
            vListInitialise( &( pxNewQueue->xTasksWaitingToReceive ) );

            traceCREATE_MUTEX( pxNewQueue );

            /* Start with the semaphore in the expected state. */
            xQueueGenericSend( pxNewQueue, NULL, ( portTickType ) 0U, queueSEND_TO_BACK );
        }
        else
        {
            traceCREATE_MUTEX_FAILED();
        }

        configASSERT( pxNewQueue );
        return pxNewQueue;
    }

互斥锁的创建,没有使用通用的队列创建函数,我们可以看到与普通队列创建,其中一个区别就是

pxNewQueue->pxMutexHolder = NULL;
pxNewQueue->uxQueueType = queueQUEUE_IS_MUTEX; 

前一个是将当前信号量的持有者设置为NULL,也就是没有被任何任务持有。后一个是将队列类型设置为queueQUEUE_IS_MUTEX 。同时互斥量创建时也会进行一次GIVE操作,这样可以确保互斥信号量一创建好就可以,就可以被任务进行TAKE操作,也就是可以被任务获得。互斥信号量牵扯到一个优先级反转的问题的问题,优先级反转与FreeRTOS的解决方法,** FREERTOS 实时内核
实用指南 ** 这本书中讲述的很清楚,现抄录如下:

优先级反转

FreeRTOS内核详解----信号量

图 38 也展现出了采用互斥量提供互斥功能的潜在缺陷之一。在这种可能的执行流程描述中,高优先级的任务 2 竟然必须等待低优先级的任务 1 放弃对互斥量的持有权。高优先级任务被低优先级任务阻塞推迟的行为被称为”优先级反转”。这是一种不合理的行为方式,如果把这种行为再进一步放大,当高优先级任务正等待信号量的时候,一个介于两个任务优先之间的中等优先级任务开始执行——这就会导致一个高优先级任务在等待一个低优先级任务,而低优先级任务却无法执行!这种最坏的情形在图 39 中进行展示。

FreeRTOS内核详解----信号量

优先级反转可能会产生重大问题。但是在一个小型的嵌入式系统中,通常可以在设计阶段就通过规划好资源的访问方式避免出现这个问题。

优先级继承
FreeRTOS 中互斥量与二值信号量十分相似——唯一的区别就是互斥量自动提供了一个基本的”优先级继承”机制。优先级继承是最小化优先级反转负面影响的一种方案——其并不能修正优先级反转带来的问题,仅仅是减小优先级反转的影响。优先级继承使得系统行为的数学分析更为复杂,所以如果可以避免的话,并不建议系统实现对优先级继承有所依赖。优先级继承暂时地将互斥量持有者的优先级提升至所有等待此互斥量的任务所具有的最高优先级。持有互斥量的低优先级务”继承”了等待互斥量的任务的优先级。这种机制在图 40 中进行展示。互斥量持有者在归还互斥量时,优先级会自动设置为其原来的优先级。

FreeRTOS内核详解----信号量

由于最好是优先考虑避免优先级反转,并且因为 FreeRTOS 本身是面向内存有限的微控制器,所以只实现了最基本的互斥量的优先级继承机制,这种实现假定一个任务在任意时刻只会持有一个互斥量。

5.递归互斥锁的创建


xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken )          xQueueGenericSendFromISR( ( xQueueHandle ) ( xSemaphore ), NULL, ( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK )
#define xSemaphoreTakeFromISR

递归互斥信号量的创建方法与普通互斥信号量的创建方法相同,

6.通用发送信号量函数

#define xSemaphoreGive( xSemaphore ) xQueueGenericSend( ( xQueueHandle ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )

通用发送使用一个不带阻塞时间的队列发送函数,调用这个函数与普通队列的不同就是在调用 prvCopyDataToQueue 这个函数时,此函数处理不同,这个函数之前在将队列时讲述过,函数代码如下:

static void prvCopyDataToQueue( xQUEUE *pxQueue, const void *pvItemToQueue, portBASE_TYPE xPosition )
{
    if( pxQueue->uxItemSize == ( unsigned portBASE_TYPE ) 0 )
    {   //二值信号量 计数信号量等uxItemSize值为0 不进行数据的拷贝,这个是与普通队列的区别1
        #if ( configUSE_MUTEXES == 1 )
        {
            if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
            {
                vTaskPriorityDisinherit( ( void * ) pxQueue->pxMutexHolder );//将优先级降到设置值
                pxQueue->pxMutexHolder = NULL;
            }
        }
        #endif
    }
    else if( xPosition == queueSEND_TO_BACK )
    {   //插入队列的结尾
        memcpy( ( void * ) pxQueue->pcWriteTo, pvItemToQueue, ( unsigned ) pxQueue->uxItemSize );
        pxQueue->pcWriteTo += pxQueue->uxItemSize;  //更新下次写数据的位置指针
        if( pxQueue->pcWriteTo >= pxQueue->pcTail )
        {
            pxQueue->pcWriteTo = pxQueue->pcHead;   //如果写到了队列结尾则从头开始写
        }
    }
    else
    {   //插入到队列开头
        memcpy( ( void * ) pxQueue->pcReadFrom, pvItemToQueue, ( unsigned ) pxQueue->uxItemSize );
        pxQueue->pcReadFrom -= pxQueue->uxItemSize; //更新上次读数据位置
        if( pxQueue->pcReadFrom < pxQueue->pcHead )     
        {
            pxQueue->pcReadFrom = ( pxQueue->pcTail - pxQueue->uxItemSize );//如果读到了队列结尾则从头开始读
        }
    }

    ++( pxQueue->uxMessagesWaiting );   //更新当前队列数据个数 信号量只更新数据项个数
}

信号量的发送数据函数,并不会真的把数据存储到队列数据存储区中去,而且也没有数据存储区,它仅仅记录数据项的个数,而且这个发送函数没有阻塞时间,也就是一旦满了,任务就不能再向队列尝试添加数据,如果有任务尝试添加,会直接返回一个满列错误,而不会进入阻塞以便等到队列有空间时再将数据添加到队列中区。此函数不可用于递归互斥锁的操作。

上面有一个是互斥锁需要单独处理的,就是当任务不再拥有互斥锁时,将优先级的还原,就是如果发生了优先级继承,则要任务优先级还原成设定值,通过调用 vTaskPriorityDisinherit 函数实现,源码如下:


void vTaskPriorityDisinherit( xTaskHandle * const pxMutexHolder )
    {
    tskTCB * const pxTCB = ( tskTCB * ) pxMutexHolder;

        if( pxMutexHolder != NULL )
        {
            if( pxTCB->uxPriority != pxTCB->uxBasePriority )
            {
                // 将当前任务从当前优先级的任务就绪链表中移除
                if( uxListRemove( ( xListItem * ) &( pxTCB->xGenericListItem ) ) == 0 )
                {
                    taskRESET_READY_PRIORITY( pxTCB->uxPriority );
                }

                // 还原优先级 并且将任务加入到新优先级的就绪链表中
                traceTASK_PRIORITY_DISINHERIT( pxTCB, pxTCB->uxBasePriority );
                pxTCB->uxPriority = pxTCB->uxBasePriority;
                listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ), configMAX_PRIORITIES - ( portTickType ) pxTCB->uxPriority );
                prvAddTaskToReadyQueue( pxTCB );
            }
        }
    }

7.通用接收信号量函数

#define xSemaphoreTake( xSemaphore, xBlockTime ) xQueueGenericReceive( ( xQueueHandle ) ( xSemaphore ), NULL, ( xBlockTime ), pdFALSE )

通用接收信号量函数并不会尝试从队列中真的获取数据,而仅仅是产看队列是否为空,如果不为空,返回一个pdTRUE ,表明有信号量, 如果没有信号量,且设置了阻塞时间,则会将当前任务添加到等待接收链表中。此函数不可用于递归互斥量的操作。通用接收信号量函数牵涉到我们前面讲述的优先级反转的具体实现,通过函数vTaskPriorityInherit 源代码如下:

void vTaskPriorityInherit( xTaskHandle * const pxMutexHolder )
    {
    tskTCB * const pxTCB = ( tskTCB * ) pxMutexHolder;

        if( pxMutexHolder != NULL )
        {
            if( pxTCB->uxPriority < pxCurrentTCB->uxPriority )
            {   // 拥有此信号量的任务优先级比当前任务优先级低
                // 更新链表项的值 存储新的优先级值
                listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ), configMAX_PRIORITIES - ( portTickType ) pxCurrentTCB->uxPriority );


                if( listIS_CONTAINED_WITHIN( &( pxReadyTasksLists[ pxTCB->uxPriority ] ), &( pxTCB->xGenericListItem ) ) != pdFALSE )
                {   //将任务从当前优先级链表中移除
                    if( uxListRemove( ( xListItem * ) &( pxTCB->xGenericListItem ) ) == 0 )
                    {
                        taskRESET_READY_PRIORITY( pxTCB->uxPriority );
                    }

                    // 将拥有此信号量的任务优先级设置为与当前任务优先级一致
                    pxTCB->uxPriority = pxCurrentTCB->uxPriority;   
                    prvAddTaskToReadyQueue( pxTCB );        //加入就绪链表
                }
                else
                {
                    pxTCB->uxPriority = pxCurrentTCB->uxPriority;   // 将拥有此信号量的任务优先级设置为与当前任务优先级一致
                }

                traceTASK_PRIORITY_INHERIT( pxTCB, pxCurrentTCB->uxPriority );
            }
        }
    }

这个函数的作用就是假如拥有信号量的任务优先级比尝试TAKE信号量的任务优先级任务优先级低,则将其设置为等待TAKE信号量的任务相同。其他的都是更新与优先级相关的参数。

8.递归信号量发送函数


portBASE_TYPE xQueueGiveMutexRecursive( xQueueHandle xMutex )
    {
    portBASE_TYPE xReturn;
    xQUEUE *pxMutex;

        pxMutex = ( xQUEUE * ) xMutex;
        configASSERT( pxMutex );

        if( pxMutex->pxMutexHolder == xTaskGetCurrentTaskHandle() )
        {   //只有递归互斥量的拥有者才能进行GIVE操作
            traceGIVE_MUTEX_RECURSIVE( pxMutex );

            ( pxMutex->uxRecursiveCallCount )--;        //更新递归调用计数值

            /* Have we unwound the call count? */
            if( pxMutex->uxRecursiveCallCount == 0 )
            {
                // 当递归调用计数值为0 表明当前任务已经不再拥有此互斥量,进行一次SEND操作
                xQueueGenericSend( pxMutex, NULL, queueMUTEX_GIVE_BLOCK_TIME, queueSEND_TO_BACK );
            }

            xReturn = pdPASS;
        }
        else
        {
            //只有递归互斥量的拥有者才能进GIVE操作,不是返回pdFAIL
            xReturn = pdFAIL;

            traceGIVE_MUTEX_RECURSIVE_FAILED( pxMutex );
        }

        return xReturn;
    }

通过上述函数,我们可以看到,递归互斥量可以被多个任务TAKE,但是只能只能被同一个任务进行多次TAKE。当 uxRecursiveCallCount = 0 时,则当前任务不再拥有此信号量。注意递归互斥量只能使用此函数GIVE。

9.递归信号量接收函数

portBASE_TYPE xQueueTakeMutexRecursive( xQueueHandle xMutex, portTickType xBlockTime )
    {
    portBASE_TYPE xReturn;
    xQUEUE *pxMutex;

        pxMutex = ( xQUEUE * ) xMutex;
        configASSERT( pxMutex );

        traceTAKE_MUTEX_RECURSIVE( pxMutex );

        if( pxMutex->pxMutexHolder == xTaskGetCurrentTaskHandle() )
        {
            ( pxMutex->uxRecursiveCallCount )++;    //更新递归调用计数值
            xReturn = pdPASS;
        }
        else
        {
            xReturn = xQueueGenericReceive( pxMutex, NULL, xBlockTime, pdFALSE );
            //之所以没有检测当前pxMutexHolder是否为空就进行上述操作,因为pxMutex是一个队列项目个数为1的队列,如果被其他任务拥有,则返回pdFAIL
            if( xReturn == pdPASS )
            {
                ( pxMutex->uxRecursiveCallCount )++;
            }
            else
            {
                traceTAKE_MUTEX_RECURSIVE_FAILED( pxMutex );
            }
        }

        return xReturn;
    }

递归互斥信号量的获取函数,只能被同一个任务TAKE且可以多次TAKE。注意递归互斥量只能使用此函数GIVE。