FREERTOS信号量详解

信号量是操作系统中重要的一部分,信号量一般用来进行资源管理和任务同步,资源管理其实就是用变量来标记现有资源的数量,任务同步其实就是用标志位来控制任务的先后执行顺序,这些概念在操作系统中以及裸机开发中都有所涉及。

FreeRTOS中信号量又分为二值信号量、计数型信号量、互斥信号量和递归互斥信号量。不同的信号量其应用场景不同,但有些应用场景是可以互换着使用的。

信号量简介

信号量常常用于控制对共享资源的访问和任务同步。举一个很常见的例子,某个停车场有 100 个停车位,这 100 个停车位大家都可以用,对于大家来说这 100 个停车位就是共享资源。

假设现在这个停车场正常运行,你要把车停到这个这个停车场肯定要先看一下现在停了多少车了?还有没有停车位?当前停车数量就是一个信号量,具体的停车数量就是这个信号量值,当这个值到 100 的时候说明停车场满了。停车场满的时你可以等一会看看有没有其他的车开出停车场,当有车开出停车场的时候停车数量就会减一,也就是说信号量减一,此时你就可以把车停进去了,你把车停进去以后停车数量就会加一,也就是信号量加一。这就是一个典型的使用信号量进行共享资源管理的案例,在这个案例中使用的就是计数型信号量。再看另外一个案例:使用公共电话,我们知道一次只能一个人使用电话,这个时候公共电话就只可能有两个状态:使用或未使用,如果用电话的这两个状态作为信号量的话,那么这个就是二值信号量。信号量用于控制共享资源访问的场景相当于一个上锁机制,代码只有获得了这个锁的钥匙 才能够执行。

上面我们讲了信号量在共享资源访问中的使用,信号量的另一个重要的应用场合就是任务 同步,用于任务与任务或中断与任务之间的同步。在执行中断服务函数的时候可以通过向任务发送信号量来通知任务它所期待的事件发生了,当退出中断服务函数以后在任务调度器的调度下同步的任务就会执行。在编写中断服务函数的时候我们都知道一定要快进快出,中断服务函数里面不能放太多的代码,否则的话会影响的中断的实时性。裸机编写中断服务函数的时候一般都只是在中断服务函数中打个标记,然后在其他的地方根据标记来做具体的处理过程。在使用 RTOS 系统的时候我们就可以借助信号量完成此功能,当中断发生的时候就释放信号量,中断服务函数不做具体的处理。具体的处理过程做成一个任务,这个任务会获取信号量,如果获取到信号量就说明中断发生了,那么就开始完成相应的处理,这样做的好处就是中断执行时间非常短。这个例子就是中断与任务之间使用信号量来完成同步,当然了,任务与任务之间也可以使用信号量来完成同步。

FreeRTOS 中还有一些其他特殊类型的信号量,比如互斥信号量和递归互斥信号量,这些具体遇到的时候再讲解。有关信号量的知识在 FreeRTOS 的官网上都有详细的讲解,包括二值信号量、计数型信号量、互斥信号量和递归互斥信号量,我们下面要讲解的这些涉及到理论性的知识都是翻译自 FreeRTOS 官方资料,感兴趣的可以去官网看原版的英文资料。

其用法就是取代裸机中的全局变量,使得可以被操作系统进行统一管理。

要注意体会队列和信号量的不同之处,队列像是全局变量中的数据传递使用,属于数据类型;而信号量更像是全局变量中的标志位使用,起到控制和设置作用。

二值信号量

二值信号量通常用于互斥访问或同步,二值信号量和互斥信号量非常类似,但是还是有一些细微的差别,互斥信号量拥有优先级继承机制,二值信号量没有优先级继承。因此二值信号量更适合用于同步(任务与任务或任务与中断的同步),而互斥信号量适合用于简单的互斥访问,有关互斥信号量的内容后面会专门讲解,本节只讲解二值信号量在同步中的应用。

和队列一样,信号量 API 函数允许设置一个阻塞时间,阻塞时间是当任务获取信号量的时候由于信号量无效从而导致任务进入阻塞态的最大时钟节拍数。如果多个任务同时阻塞在同一个信号量上的话那么优先级最高的哪个任务优先获得信号量,这样当信号量有效的时候高优先级的任务就会解除阻塞状态。

