1 二值信号量
1.1 二值信号量简介
二值信号量通常用于互斥访问或同步,二值信号量和互斥信号量非常类似,但是还是有一 些细微的差别,互斥信号量拥有优先级继承机制,二值信号量没有优先级继承。因此二值信号量更适合用于同步(任务与任务或任务与中断的同步),而互斥信号量适合用于简单的互斥访问。
和队列一样,信号量 API 函数允许设置一个阻塞时间,阻塞时间是当任务获取信号量的时 候由于信号量无效从而导致任务进入阻塞态的最大时钟节拍数。如果多个任务同时阻塞在同 一个信号量上的话那么优先级最高的哪个任务优先获得信号量,这样当信号量有效的时候高优先级的任务就会解除阻塞状态。
二值信号量其实就是一个只有一个队列项的队列,这个特殊的队列要么是满的,要么是空的,这不正好就是二值的吗? 任务和中断使用这个特殊队列不用在乎队列中存的是什么消息,只需要知道这个队列是满的还是空的。可以利用这个机制来完成任务与中断之间的同步。
使用二值信号量来完成中断与任务同步的这个机制中,任务优先级确保了外设能够得到及时的处理,这样做相当于推迟了中断处理过程。也可以使用队列来替代二值信号量,在外设事件的中断服务函数中获取相关数据,并将相关的数据通过队列发送给任务。如果队列无效的话任务就进入阻塞态,直至队列中有数据,任务接收到数据以后就开始相关的处理过程。
1.2 创建二值信号量
同队列一样,要想使用二值信号量就必须先创建二值信号量,二值信号量创建函数 如表 14.2.2 所示
#define xSemaphoreCreateBinary() xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE )
1.4 释放信号量
不管是二值信号量、计数型信号量还是互斥信号量,释放信号量有两个函数,释放信号量的函数有两个,如表 14.2.4.1 所示。
#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,提示队列满,入队失败。
#define xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken ) xQueueGiveFromISR( ( QueueHandle_t ) ( xSemaphore ), ( pxHigherPriorityTaskWoken ) )
在中断中释放信号量真正使用的是函数 xQueueGiveFromISR(),此函数和中断级通用入队 函数 xQueueGenericSendFromISR()极其类似!只是针对信号量做了微小的改动。函数 xSemaphoreGiveFromISR()不能用于在中断中释放互斥信号量,因为互斥信号量涉及到优先级继承的问题,而中断不属于任务,没法处理中断优先级继承。
1.5 获取信号量
同释放信号量的 API 函数一样,不管是二值信号量、计数型信号量还是互斥信号量,获取信号量也有两个函数,如表 14.2.5.1 所示:
#define xSemaphoreTake( xSemaphore, xBlockTime ) xQueueSemaphoreTake( ( xSemaphore ), ( xBlockTime ) )
获取信号量的过程其实就是读取队列的过程,只是这里并不是为了读取队列中的消息。如果队列为空并且阻塞时间为 0 的话,就立即返回 errQUEUE_EMPTY,表示队列满。如果队列为空并且阻塞时间不为 0 的话就将任务 添加到延时列表中。如果队列不为空的话就从队列中读取数据(获取信号量不执行这一步),数据读取完成以后还需要将队列结构体成员变量 uxMessagesWaiting 减一,然后解除某些因为入 队而阻塞的任务,最后返回 pdPASS 表示出对成功。互斥信号量涉及到优先级继承,处理方式 不同,后面讲解互斥信号量的时候在详细的讲解。
#define xSemaphoreTakeFromISR( xSemaphore, pxHigherPriorityTaskWoken ) xQueueReceiveFromISR( ( QueueHandle_t ) ( xSemaphore ), NULL, ( pxHigherPriorityTaskWoken ) )
在中断中获取信号量真正使用的是函数 xQueueReceiveFromISR (),这个函数就是中断级 出队函数!当队列不为空的时候就拷贝队列中的数据(用于信号量的时候不需要这一步),然后 将队列结构体中的成员变量 uxMessagesWaiting 减一,如果有任务因为入队而阻塞的话就解除 阻塞态,当解除阻塞的任务拥有更高优先级的话就将参数pxHigherPriorityTaskWoken 设置为 pdTRUE,最后返回 pdPASS 表示出队成功。如果队列为空的话就直接返回 pdFAIL 表示出队失 败!这个函数还是很简单的。
2 计数型信号量
2.1 计数型信号量简介
有些资料中也将计数型信号量叫做数值信号量,二值信号量相当于长度为 1 的队列,那么 计数型信号量就是长度大于 1 的队列。同二值信号量一样,用户不需要关心队列中存储了什么 数据,只需要关心队列是否为空即可。计数型信号量通常 用于如下两个场合:
1 、事件计数
在这个场合中,每次事件发生的时候就在事件处理函数中释放信号量(增加信号量的计数值),其他任务会获取信号量(信号量计数值减一,信号量值就是队列结构体成员变量 uxMessagesWaiting来处理事件。在这种场合中创建的计数型信号量初始计数值为 0。
2 、资源管理
在这个场合中,信号量值代表当前资源的可用数量。 一个任务要想获得资源的使用权,首先必须获取信号量,信号量获取成功以后信号量值就会减 一。当信号量值为 0 的时候说明没有资源了。当一个任务使用完资源以后一定要释放信号量, 释放信号量以后信号量值会加一。
2.2 创建计数型信号量
FreeRTOS 提供了两个计数型信号量创建函数,如表 14.4.2.1 所示
计数型信号量也是在队列的基础上实现的,所以需要调用函数 xQueueGenericCreate() 创 建 一 个 队 列 , 队 列 长 度 为 uxMaxCount , 对 列 项 长 度 为 queueSEMAPHORE_QUEUE_ITEM_LENGTH( 此 宏 为 0) , 队 列 的 类 型 为 queueQUEUE_TYPE_COUNTING_SEMAPHORE,表示是个计数型信号量。
#define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ) xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) )
此函数用于创建一个计数型信号量,所需要的内存通过动态内存管理方法分配。此函数本质是一个宏,真正完成信号量创建的是函数 xQueueCreateCountingSemaphore()。
#define xSemaphoreCreateCountingStatic( uxMaxCount, uxInitialCount, pxSemaphoreBuffer ) xQueueCreateCountingSemaphoreStatic( ( uxMaxCount ), ( uxInitialCount ), ( pxSemaphoreBuffer ) )
此函数也是用来创建计数型信号量的,使用此函数创建计数型信号量的时候所需要的内存 需要由用户分配。此函数也是一个宏,真正执行的是函数xQueueCreateCountingSemaphoreStatic(),
3 互斥信号量
3.1 互斥信号量简介
互斥信号量其实就是一个拥有优先级继承的二值信号量,在同步的应用中(任务与任务或中 断与任务之间的同步)二值信号量最适合。互斥信号量适合用于那些需要互斥访问的应用中。在 互斥访问中互斥信号量相当于一个钥匙,当任务想要使用资源的时候就必须先获得这个钥匙,当使用完资源以后就必须归还这个钥匙,这样其他的任务就可以拿着这个钥匙去使用资源。 互斥信号量使用和二值信号量相同的 API 操作函数,所以互斥信号量也可以设置阻塞时间, 不同于二值信号量的是互斥信号量具有优先级继承的特性。当一个互斥信号量正在被一个低优先级的任务使用,而此时有个高优先级的任务也尝试获取这个互斥信号量的话就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级,这个过程就是优先级继承。优先级继承尽可能的降低了高优先级任务处于阻塞态的时间,并且将已经出现的“优先级翻转”的影响降到最低。 优先级继承并不能完全的消除优先级翻转,它只是尽可能的降低优先级翻转带来的影响。 硬实时应用应该在设计之初就要避免优先级翻转的发生。互斥信号量不能用于中断服务函数中, 原因如下:
● 互斥信号量有优先级继承的机制,所以只能用在任务中,不能用于中断服务函数。
● 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态。
3.2 创建互斥信号量
FreeRTOS 提供了两个互斥信号量创建函数,如表 14.8.2.1 所示
#define xSemaphoreCreateMutex() xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )
此函数用于创建一个互斥信号量,所需要的内存通过动态内存管理方法分配。此函数本质 是一个宏。
#define xSemaphoreCreateMutexStatic( pxMutexBuffer ) xQueueCreateMutexStatic( queueQUEUE_TYPE_MUTEX, ( pxMutexBuffer ) )
此函数也是创建互斥信号量的,只不过使用此函数创建互斥信号量的话信号量所需要的 RAM 需要由用户来分配,此函数是个宏。
3.3 释放互斥信号量
释 放 互 斥 信 号 量 的 时 候 和 二 值 信 号 量 、 计 数 型 信 号 量 一 样 , 都 是 用 的 函 数 xSemaphoreGive()(实际上完成信号量释放的是函数 xQueueGenericSend())。不过由于互斥信号量 涉及到优先级继承的问题,所以具体处理过程会有点区别。使用函数 xSemaphoreGive()释放信 号 量 最 重 要 的 一 步 就 是 将uxMessagesWaiting 加 一 , 而 这 一 步 就 是 通 过 函 数 prvCopyDataToQueue() 来 完 成 的 , 释 放 信 号 量 的 函 数 xQueueGenericSend() 会 调 用
prvCopyDataToQueue()。互斥信号量的优先级继承也是在函数 prvCopyDataToQueue()中完成的,
4 递归互斥信号量
递归互斥信号量可以看作是一个特殊的互斥信号量,已经获取了互斥信号量的任务就不能 再次获取这个互斥信号量,但是递归互斥信号量不同,已经获取了递归互斥信号量的任务可以 再次获取这个递归互斥信号量,而且次数不限.一个任务使用函数 xSemaphoreTakeRecursive() 成功的获取了多少次递归互斥信号量就得使用函数 xSemaphoreGiveRecursive()释放多少次!比 如某个任务成功的获取了 5 次递归信号量,那么这个任务也得同样的释放 5 次递归信号量。 递归互斥信号量也有优先级继承的机制,所以当任务使用完递归互斥信号量以后一定要记 得释放。同互斥信号量一样,递归互斥信号量不能用在中断服务函数中。
● 由于优先级继承的存在,就限定了递归互斥信号量只能用在任务中,不能用在中断服务函数中!
● 中断服务函数不能设置阻塞时间。 要使用递归互斥信号量的话宏 configUSE_RECURSIVE_MUTEXES 必须为 1!
5 优先级翻转
在使用二值信号量的时候会遇到很常见的一个问题:优先级翻转。优先级翻转在可剥夺内核中是非常常的,在实时系统中不允许出现这种现象,这样会破坏任务的预期顺序,可能会导致严重的后果。下图是一个优先级翻转的例子。
(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】操作系统之一-任务调度
【freeRTOS】操作系统之二-队列
【freeRTOS】操作系统之三-信号量
【freeRTOS】操作系统之四-事件标志组
【freeRTOS】操作系统之五.-内存管理
【freeRTOS】操作系统之六-低功耗模式
【freeRTOS】操作系统之七-freeRtos移植