目录
什么是信号量?
1.信号量简介
2.二值信号量
2.1二值信号量简介
1. 首先,创建时,二值信号量默认无效
2. 之后中断释放信号量
3.信号量获取成功
4、任务再次进入阻塞态
2.2 创建二值信号量
1、函数vSemaphoreCreateBinary ()
2、函数xSemaphoreCreateBinary()
3、函数xSemaphoreCreateBinaryStatic()
2.3 二值信号量的释放
1 、函数 xSemaphoreGive()
2、函数xSemaphoreGiveFromISR()
2.5 获取信号量
1、函数 xSemaphoreTake()
2、函数xSemaphoreTakeFromISR ()
总结:
什么是信号量?
信号量是操作系统中重要的一部分,信号量一般用来进行资源管理和任务同步FreeRTOS 中信号量又分为二值信号量、计数型信号量、互斥信号量和递归互斥信号量。不同的信号量其应用场景不同,但有些应用场景是可以互换着使用的, 本章我们就来学习一下 FreeRTOS 的信号量。
1.信号量简介
信号量常常有两种使用方式:
1. 用于控制对共享资源的访问
2. 用于任务同步
对于第一种使用方式:一个很常见的例子,某个停车场有 100 个停车位,这 100 个停车位大家都可以用,对于大家来说这 100 个停车位就是共享资源。 假设现在这个停车场正常运行,你要把车停到这个这个停车场肯定要先看一下现在停了多少车 了?还有没有停车位?当前停车数量就是一个信号量,具体的停车数量就是这个信号量值,当这个值到 100 的时候说明停车场满了。停车场满的时你可以等一会看看有没有其他的车开出停车场,当有车开出停车场的时候停车数量就会减一,也就是说信号量减一,此时你就可以把车停进去了,你把车停进去以后停车数量就会加一,也就是信号量加一。这就是一个典型的使用 信号量进行共享资源管理的案例,在这个案例中使用的就是计数型信号量。再看另外一个案例: 使用公共电话,我们知道一次只能一个人使用电话,这个时候公共电话就只可能有两个状态: 使用或未使用,如果用电话的这两个状态作为信号量的话,那么这个就是二值信号量。
信号量用于控制共享资源访问的场景相当于一个上锁机制,代码只有获得了这个锁的钥匙 才能够执行。
第二种方式,应用于任务同步场景,用于任务与任务或中断与任务之间的同步。在执行中断服务函数的时候可以通过向任务 发送信号量来通知任务它所期待的事件发生了,当退出中断服务函数以后在任务调度器的调度下同步的任务就会执行。在编写中断服务函数的时候我们都知道一定要快进快出,中断服务函数里面不能放太多的代码,否则的话会影响的中断的实时性。裸机编写中断服务函数的时候一般都只是在中断服务函数中打个标记,然后在其他的地方根据标记来做具体的处理过程。在使用 RTOS 系统的时候我们就可以借助信号量完成此功能,当中断发生的时候就释放信号量,中 断服务函数不做具体的处理。具体的处理过程做成一个任务,这个任务会获取信号量,如果获 取到信号量就说明中断发生了,那么就开始完成相应的处理,这样做的好处就是中断执行时间非常短。这个例子就是中断与任务之间使用信号量来完成同步,当然了,任务与任务之间也可以使用信号量来完成同步。
FreeRTOS 中还有一些其他特殊类型的信号量,比如互斥信号量和递归互斥信号量,这些具体遇到的时候在讲解。有关信号量的知识在 FreeRTOS 的官网上都有详细的讲解,包括二值信号量、计数型信号量、互斥信号量和递归互斥信号量,我们下面要讲解的这些涉及到理论性的 知识都是翻译自 FreeRTOS 官方资料,感兴趣的可以去官网看原版的英文资料。
2.二值信号量
2.1二值信号量简介
二值信号量通常用于互斥访问或同步,二值信号量和互斥信号量非常类似,但是还是有一些细微的差别,互斥信号量拥有优先级继承机制,二值信号量没有优先级继承。因此二值信号 另更适合用于同步(任务与任务或任务与中断的同步),而互斥信号量适合用于简单的互斥访问,
和队列一样,信号量 API 函数允许设置一个阻塞时间,阻塞时间是当任务获取信号量的时 候由于信号量无效从而导致任务进入阻塞态的最大时钟节拍数。如果多个任务同时阻塞在同一 一个信号量上的话那么优先级最高的哪个任务优先获得信号量,这样当信号量有效的时候高优 先级的任务就会解除阻塞状态。
二值信号量其实就是一个只有一个队列项的队列,这个特殊的队列要么是满的,要么是空 的,这不正好就是二值的吗? 任务和中断使用这个特殊队列不用在乎队列中存的是什么消息,只需要知道这个队列是满的还是空的。可以利用这个机制来完成任务与中断之间的同步。
在实际应用中通常会使用一个任务来处理 MCU 的某个外设,比如网络应用中,一般最简单的方法就是使用一个任务去轮询的查询 MCU 的 ETH(网络相关外设,如 STM32 的以太网 MAC)外设是否有数据,当有数据的时候就处理这个网络数据。这样使用轮询的方式是很浪费 CPU 资源的,而且也阻止了其他任务的运行。最理想的方法就是当没有网络数据的时候网络任 务就进入阻塞态,把 CPU 让给其他的任务,当有数据的时候网络任务才去执行。现在使用二值 信号量就可以实现这样的功能,任务通过获取信号量来判断是否有网络数据,没有的话就进入 阻塞态,而网络中断服务函数(大多数的网络外设都有中断功能,比如 STM32 的 MAC 专用 DMA 中断,通过中断可以判断是否接收到数据)通过释放信号量来通知任务以太网外设接收到了网络 数据,网络任务可以去提取处理了。网络任务只是在一直的获取二值信号量,它不会释放信号 量,而中断服务函数是一直在释放信号量,它不会获取信号量。在中断服务函数中发送信号量 可以使用函数 xSemaphoreGiveFromISR(),也可以使用任务通知功能来替代二值信号量,而且使 用任务通知的话速度更快,代码量更少,有关任务通知的内容后面会有专门的章节介绍。
使用二值信号量来完成中断与任务同步的这个机制中,任务优先级确保了外设能够得到及 时的处理,这样做相当于推迟了中断处理过程。也可以使用队列来替代二值信号量,在外设事件的中断服务函数中获取相关数据,并将相关的数据通过队列发送给任务。如果队列无效的话 任务就进入阻塞态,直至队列中有数据,任务接收到数据以后就开始相关的处理过程。下面几 个步骤演示了二值信号量的工作过程。
1. 首先,创建时,二值信号量默认无效
在图中任务 Task 通过函数 xSemaphoreTake()获取信号量,但是此时二值信号量无 效,所以任务 Task 进入阻塞态。
2. 之后中断释放信号量
此时中断发生了,在中断服务函数中通过函数 xSemaphoreGiveFromISR()释放信号量,因此信号量变为有效。
3.信号量获取成功
由于信号量已经有效了,所以任务 Task 获取信号量成功,任务从阻塞态解除,开始执行相 关的处理过程。
4、任务再次进入阻塞态
由于任务函数一般都是一个大循环,所以在任务做完相关的处理以后就会再次调用函数 xSemaphoreTake()获取信号量。在执行完第三步以后二值信号量就已经变为无效的了,所以任务 将再次进入阻塞态,和第一步一样,直至中断再次发生并且调用函数 xSemaphoreGiveFromISR() 释放信号量。
2.2 创建二值信号量
同队列一样,要想使用二值信号量就必须先创建二值信号量,二值信号量创建函数如下表:
1、函数vSemaphoreCreateBinary ()
此函数是老版本 FreeRTOS 中的创建二值信号量函数,新版本已经不再使用了,新版本的 FreeRTOS 使用 xSemaphoreCreateBinary()来替代此函数,这里还保留这个函数是为了兼容那些 基 于 老版 本 FreeRTOS 而做 的应用层代码 。此函数 是个宏 , 具体创建过程是由函 数 xQueueGenericCreate()来完成的,在文件 semphr.h 中有如下定义:
void vSemaphoreCreateBinary( SemaphoreHandle_t xSemaphore )
参数:
xSemaphore :保存创建成功的二值信号量句柄。
返回值:
NULL: 二值信号量创建失败。
其他值: 二值信号量创建成功。
2、函数xSemaphoreCreateBinary()
此函数是 vSemaphoreCreateBinary()的新版本,新版本的 FreeRTOS 中统一用此函数来创建二值信号量。使用此函数创建二值信号量的话信号量所需要的 RAM 是由 FreeRTOS 的内存管 理部分来动态分配的。此函数创建好的二值信号量默认是空的,也就是说刚创建好的二值信号 量使用函数xSemaphoreTake()是获取不到的, 此函数也是个宏, 具体创建过程是由函数 xQueueGenericCreate()来完成的,函数原型如下:
SemaphoreHandle_t xSemaphoreCreateBinary( void )
参数:
无
返回值:
NULL: 二值信号量创建失败。
其他值: 创建成功的二值信号量的句柄。
3、函数xSemaphoreCreateBinaryStatic()
此函数也是创建二值信号量的,只不过使用此函数创建二值信号量的话信号量所需要的 RAM 需要由用户来分配,此函数是个宏,具体创建过程是通过函数xQueueGenericCreateStatic() 来完成的,函数原型如下:
SemaphoreHandle_t xSemaphoreCreateBinaryStatic(StaticSemaphore_t *pxSemaphoreBuffer )
参数:
pxSemaphoreBuffer:此参数指向一个 StaticSemaphore_t 类型的变量,用来保存信号量结构体。
返回值:
NULL: 二值信号量创建失败。
其他值: 创建成功的二值信号量句柄。
2.3 二值信号量的释放
释放信号量的函数有两个,如下表:
同队列一样,释放信号量也分为任务级和中断级。还有! 不管是二值信号量、计数型信号量还是互斥信号量,它们都使用上表中的函数释放信号量,递归互斥信号量有专用的释放函数。
1 、函数 xSemaphoreGive()
此函数用于释放二值信号量、计数型信号量或互斥信号量,此函数是一个宏,真正释放信 号量的过程是由函数 xQueueGenericSend()来完成的,函数原型如下:
BaseType_t xSemaphoreGive( xSemaphore )
参数:
xSemaphore:要释放的信号量句柄。
返回值:
pdPASS: 释放信号量成功。
errQUEUE_FULL: 释放信号量失败。
我们再来看一下函数 xSemaphoreGive()的具体内容,此函数在文件 semphr.h 中有如下定义:
#define xSemaphoreGive( xSemaphore )
xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ),
NULL,
semGIVE_BLOCK_TIME,
queueSEND_TO_BACK )
可以看出任务级释放信号量就是向队列发送消息的过程,只是这里并没有发送具体的消息, 阻塞时间为 0(宏 semGIVE_BLOCK_TIME 为 0),入队方式采用的后向入队,入队的时候队列结构体成员变量 uxMessagesWaiting 会加一,对于二值信号量通过判断 uxMessagesWaiting 就可以知道信号量是否有效了,当 uxMessagesWaiting 为 1 的话说明二值信号量有效,为 0 就无效。如果队列满的话就返回错误值 errQUEUE_FULL,提 示队列满,入队失败。
2、函数xSemaphoreGiveFromISR()
此函数用于在中断中释放信号量,此函数只能用来释放二值信号量和计数型信号量,绝对 不 能用来在中断服务函数中释放互斥信号量!此函数是一个宏,真正执行的是函数 xQueueGiveFromISR() ,此函数原型如下:
BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore,
BaseType_t * pxHigherPriorityTaskWoken)
参数:
xSemaphore : 要释放的信号量句柄。
pxHigherPriorityTaskWoken : 标记退出此函数以后是否进行任务切换,这个变量的值由这
三个函数来设置的,用户不用进行设置,用户只需要提供一 个变量来保存这个值就行了。当此值为 pdTRUE 的时候在退 出中断服务函数之前一定要进行一次任务切换。
返回值:
pdPASS: 释放信号量成功。
errQUEUE_FULL: 释放信号量失败。
在中断中释放信号量真正使用的是函数 xQueueGiveFromISR(),此函数和中断级通用入队函数 xQueueGenericSendFromISR() 极其类似!
只是针对信号量做了微 小 的 改 动 。 函 数 xSemaphoreGiveFromISR()不能用于在中断中释放互斥信号量,因为互斥信号量涉及到优先级继 承的问题,而中断不属于任务,没法处理中断优先级继承。大家可以参考第十三章分析函数 xQueueGenericSendFromISR()的过程来分析 xQueueGiveFromISR()。
2.5 获取信号量
获取信号量也有两个函数,如下表:
同释放信号量的 API 函数一样,不管是二值信号量、计数型信号量还是互斥信号量,它们都使用表中的函数获取信号量
1、函数 xSemaphoreTake()
此函数用于获取二值信号量、计数型信号量或互斥信号量,此函数是一个宏,真正获取信 号量的过程是由函数 xQueueGenericReceive ()来完成的,函数原型如下:
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore,
TickType_t xBlockTime)
参数:
xSemaphore:要获取的信号量句柄。 xBlockTime: 阻塞时间。
返回值:
pdTRUE: 获取信号量成功。
pdFALSE: 超时,获取信号量失败。
再来看一下函数 xSemaphoreTake ()的具体内容,此函数在文件 semphr.h 中有如下定义:
#define xSemaphoreTake( xSemaphore, xBlockTime )
xQueueGenericReceive( ( QueueHandle_t ) ( xSemaphore ),
NULL,
( xBlockTime ),
pdFALSE )
取信号量的过程其实就是读取队列的过程,只是这里并不是为了读取队列中的消息。在讲解函数 xQueueGenericReceive()的时候说过如果队列为空并且阻塞时间为 0 的话就立即返回 errQUEUE_EMPTY,表示队列满。如果队列为空并且阻塞时间不为 0 的话就将任务 添加到延时列表中。如果队列不为空的话就从队列中读取数据(获取信号量不执行这一步),数 据读取完成以后还需要将队列结构体成员变量 uxMessagesWaiting 减一,然后解除某些因为入队而阻塞的任务,最后返回 pdPASS 表示出对成功。互斥信号量涉及到优先级继承,处理方式不同,后面讲解互斥信号量的时候在详细的讲解。
2、函数xSemaphoreTakeFromISR ()
此函数用于在中断服务函数中获取信号量,此函数用于获取二值信号量和计数型信号量, 绝对不能使用此函数来获取互斥信号量 ! 此函数是一个宏 , 真正执行的是函数 xQueueReceiveFromISR (),此函数原型如下:
BaseType_t xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore,
BaseType_t * pxHigherPriorityTaskWoken)
参数:
xSemaphore: 要获取的信号量句柄。
pxHigherPriorityTaskWoken : 标记退出此函数以后是否进行任务切换,这个变量的值由这
三个函数来设置的,用户不用进行设置,用户只需要提供一 个变量来保存这个值就行了。当此值为 pdTRUE 的时候在退 出中断服务函数之前一定要进行一次任务切换。
返回值:
pdPASS: 获取信号量成功。
pdFALSE: 获取信号量失败。
在中断中获取信号量真正使用的是函数 xQueueReceiveFromISR (),这个函数就是中断级出队函数!当队列不为空的时候就拷贝队列中的数据(用于信号量的时候不需要这一步),然后将队列结构体中的成员变量 uxMessagesWaiting 减一,如果有任务因为入队而阻塞的话就解除阻塞态,当解除阻塞的任务拥有更高优先级的话就将参数 pxHigherPriorityTaskWoken 设置为 pdTRUE,最后返回 pdPASS 表示出队成功。如果队列为空的话就直接返回 pdFAIL 表示出队失败!这个函数还是很简单的。
总结:
FreeRTOS的二值信号量是我们学习信号量的基础,通过它,我们可以更好的了解信号量的知识,即信号量用于任务同步场景时的作用,也能够让我们了解信号量的基础操作,比如信号量创建、释放与获取函数,有了这些基础,可以让我们更加轻松的学习数值型信号量、互斥信号量。而且二值信号量防止中断执行过长的功能,也让FreeRTOS系统变得更加稳定,是实时系统的不可或缺的一部分,值得深入学习。