二值信号量其实就是一个只有一个队列项的队列,这个特殊的队列要么是满的,要么是空的,这不正好就是二值的吗? 任务和中断使用这个特殊队列不用在乎队列中存的是什么消息,只需要知道这个队列是满的还是空的。可以利用这个机制来完成任务与中断之间的同步。

在实际应用中通常会使用一个任务来处理 MCU 的某个外设,比如网络应用中,一般最简单的方法就是使用一个任务去轮询的查询 MCU ETH(网络相关外设,如 STM32 的以太网MAC)外设是否有数据,当有数据的时候就处理这个网络数据。这样使用轮询的方式是很浪费CPU 资源的,而且也阻止了其他任务的运行。最理想的方法就是当没有网络数据的时候网络任务就进入阻塞态,把CPU 让给其他的任务,当有数据的时候网络任务才去执行。现在使用二值信号量就可以实现这样的功能,任务通过获取信号量来判断是否有网络数据,没有的话就进入阻塞态,而网络中断服务函数(大多数的网络外设都有中断功能,比如STM32 MAC 专用 DMA 中断,通过中断可以判断是否接收到数据)通过释放信号量来通知任务以太网外设接收到了网络数据,网络任务可以去提取处理了。网络任务只是在一直的获取二值信号量,它不会释放信号量,而中断服务函数是一直在释放信号量,它不会获取信号量。

在中断服务函数中发送信号量可以使用函数 xSemaphoreGiveFromISR(),也可以使用任务通知功能来替代二值信号量,而且使用任务通知的话速度更快,代码量更少,有关任务通知的内容后面会有专门的章节介绍。

使用二值信号量来完成中断与任务同步的这个机制中,任务优先级确保了外设能够得到及时的处理,这样做相当于推迟了中断处理过程。也可以使用队列来替代二值信号量,在外设事件的中断服务函数中获取相关数据,并将相关的数据通过队列发送给任务。如果队列无效的话任务就进入阻塞态,直至队列中有数据,任务接收到数据以后就开始相关的处理过程。下面几个步骤演示了二值信号量的工作过程。

由于任务函数一般都是一个大循环,所以在任务做完相关的处理以后就会再次调用函数 xSemaphoreTake()获取信号量。在执行完第三步以后二值信号量就已经变为无效的了,所以任务将再次进入阻塞态,和第一步一样,直至中断再次发生并且调用函数 xSemaphoreGiveFromISR()释放信号量。

这一过程和裸机中标志位的使用是一样的思路,只不过操作系统对其进行了管理。

二值信号量相对队列来说,不需要传递数据,只是使用了队列的“标志位”作用:有数据还是没数据的标志。

计数信号量相对好理解,主要是用来进行资源管理的,有资源的时候才会执行,没资源的时候就会被阻塞。

二值信号量和互斥信号容易搞混,互斥信号量也属于二值信号量,或者说,二值信号量可以分为普通的二值信号量和互斥信号量。普通的二值信号量,主要是用来进行任务同步的,互斥信号量主要是用来互斥访问的。任务同步是指两个任务之间按照一定的顺序去执行,互斥访问指的是多个任务都想访问同一段程序代码,但是如果已经有一个任务已经在执行了,其他任务就得阻塞等待,无法同时访问,这就是互斥访问。要注意和临界保护的区别,临界保护指的是确保某段程序能够完整运行。

与互斥锁不同,二进制信号量可用于中断服务程序,互斥锁不可以。

二进制信号量和互斥锁非常相似,但 仍有一些细微差异: 互斥锁具有优先级继承机制, 但二进制信号量没有。 因此,二进制信号量是 实现同步的更好选择(任务之间或任务与中断之间), 而互斥锁是实现简单互斥的更好选择。

二值信号量并不需要在得到后立即释放, 因此,任务同步可以通过一个任务/中断持续释放信号量而另外一个持续获得信号量来实现。 

创建二值信号量

同队列一样,要想使用二值信号量就必须先创建二值信号量,二值信号量创建函数如下表所示:

此函数是 vSemaphoreCreateBinary()的新版本,新版本的 FreeRTOS 中统一用此函数来创建二值信号量。使用此函数创建二值信号量的话信号量所需要的 RAM 是由 FreeRTOS 的内存管理部分来动态分配的。此函数创建好的二值信号量默认是空的,也就是说刚创建好的二值信号量使用函数 xSemaphoreTake()是获取不到的,此函数也是个宏,具体创建过程是由函数xQueueGenericCreate()来完成的,由此也可知,二值信号量确实是一个队列。

函数原型如下:

SemaphoreHandle_t xSemaphoreCreateBinary( void )

返回值: 

NULL: 二值信号量创建失败。

其他值: 创建成功的二值信号量的句柄。

二值信号量创建过程分析

看一下新版本的二值信号量创建函数 xSemaphoreCreateBinary(),函数代码如下:

可以看出新版本的二值信号量创建函数也是使用函数 xQueueGenericCreate()来创建一个类型为 queueQUEUE_TYPE_BINARY_SEMAPHORE、长度为 1、队列项长度为 0 的队列。这一步和老版本的二值信号量创建函数一样,唯一不同的就是新版本的函数在成功创建二值信号量以后不会立即调用函数 xSemaphoreGive()释放二值信号量。也就是说新版函数创建的二值信号量默认是无效的,而老版本是有效的。

大家注意看,创建的队列是个没有存储区的队列,前面说了使用队列是否为空来表示二值信号量,而队列是否为空可以通过队列结构体的成员变量 uxMessagesWaiting 来判断。

注意,创建二值信号量时不需要传入任务参数。

另外,信号量使用时需要导入#include "semphr. h"头文件。

释放信号量

释放信号量的函数有两个,如下表所示:

同队列一样,释放信号量也分为任务级和中断级。还有!不管是二值信号量、计数型信号量还是互斥信号量,它们都使用上表中的函数释放信号量,递归互斥信号量有专用的释放函数。

注意:释放信号量是说给一个信号量,不是说把信号量搞没。这名字容易让人产生误解。

函数 xSemaphoreGive()

此函数用于释放二值信号量、计数型信号量或互斥信号量,此函数是一个宏,真正释放信号量的过程是由函数 xQueueGenericSend()来完成的,函数原型如下:

BaseType_t xSemaphoreGive( xSemaphore )

参数:

xSemaphore:要释放的信号量句柄。

返回值: 

pdPASS: 释放信号量成功。

errQUEUE_FULL: 释放信号量失败。

我们再来看一下函数 xSemaphoreGive()的具体内容,此函数在文件 semphr.h 中有如下定义:

可以看出任务级释放信号量就是向队列发送消息的过程,只是这里并没有发送具体的消息,阻塞时间为0(semGIVE_BLOCK_TIME 0),入队方式采用的后向入队。具体入队过程之前已经做了详细的讲解,入队的时候队列结构体成员变量 uxMessagesWaiting 会加一,对于二值信号量通过判断 uxMessagesWaiting 就可以知道信号量是否有效了,当uxMessagesWaiting 1 的话说明二值信号量有效,为 0 就无效。如果队列满的话就返回错误值 errQUEUE_FULL,提示队列满,入队失败。

函数xSemaphoreGiveFromISR()

此函数用于在中断中释放信号量,此函数只能用来释放二值信号量和计数型信号量,绝对不能用来在中断服务函数中释放互斥信号量!此函数是一个宏,真正执行的是函数 xQueueGiveFromISR(),

在中断中释放信号量真正使用的是函数 xQueueGiveFromISR(),此函数和中断级通用入队函数 xQueueGenericSendFromISR()极其类似!只是针对信号量做了微小的改动。函数xSemaphoreGiveFromISR()不能用于在中断中释放互斥信号量,因为互斥信号量涉及到优先级继承的问题,而中断不属于任务,没法处理中断优先级继承。

获取信号量

获取信号量也有两个函数,如下表所示:

此函数用于获取二值信号量、计数型信号量或互斥信号量,此函数是一个宏,真正获取信号量的过程是由函数 xQueueGenericReceive ()来完成的,函数原型如下:

BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xBlockTime)

参数:

xSemaphore:要获取的信号量句柄。

xBlockTime: 阻塞时间。 

再来看一下函数 xSemaphoreTake ()的具体内容,此函数在文件 semphr.h 中有如下定义:

获取信号量的过程其实就是读取队列的过程,只是这里并不是为了读取队列中的消息。之前讲解函数 xQueueGenericReceive()的时候说过如果队列为空并且阻塞时间为 0 的话就立即返回errQUEUE_EMPTY,表示队列满。如果队列为空并且阻塞时间不为 0 的话就将任务添加到延时列表中。如果队列不为空的话就从队列中读取数据(获取信号量不执行这一步),数据读取完成以后还需要将队列结构体成员变量 uxMessagesWaiting 减一,然后解除某些因为入队而阻塞的任务,最后返回 pdPASS 表示出对成功。

互斥信号量涉及到优先级继承,处理方式不同,后面讲解互斥信号量的时候在详细的讲解。

函数 xSemaphoreTakeFromISR ()

此函数用于在中断服务函数中获取信号量,此函数用于获取二值信号量和计数型信号量,绝对不能使用此函数来获取互斥信号量!此函数是一个宏,真正执行的是函数xQueueReceiveFromISR ()

在中断中获取信号量真正使用的是函数 xQueueReceiveFromISR (),这个函数就是中断级出队函数!当队列不为空的时候就拷贝队列中的数据(用于信号量的时候不需要这一步),然后将队列结构体中的成员变量 uxMessagesWaiting 减一,如果有任务因为入队而阻塞的话就解除阻塞态,当解除阻塞的任务拥有更高优先级的话就将参数pxHigherPriorityTaskWoken 设置为pdTRUE,最后返回 pdPASS 表示出队成功。如果队列为空的话就直接返回 pdFAIL 表示出队失败!这个函数还是很简单的。

二值信号量的使命就是同步,完成任务与任务或中断与任务之间的同步。大多数情况下都是中断与任务之间的同步。类似于裸机中的标志位。

使用示例

二值信号量在释放和获取的过程中,都不需要处理数据,因为只是利用了内部实现的标志位功能,而不需要传递业务数据。注意和队列相区分。 

按键中断里释放二值信号量
SemaphoreHandle_t  s_KeyIsPushHandler;   		//按键是否按下的二值信号量
//按键相关数据初始化
void KeyDataInit(void)
{
    s_KeyIsPushHandler = xSemaphoreCreateBinary();
}
//按键被按下的响应处理
void EXTI0_IRQHandler(void)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    
    if(EXTI_GetITStatus(EXTI_Line0) != RESET)//判断中断是否发生
    { 
        if(s_KeyIsPushHandler != NULL)
        {
            xSemaphoreGiveFromISR(s_KeyIsPushHandler, &xHigherPriorityTaskWoken);
        }

        /* Now the buffer is empty we can switch context if necessary. */
        if( xHigherPriorityTaskWoken )
        {
            /* Actual macro used here is port specific. */
            portYIELD_FROM_ISR (xHigherPriorityTaskWoken);
        }
        
        EXTI_ClearITPendingBit(EXTI_Line0); //清除 LINE 上的中断标志位
    }
}

任务中获取该二值信号量
extern SemaphoreHandle_t  s_KeyIsPushHandler;
//打印任务1
void PrintTask1(void *p)
{
    BaseType_t err = 0;
    
    while(1)
    {
        err = xSemaphoreTake(s_KeyIsPushHandler, portMAX_DELAY);
        taskENTER_CRITICAL();           //进入临界区
        vTaskSuspendAll();
        if(err == pdTRUE)//获取二值信号量成功
        {
            printf("get semphr successfully!");
        }
//         xTaskResumeAll();
//        taskEXIT_CRITICAL();            //退出临界区
    }
}

二值信号量的释放和获取通常是成对出现的,释放二值信号量时相当于置标志位,获取二值信号量时相当于判断标志位并执行程序后再清除标志位。

计数型信号量

有些资料中也将计数型信号量叫做数值信号量,二值信号量相当于长度为 1 的队列,那么计数型信号量就是长度大于 1 的队列,也是通过队列结构体成员uxMessagesWaiting来实现的。同二值信号量一样,用户不需要关心队列中存储了什么数据,只需要关心队列结构体成员uxMessagesWaiting当前数值为多少即可。

计数型信号量通常用于如下两个场合:

1、事件计数

在这个场合中,每次事件发生的时候就在事件处理函数中释放信号量(增加信号量的计数 值),其他任务会获取信号量(信号量计数值减一,信号量值就是队列结构体成员变量 uxMessagesWaiting)来处理事件。在这种场合中创建的计数型信号量初始计数值为 0

2、资源管理

在这个场合中,信号量值代表当前资源的可用数量,比如停车场当前剩余的停车位数量。 一个任务要想获得资源的使用权,首先必须获取信号量,信号量获取成功以后信号量值就会减

一。当信号量值为 0 的时候说明没有资源了。当一个任务使用完资源以后一定要释放信号量,释放信号量以后信号量值会加一。在这个场合中创建的计数型信号量初始值应该是资源的数量,比如停车场一共有 100 个停车位,那么创建信号量的时候信号量值就应该初始化为 100

创建计数型信号量 

FreeRTOS 提供了两个计数型信号量创建函数,如下表所示:

函数 xSemaphoreCreateCounting()

此函数用于创建一个计数型信号量,所需要的内存通过动态内存管理方法分配。此函数本质是一个宏,真正完成信号量创建的是函数 xQueueCreateCountingSemaphore(),此函数原型如下:

SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount )

参数:

uxMaxCount: 计数信号量最大计数值,当信号量值等于此值的时候释放信号量就会失败。

uxInitialCount: 计数信号量初始值。

返回值:

NULL: 计数型信号量创建失败。

其他值: 计数型信号量创建成功,返回计数型信号量句柄。

计数型信号量创建过程分析

这里只分析动态创建计数型信号量函数 xSemaphoreCreateCounting(),此函数是个宏,定义

如下:

可以看出,真正干事的是函数 xQueueCreateCountingSemaphore()(并没有直接调用函数xQueueGenericCreate()),此函数在文件 queue.c 中,有如下定义:

(1)、计数型信号量也是在队列的基础上实现的,所以需要调用函数 xQueueGenericCreate()

创建一个队列,队列长度为uxMaxCount , 队列项长度为 queueSEMAPHORE_QUEUE_ITEM_LENGTH( 此宏为0) , 队 列 的 类 型 为

queueQUEUE_TYPE_COUNTING_SEMAPHORE,表示是个计数型信号量。

(2)、队列结构体的成员变量 uxMessagesWaiting 用于计数型信号量的计数,根据计数型信 号量的初始值来设置 uxMessagesWaiting

释放和获取计数信号量 

计数型信号量的释放和获取与二值信号量相同。

这里面有个问题,获取计数信号量的函数和获取二值信号量的函数是一样的,那么,也没法获取里面具体的计数值是多少呀?

怎么解决这个问题?

获取信号量当前计数值大小的函数

UBaseType_t uxSemaphoreGetCount( SemaphoreHandle_t xSemaphore );

我们注意计数型信号量的两种使用场景,用作计数时,初始值为0,用作资源管理时,初始值为资源的数量n,另外,参数里还有个最大值,当用作计数时,表示能计数到的最大值,当用作资源管理时,表示资源的最大数量。

比如,用作计数时:

我们设置最大计数到50,然后初始值为0

中断里释放计数信号量,值+1

这里注意,计数信号量释放一次加1,获取一次减1,成对出现的话,计数值就不变了。

当计数值为0的时候,就无法获取,也就是获取时会阻塞。

看下以下程序

 

//按键被按下的响应处理
void EXTI0_IRQHandler(void)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    
    if(EXTI_GetITStatus(EXTI_Line0) != RESET)//判断中断是否发生
    { 
        if(s_KeyPushCountHandler != NULL)
        {
            xSemaphoreGiveFromISR(s_KeyPushCountHandler, &xHigherPriorityTaskWoken);
        }

        /* Now the buffer is empty we can switch context if necessary. */
        if( xHigherPriorityTaskWoken )
        {
            /* Actual macro used here is port specific. */
            portYIELD_FROM_ISR (xHigherPriorityTaskWoken);
        }
        
        EXTI_ClearITPendingBit(EXTI_Line0); //清除 LINE 上的中断标志位
    }
}

//打印任务1
void PrintTask1(void *p)
{
//    uint16_t currentCount = 0;
    BaseType_t err = 0;
    
    while(1)
    {
        err = xSemaphoreTake(s_KeyPushCountHandler, portMAX_DELAY);
        taskENTER_CRITICAL();           //进入临界区
        vTaskSuspendAll();
        if(err == pdTRUE)
        {
//            currentCount = (uint16_t)uxSemaphoreGetCount(s_KeyPushCountHandler);
            printf("current push count is %d\r\n", (uint32_t)uxSemaphoreGetCount(s_KeyPushCountHandler));
        }
//         xTaskResumeAll();
//        taskEXIT_CRITICAL();            //退出临界区
    }
}

中断里释放一次,任务里获取一次,也就是中断里加1,任务里又减1,计数值不变。

如果我们只释放,不获取,然后打印

//打印任务1
void PrintTask1(void *p)
{    
    while(1)
    {
            printf("current push count is %d\r\n", (uint32_t)uxSemaphoreGetCount(s_KeyPushCountHandler));
    }
}

就会不断打印当前的按键按下次数

可以再配合一个二值信号量,用于标记是否按下按键,按下时再打印按键的按下次数。

可见,计数信号量更侧重于数量的计数,然后传递数据,而不是标志位的判断。

这种情况,其实可以使用单项队列来记录按键按下的次数,队列既能判断,又能传递数据。

信号量使用队列作为其底层机制,因此函数在某种程度上可互操作。

总之,根据实际需要灵活使用freertos的各种机制吧。

注意,释放到最大值的时候就不会再释放了。所以按键按到最后,就会一直打印50这个值。

如果初始值为50呢?那就是资源管理的场景。

之后就是各任务根据需要,先获取信号量,计数值减1,用完之后释放信号量,计数值加1,这种情况通常是成对出现的。相当于使用时占用一个资源,用完后就释放给别人用。如果是最大值为1的计数信号量,其实就相当于二值信号量。

计数信号量只有为0时会阻塞,其他情况都是有有效数据,不会阻塞。

优先级翻转

在使用二值信号量的时候会遇到很常见的一个问题——优先级翻转,优先级翻转在可剥夺内核中是非常常见的,在实时系统中不允许出现这种现象,这样会破坏任务的预期顺序,可能

会导致严重的后果。

所谓的优先级翻转,就是本来应该是高优先级抢占低优先级任务,当有高优先级任务执行时,低优先级的任务就只能等着。但是,如果出现优先级翻转,就会有低优先级任务一直在执行,导致高优先级任务得不到执行,这显然是不符合我们的预期的。

下图所示就是一个优先级翻转的例子:

这个图要说明的就是,当存在高中低三种优先级的任务,其中高和低优先级任务都需要用到一个相同的信号量,但是中优先级的不用这个信号量,此时,低优先级任务先获取该信号量,然后高优先级任务优先级最高就会因此抢占执行,但是因为该信号量此时还被低优先级任务占用未释放,所以高优先级任务就会阻塞,此时,中等优先级任务就会抢占低优先级任务开始执行,只有当中等优先级任务执行完,再等低优先级任务执行完并释放信号量,高优先任务获取到信号量了,才会开始执行。

这么一来,本该优先执行的高优先任务,却是最后一个执行完的。

其过程如下所示:

(1) 任务 H 和任务 M 处于挂起状态,等待某一事件的发生,任务 L 正在运行。

(2) 某一时刻任务 L 想要访问共享资源,在此之前它必须先获得对应该资源的信号量。

(3) 任务 L 获得信号量并开始使用该共享资源。

(4) 由于任务 H 优先级高,它等待的事件发生后便剥夺了任务 L CPU 使用权。

(5) 任务 H 开始运行。

(6) 任务 H 运行过程中也要使用任务 L 正在使用着的资源,由于该资源的信号量还被任务L 占用着,任务 H 只能进入挂起状态,等待任务 L 释放该信号量。

(7) 任务 L 继续运行。

(8) 由于任务 M 的优先级高于任务 L,当任务 M 等待的事件发生后,任务 M 剥夺了任务L 的 CPU 使用权。

(9) 任务 M 处理该处理的事。

(10) 任务 M 执行完毕后,将 CPU 使用权归还给任务 L

(11) 任务 L 继续运行。

(12) 最终任务 L 完成所有的工作并释放了信号量,到此为止,由于实时内核知道有个高优先级的任务在等待这个信号量,故内核做任务切换。

(13) 任务 H 得到该信号量并接着运行。

在这种情况下,任务 H 的优先级实际上降到了任务 L 的优先级水平。因为任务 H 要一直等待直到任务 L 释放其占用的那个共享资源。由于任务 M 剥夺了任务 L CPU 使用权,使得任务 H 的情况更加恶化,这样就相当于任务 M 的优先级高于任务 H,导致优先级翻转。

当一个低优先级任务和一个高优先级任务同时使用同一个信号量,而系统中还有其他中等优先级任务时。如果低优先级任务获得了信号量,那么高优先级的任务就会处于等待状态,但是,中等优先级的任务可以打断低优先级任务而先于高优先级任务运行 (此时高优先级的任务在等待信号量 ,所以不能运行),这是就出现了优先级翻转的现象。

既然优先级翻转是个很严重的问题,那么有没有解决方法呢?有!这就要引出另外一种信号量——互斥信号量!  

互斥信号量

互斥信号量其实就是一个拥有优先级继承的二值信号量,在同步的应用中(任务与任务或中断与任务之间的同步)二值信号量最适合。互斥信号量适合用于那些需要互斥访问的应用中。在 互斥访问中互斥信号量相当于一个钥匙,当任务想要使用资源的时候就必须先获得这个钥匙,当使用完资源以后就必须归还这个钥匙,这样其他的任务就可以拿着这个钥匙去使用资源。

不同于二值信号量的是互斥信号量具有优先级继承的特性。当一个互斥信号量正在被一个低优先级的任务使用,而此时有个高优先级的任务也尝试获取这个互斥信号量的话就会被阻塞,不过虽然如此,此时这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级,这个过程就是优先级继承,从而使得信号量尽快被释放。

优先级继承尽可能地降低了高优先级任务处于阻塞态的时间,并且将已经出现的“优先级翻转”的影响降到最低。

优先级继承并不能完全消除优先级翻转,它只是尽可能的降低优先级翻转带来的影响。

硬实时应用应该在设计之初就要避免优先级翻转的发生。

互斥信号量不能用于中断服务函数中,原因如下:

● 互斥信号量有优先级继承的机制,所以只能用在任务中,不能用于中断服务函数。

● 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态。

创建互斥信号量

FreeRTOS 提供了两个互斥信号量创建函数,如下表所示:

 函数  xSemaphoreCreateMutex()

此函数用于创建一个互斥信号量,所需要的内存通过动态内存管理方法分配。此函数本质

是一个宏,真正完成信号量创建的是函数 xQueueCreateMutex(),此函数原型如下:

SemaphoreHandle_t xSemaphoreCreateMutex( void )

返回值:

NULL: 互斥信号量创建失败。

其他值: 创建成功的互斥信号量的句柄。   

互斥信号量创建过程分析

这里只分析动态创建互斥信号量函数 xSemaphoreCreateMutex (),此函数是个宏,定义如下:

#define xSemaphoreCreateMutex() xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )

可以看出,真正干事的是函数 xQueueCreateMutex(),此函数在文件 queue.c 中有如下定义,

(1)、调用函数 xQueueGenericCreate()创建一个队列,队列长度为 1,队列项长度为 0,队列类型为参数 ucQueueType。由于本函数是创建互斥信号量的,所以参数 ucQueueType

queueQUEUE_TYPE_MUTEX

(2)、调用函数 prvInitialiseMutex()初始化互斥信号量。

互斥信号量创建成功以后会调用函数 xQueueGenericSend()释放一次信号量,说明互斥信号量默认就是有效的!

普通二值信号量创建后默认是空的,表示当前任务条件不满足;

互斥信号量创建后默认是有值的,表示当前钥匙是可获取的,一般在互斥访问中,某个任务进入之前,会先获取互斥信号量,使用完再释放互斥信号量,成对出现。显然,这个普通的二值信号量也能做到,只要在创建后就手动释放一次即可。后续成对使用。普通的二值信号量其实也可以实现互斥访问,但是存在优先级翻转的问题。

更多参考这篇文章

韦东山freeRTOS系列教程之【第七章】互斥量(mutex) - 知乎 (zhihu.com)

尽管互斥锁和二进制信号量很像,但还是不一样。主要的区别是信号量被获取后会发生什么:用于互斥的信号量必须始终返还(take后要give)。用于同步的信号量通常被丢弃而不返还(take后不用give)。还有一个区别是互斥锁有优先级继承

释放互斥信号量

释 放 互 斥 信 号 量 的 时 候 和 二 值 信 号 量 、 计 数 型 信 号 量 一 样 , 都 是 用 的 函 数

xSemaphoreGive()(实际上完成信号量释放的是函数 xQueueGenericSend())

不过由于互斥信号量涉及到优先级继承的问题,所以具体处理过程会有点区别。使用函数 xSemaphoreGive()释放信号 量 最 重 要 的 一 步 就 是 将 uxMessagesWaiting 加 一 , 而 这 一 步 就 是 通 过 函 数prvCopyDataToQueue() 来完成的,释放信号量的函数xQueueGenericSend() 会调用prvCopyDataToQueue()。互斥信号量的优先级继承也是在函数 prvCopyDataToQueue()中完成的。

获取互斥信号量

获取互斥信号量的函数同获取二值信号量和计数型信号量的函数相同,都是xSemaphoreTake()(实际执行信号量获取的函数是 xQueueGenericReceive()),获取互斥信号量的过程也需要处理优先级继承的问题,函数 xQueueGenericReceive()在文件 queue.c 中有定义,可自行查阅。

互斥访问

如果想要多个任务对某个函数进行互斥访问,那么就可以给该函数加上互斥信号量。 

其实就是操作系统课程里说的互斥锁的一种。

函数进入时就上锁,即获取信号量,相当于手上拿了一把通行钥匙,钥匙只有一把,别的任务获取不了信号量,就会阻塞,等待该信号量被释放,函数执行结束,就释放信号量,即返还钥匙,让别的任务能拿着钥匙进入。

互斥信号量使用完成以后一定要释放!否则就会出现钥匙丢失,后面谁都没法进入了。

递归互斥信号量

递归互斥信号量可以看作是一个特殊的互斥信号量,已经获取了互斥信号量的任务就不能再次获取这个互斥信号量,但是递归互斥信号量不同,已经获取了递归互斥信号量的任务可以

再次获取这个递归互斥信号量,而且次数不限!一个任务使用函数xSemaphoreTakeRecursive() 成功的获取了多少次递归互斥信号量就得使用函数xSemaphoreGiveRecursive()释放多少次!比如某个任务成功的获取了 5 次递归信号量,那么这个任务也得同样的释放 5 次递归信号量。

递归互斥信号量也有优先级继承的机制,所以当任务使用完递归互斥信号量以后一定要记

得释放。同互斥信号量一样,递归互斥信号量不能用在中断服务函数中。

● 由于优先级继承的存在,就限定了递归互斥信号量只能用在任务中,不能用在中断服务函数中!

● 中断服务函数不能设置阻塞时间。

要使用递归互斥信号量的话宏 configUSE_RECURSIVE_MUTEXES 必须为 1

创建递归互斥信号量

FreeRTOS 提供了两个互斥信号量创建函数,如表 14.10.2.1 所示:

函数 xSemaphoreCreateRecursiveMutex()

此函数用于创建一个递归互斥信号量,所需要的内存通过动态内存管理方法分配。此函数

本质是一个宏,真正完成信号量创建的是函数 xQueueCreateMutex ()

递归信号量创建过程分析

这里只分析动态创建互斥信号量函数 xSemaphoreCreateRecursiveMutex (),此函数是个宏,定义如下:

可以看出,真正干事的是函数 xQueueCreateMutex(),互斥信号量的创建也是用的这个函数,只是在创建递归互斥信号量的时候类型选择为queueQUEUE_TYPE_RECURSIVE_MUTEX

释放递归互斥信号量

递归互斥信号量有专用的释放函数:xSemaphoreGiveRecursive(),此函数为宏,如下:

#define xSemaphoreGiveRecursive( xMutex )       xQueueGiveMutexRecursive( ( xMutex ) )

函数的参数就是就是要释放的递归互斥信号量,真正的释放是由函数 xQueueGiveMutexRecursive()来完成的。

由于递归互斥信号量可以被一个任务重复的获取,因此在释放的时候也要释放多次,但是只有在最后一次释放的时候才会调用函数 xQueueGenericSend()完成真正的释放。其他释放的话只是简单的将 uxRecursiveCallCount 减一。

获取递归互斥信号量 

递归互斥信号量的获取使用函数 xSemaphoreTakeRecursive(),此函数是个宏,定义如下

函数第一个参数是要获取的递归互斥信号量句柄,第二个参数是阻塞时间。真正的获取过程是由函数 xQueueTakeMutexRecursive()来完成的。

补充

总的来说,信号量有几大作用:

任务同步

互斥访问

事件计数

资源管理

  • 30
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值