FreeRTOS 中的信号量是一种重要的同步机制,用于实现任务之间或任务与中断之间的通信和资源共享管理。信号量最初是为解决并发编程中互斥访问共享资源的问题而设计的,但在 FreeRTOS 中还可以用于任务间的同步。
FreeRTOS 提供了三种主要类型的信号量:
- 二值信号量(Binary Semaphore)
- 只有两个状态:空(0) 和 满(1)。
- 常用于任务间同步和中断与任务的同步。
- 在创建时,二值信号量可以通过 xSemaphoreCreateBinary() 或 xSemaphoreCreateBinaryStatic() 创建。
- 计数信号量(Counting Semaphore)
- 允许多个任务或中断访问资源,类似于一个计数器。
- 计数信号量的计数值可以大于 1,表示同一时间允许多个任务获取信号量。
- 常用于管理多个等价资源或实现事件计数。
- 在创建时,可以通过 xSemaphoreCreateCounting() 或 xSemaphoreCreateCountingStatic() 创建。
- 互斥量(Mutex)
- 专门用于保护共享资源,实现任务间的互斥访问。
- 互斥量具有优先级继承机制,用于防止优先级反转问题。
- 互斥量始终处于二值信号量的状态,但为互斥访问场景进行了特殊优化。
- 在创建时,可以通过 xSemaphoreCreateMutex() 或 xSemaphoreCreateMutexStatic() 创建。
提示:在许多使用场景中,使用直达任务通知要比使用信号量的速度更快,内存效率更高。
二进制信号量
二进制信号量用于互斥和同步目的。
二进制信号量和互斥锁极为相似,但存在一些细微差别:互斥锁包括优先继承机制, 而二进制信号量则不然。因此,二进制信号量是实现同步的更好选择(任务之间或任务与中断之间), 而互斥锁是实现简单互斥的更好选择。
信号量相关函数允许指定阻塞时间。阻塞时间表示在尝试“获取”信号量时, 如果信号量不是立即可用, 任务应进入阻塞状态的最大“滴答”数。如果多个任务在同一信号量上阻塞, 则具有最高优先级的任务将成为下次信号量可用时解除阻塞的任务。
可将二进制信号量视为仅能容纳一个项目的队列。因此,队列只能为空或满 (因此称为二进制)。
使用信号量同步任务与中断。中断仅“提供” 信号量,而任务仅“获取”信号量。
xSemaphoreGiveFromISR 用于释放信号量
xSemaphoreTake 用于获取信号量的宏。
信号量的“给出”和“获取”机制:
- 二进制信号量的计数值最多只能为 1(表示信号量可用)。
- 当一个任务成功调用 xSemaphoreTake 时,信号量的计数值立即变为
0
,表示信号量被“占用”。 - 在信号量变为 0 后,其他试图获取信号量的任务都会进入阻塞状态,直到信号量被重新“给出”并变为 1。
API
xSemaphoreCreateBinary
创建一个二进制信号量,并返回一个可以引用该信号量的句柄。
每个二进制信号量需要少量 RAM,用于保存信号量状态。如果 使用 xSemaphoreCreateBinary() 创建了二进制信号量,那么所需的 RAM 将自动从 FreeRTOS 堆中分配。如果二进制信号量 是使用 xSemaphoreCreateBinaryStatic() 创建的,则 RAM 由应用程序写入器提供,这需要一个附加参数,但允许 RAM 在编译时进行静态分配 。
1 | SemaphoreHandle_t xSemaphoreCreateBinary( void ); |
信号量是在“空”状态下创建的,这意味着必须先用 xSemaphoreGive() API 函数给出信号量, 然后才能使用 xSemaphoreTake() 函数来获取(获得)该信号量。
二值信号量并不需要在得到后立即释放, 因此,任务同步可以通过一个任务/中断持续释放信号量 而另外一个持续获得信号量来实现。
与互斥锁不同, 二进制信号量可用于中断服务程序。
返回值:
NULL
因为可用 FreeRTOS 堆不足,所以无法创建信号量。
其他任何值
信号量已成功创建。返回值是一个句柄, 通过该句柄可以引用信号量。
示例
1 | xSemaphore = xSemaphoreCreateBinary(); |
xSemaphoreTake
用于获取信号量的。
不得从 ISR 调用此宏。如果需要,xQueueReceiveFromISR() 可用来从中断中获取一个信号量, 尽管这不是正常操作。信号量使用队列作为 其底层机制,因此函数在某种程度上可互操作。
1 | xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait); |
参数:
xSemaphore
正在获取的信号量的句柄——在创建信号量时获得。
xTicksToWait
等待信号量变为可用的时间(以滴答为单位)。
如果 INCLUDE_vTaskSuspend 设置为 1,则将阻塞时间指定为 portMAX_DELAY 会导致任务无限期地阻塞(没有超时限制)。
返回:
- 如果获得信号量,则返回 pdTRUE。
- 如果 xTicksToWait 过期,信号量不可用,则返回 pdFALSE。
示例
1 | if( xSemaphoreTake(xSemaphore, ( TickType_t )10) == pdTRUE ) { |
xSemaphoreGive
用于释放信号量的。释放前信号量必须已经创建
参数:
xSemaphore
要释放的信号量的句柄。这是创建信号量时返回的句柄。 |
返回:
- 如果信号量被释放,则返回 pdTRUE。
- 如果发生错误,则返回 pdFALSE。信号量是使用队列实现的。发布消息时,如果队列上没有空间, 那么可能会发生错误,这表明最初未能正确获取信号量。
示例
1 | if( xSemaphoreGive( xSemaphore ) != pdTRUE ) { |
xSemaphoreTakeFromISR
可从 ISR 调用的 xSemaphoreTake() 版本。与 xSemaphoreTake() 不同,xSemaphoreTakeFromISR() 不允许指定阻塞时间。
1 | xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore, signed BaseType_t *pxHigherPriorityTaskWoken) |
参数:
xSemaphore
信号量被“获取”。信号量由 SemaphoreHandle_t 类型的变量引用, 必须在使用之前显式创建。
pxHigherPriorityTaskWoken
信号量由于其类型可能阻塞一个或多个任务,等待给出信号量。调用 xSemaphoreTakeFromISR() 会尝试获取信号量,如果成功获取信号量,则会唤醒一个正在等待该信号量的任务,使其离开阻塞状态。
如果调用函数导致任务离开阻塞状态, 且未阻塞任务的优先级等于或高于当前正在执行的任务(被中断的任务),那么 API 函数将从内部把 *pxHigherPriorityTaskWoken 设置为 pdTRUE。
如果 xSemaphoreTakeFromISR() 将 pxHigherPriorityTaskWoken 设置为 pdTRUE,则应在退出中断之前执行上下文切换。这将确保中断直接返回到最高优先级的就绪状态任务(返回优先级最高的任务,而不是被中断的任务) 。该机制与 xQueueReceiveFromISR() 函数中使用的机制相同, pxHigherPriorityTaskWoken 是一个可选参数,可设置为 NULL。
返回:
如果成功获取信号量,则返回 pdTRUE。如果因为信号量不可用而未成功获取信号量, 则返回 pdFALSE。
xSemaphoreGiveFromISR
用于释放信号量的宏。释放前信号量必须已经 通过调用 xSemaphoreCreateBinary() 或 xSemaphoreCreateCounting() 创建。
互斥锁型信号量(那些调用 xSemaphoreCreateMutex() 创建的信号量) 不得与此宏一起使用。
1 | xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore, signed BaseType_t *pxHigherPriorityTaskWoken) |
参数:
xSemaphore
要释放的信号量的句柄。这是创建信号量时返回的句柄。
pxHigherPriorityTaskWoken
如果给出信号量会导致任务解除阻塞,并且解除阻塞的任务的优先级高于当前正在运行的任务, 则 xSemaphoreGiveFromISR() 会将 *pxHigherPriorityTaskWoken 设置为 pdTRUE。
如果 xSemaphoreGiveFromISR() 将此值设置为 pdTRUE,则应在退出中断之前请求上下文切换。
从 FreeRTOS V7.3.0 开始,pxHigherPriorityTaskWoken 为可选参数, 可设置为 NULL。
返回:
如果成功给出信号量,则返回 pdTRUE,否则 errQUEUE_FULL。
实现
开启两个任务,分别去等待信号。
开启按键扫描任务,当点击按键时,发送信号
1 |
|
计数信号量
正如二进制信号量可以被认为是长度为 1 的队列那样, 计数信号量也可以被认为是长度大于 1 的队列。同样,信号量的使用者对存储在队列中的数据并不感兴趣, 他们只关心队列是否为空。
计数信号量通常用于两种情况:
盘点事件。
在此使用场景中,事件处理程序将在每次事件发生时“提供”信号量(递增信号量计数值), 而处理程序任务将在每次处理事件时“获取”信号量 (递减信号量计数值)。因此,计数值是已发生的事件数与已处理的事件数之间的差值。在这种情况下, 创建信号量时希望计数值为零。
资源管理。
在此使用情景中,计数值表示可用资源的数量。为了获得对资源的控制,任务必须首先获得信号量——递减信号量计数值。当计数值达到零时, 表示没有空闲资源可用。当任务结束使用资源时, 它会“返还”一个信号量——同时递增信号量计数值。在这种情况下, 创建信号量时希望计数值等于最大计数值。
计数信号量的“给出”和“获取”机制
给出机制:
“给出”信号量表示资源或事件的增加,可以理解为将信号量的计数值递增。
当调用 xSemaphoreGive() 或 xSemaphoreGiveFromISR() 时:
如果当前信号量的计数值小于最大允许值,则计数值增加 1。
如果计数值已达到最大值(通常在创建信号量时定义),进一步的“给出”操作不会改变计数值,函数返回
pdFAIL
。如果有任务正在等待此信号量且任务的优先级足够高,则调度器会将该任务从阻塞状态移到就绪状态。
获取机制:
“获取”信号量表示使用资源或响应事件,可以理解为将信号量的计数值递减。
当调用 xSemaphoreTake() 或 xSemaphoreTakeFromISR() 时:
如果当前信号量的计数值大于零,则计数值减少 1,函数返回 pdPASS。
如果计数值等于零:
- 如果指定了阻塞时间(xTicksToWait),任务进入阻塞状态,等待计数值变为大于零。
- 如果阻塞时间为 0,函数立即返回 pdFAIL,表示获取失败。
API
xSemaphoreCreateCounting
创建一个计数信号量, 并返回一个可以引用该新建信号量的句柄。
configSUPPORT_DYNAMIC_ALLOCATION 必须在 FreeRTOSConfig.h 中设置为 1,或处于未定义状态(在这种情况下,默认为 1),该 RTOS API 函数 才可用。
每个计数信号量需要少量 RAM ,用于保存信号量的状态。如果使用 xSemaphoreCreateCounting() 创建计数信号量则所需的 RAM 将从 FreeRTOS 堆自动分配。 如果使用 xSemaphoreCreateCountingStatic() 创建计数信号量, 则 RAM会由应用程序编写器提供,这需要其他的 但允许在编译时静态分配 RAM 。
1 | SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount); |
参数:
uxMaxCount
可以达到的最大计数值。当信号量达到此值时,它不能再被“给定”。
uxInitialCount
创建信号量时分配给信号量的计数值。
返回:
- 如果已成功创建信号量,则将返回该信号量的句柄 。
- 如果因为保留信号量所需的 RAM 无法分配而无法创建信号量, 则会返回 NULL。
示例
1 | xSemaphore = xSemaphoreCreateCounting( 10, 0 ); |
uxSemaphoreGetCount
获取信号量的计数值。
1 | UBaseType_t uxSemaphoreGetCount( SemaphoreHandle_t xSemaphore ); |
参数:
xSemaphore
正在查询的信号量的句柄。
返回:
如果信号量是计数信号量,则返回信号量的当前计数值 。如果信号量是二进制信号量, 则当信号量可用时,返回 1,当信号量不可用时, 返回 0。
xSemaphoreTake
同上
xSemaphoreGive
同上
xSemaphoreTakeFromISR
同上
xSemaphoreGiveFromISR
同上
实现
- 开启两个任务,等待信号,接收到信号后,处理耗时操作
- 开启按键扫描,点击按键时发送信号
1 |
|
互斥锁
互斥锁是包含优先级继承机制的二进制信号量。鉴于二进制信号量是实现同步(任务之间或任务与中断之间) 的更好方式,因此互斥锁更适合实现简单的 相互排斥(即互斥)。
用于互斥时,互斥锁就像用于保护资源的令牌。当一个 任务希望访问资源时,必须首先获得(“获取”)该令牌。使用完资源后, 任务必须“返还”令牌,以便其他任务有机会访问 相同的资源。
优先级继承机制
互斥锁使用相同的信号量访问 API 函数,因此也能指定阻塞时间。该 阻塞时间表示一个任务试图“获取”互斥锁,而互斥锁无法立即使用时, 任务应进入阻塞状态的最大“滴答”数。然而,与二进制信号量不同, 互斥锁采用优先级继承机制。这意味着如果高优先级任务进入阻塞状态,同时尝试获取当前由低优先级任务持有的互斥锁(令牌), 则持有令牌的任务的优先级会暂时提高到阻塞任务的优先级。这项机制 旨在确保较高优先级的任务保持阻塞状态的时间尽可能短, 从而最大限度减少已经发生的“优先级反转”现象。
优先级继承无法解决优先级反转!只是在某些情况下将影响降至最低。应用程序的设计应从一开始就避免发生优先级反转。
不能从中断中使用互斥锁
- 互斥锁使用的优先级继承机制要求从任务中(而不是从中断中)拿走和放入互斥锁。
- 中断无法保持阻塞来等待一个被互斥锁保护的资源变为可用。
互斥锁的“给出”和“获取”机制
任务请求使用共享资源时,需要通过“获取”互斥锁来确保独占访问权。
调用 xSemaphoreTake() 或 xSemaphoreTakeRecursive() 时:
锁可用:如果互斥锁当前未被其他任务占用,则获取成功,并将锁的“拥有者”设置为当前任务,函数返回 pdPASS。
锁不可用:如果互斥锁已被其他任务占用,则任务进入阻塞状态,直到:
- 锁被释放。
- 超过了指定的阻塞时间(xTicksToWait)。
递归支持(Recursive Mutex):如果当前任务已获取了互斥锁,递归获取是允许的,计数会递增(只适用于递归互斥锁)。
当任务完成资源的使用后,必须通过“给出”互斥锁将其释放,允许其他任务继续使用。
调用 xSemaphoreGive() 或 xSemaphoreGiveRecursive() 时:
当前任务为锁的拥有者:互斥锁被释放,允许其他等待的任务获取锁。
当前任务不是锁的拥有者:释放操作无效(仅在调试模式下可能触发断言错误)。
递归支持(Recursive Mutex):如果任务递归获取了多次互斥锁,则每次“给出”都会使计数递减,直到完全释放为止。
API
xSemaphoreCreateMutex
创建互斥锁,并返回 一个该互斥锁可以引用的句柄。中断服务例程中, 不能使用互斥锁。
configSUPPORT_DYNAMIC_ALLOCATION 和 configUSE_MUTEXES 必须同时在 FreeRTOSConfig.h 中设置为 1,xSemaphoreCreateMutex() 才可用。(configSUPPORT_DYNAMIC_ALLOCATION 也可以不定义, 在这种情况下,它将默认为 1。)
每个互斥锁需要少量 RAM , 以此来保持互斥锁的状态。如果互斥锁是使用 xSemaphoreCreateMutex() 创建的, 则所需的 RAM 将从 FreeRTOS 堆自动分配。 如果互斥锁是使用 xSemaphoreCreateMutexStatic() 创建的, 那么应由应用程序写入器提供 RAM, 但允许在编译时静态分配 RAM 。
使用 xSemaphoreTake() 获取互斥锁, 并使用 xSemaphoreGive() 给出互斥锁。
1 | SemaphoreHandle_t xSemaphoreCreateMutex( void ) |
返回:
- 如果已成功创建互斥锁型信号量,则返回创建的互斥锁的句柄。
- 如果由于无法分配保存互斥锁所需的内存而未创建互斥锁,则返回 NULL。
1 | xSemaphore = xSemaphoreCreateMutex(); |
xSemaphoreTake
同上
xSemaphoreGive
同上
实现
开启两个任务,同时等待和发送信号,观察任务调用
1 |
|
递归互斥锁
用户可对一把递归互斥锁重复加锁。只有用户为每成功的 xSemaphoreTakeRecursive() 请求调用 xSemaphoreGiveRecursive() 后,互斥锁才会重新变为可用。
例如,如果一个任务成功“加锁”相同的互斥锁 5 次, 那么任何其他任务都无法使用此互斥锁,直到任务也把这个互斥锁“解锁”5 次。
这种类型的信号量使用优先级继承机制,因此“加锁”一个信号量的任务必须在不需要此信号量时, 立即将信号量“解锁”。
不能从中断服务程序中使用类型是互斥锁的信号量。
不能从中断中使用互斥锁的原因是:
- 互斥锁使用的优先级继承机制要求 从任务中(而不是从中断中)拿走和放入互斥锁。
- 中断无法保持阻塞来等待一个被互斥锁保护的资源 变得可用。
递归互斥锁的“给出”和“获取”机制
递归互斥锁的基本思想是,每当一个任务成功获取锁时,锁的计数值会增加;当任务释放锁时,锁的计数值会减少。当计数值为零时,锁才会被真正释放,并允许其他任务获取该锁。
获取锁(Take)机制
- 当一个任务首次请求获取递归互斥锁时,如果锁当前没有被占用,任务会立即获得锁,并将锁的计数器设为 1。
- 如果该任务再次请求获取该锁,它会成功获取锁(锁的计数器加 1),并继续执行嵌套的代码段。
- 锁的计数器会随着任务的每次获取递增,这样任务就可以在嵌套的调用中多次获取该锁,而不会发生死锁。
释放锁(Give)机制
- 当任务释放锁时,锁的计数器会减少。如果计数器的值大于 1,表示任务还有其他嵌套的调用持有锁,释放锁只是减少计数器的值,锁仍然被当前任务持有。
- 当任务释放锁时,如果计数器的值减至 0,表示当前任务已经没有
API
xSemaphoreCreateRecursiveMutex
创建一个递归互斥锁,并返回一个可以引用该互斥锁的句柄 。不能在中断服务程序中使用递归互斥锁。
1 | SemaphoreHandle_t xSemaphoreCreateRecursiveMutex( void ) |
configSUPPORT_DYNAMIC_ALLOCATION 和 configUSE_RECURSIVE_MUTEXES 必须在 FreeRTOSConfig.h 中设置为 1,xSemaphoreCreateRecursiveMutex() 才可用(configSUPPORT_DYNAMIC_ALLOCATION 也可以不定义,这种情况下 默认为 1)。
每个递归互斥锁都需要少量 RAM,用于保存递归互斥锁的状态。 如果互斥锁是使用 xSemaphoreCreateRecursiveMutex() 创建的, 则所需的 RAM 将从 FreeRTOS 堆自动分配。 如果一个递归互斥锁是使用 xSemaphoreCreateRecursiveMutexStatic() 创建的, 那么RAM 由应用程序写入器提供,这需要用到一个附加参数, 但允许在编译时静态分配 RAM 。
递归互斥锁分别使用 xSemaphoreTakeRecursive() 和 xSemaphoreGiveRecursive() API 函数“获取”和“释放”。 不得使用 xSemaphoreTake() 和 xSemaphoreGive()。
xSemaphoreCreateMutex()和 xSemaphoreCreateMutexStatic()用于创建非递归互斥锁。非递归互斥锁只能被一个任务获取一次(在释放前),如果同一个任务想再次获取则会失败, 因为当任务第一次释放互斥锁时,互斥锁就一直处于释放状态。
与非递归互斥锁相反,递归互斥锁可以被同一个任务获取很多次, 获取多少次就需要释放多少次, 此时才会返回递归互斥锁。
返回:
如果已成功创建递归互斥锁,则返回创建的互斥锁的句柄。
如果由于无法分配保递归互斥锁所需的内存而未创建递归互斥锁,则返回 NULL。
示例
1 | xMutex = xSemaphoreCreateRecursiveMutex(); |
xSemaphoreTakeRecursive
递归的“获取”一个互斥锁型信号量。 此互斥锁必须已经事先通过调用 xSemaphoreCreateRecursiveMutex() 完成创建;
1 | xSemaphoreTakeRecursive(SemaphoreHandle_t xMutex, TickType_t xTicksToWait ); |
必须在 FreeRTOSConfig.h 中将 configUSE_RECURSIVE_MUTEXES 设置为 1, 此宏才可用。
不得在使用 xSemaphoreCreateMutex() 创建的互斥锁上使用此宏。
所有者可以反复“获取”递归使用的互斥锁。在所有者 为每个成功的“获取”请求调用 xSemaphoreGiveRecursive() 之前,该互斥锁不会再次变得可用。例如, 如果一个任务成功地“获取”了同一个互斥锁 5 次, 那么任何其他任务都无法使用此互斥锁, 直到任务也把这个互斥锁“解锁”5 次。
参数:
xMutex
正在获得的互斥锁的句柄。这是由 xSemaphoreCreateRecursiveMutex() 返回的句柄。
xTicksToWait
等待信号量变为可用的时间(以滴答为单位)。可以使用 portTICK_PERIOD_MS 宏 将其转换为实际时间。可以用一个为零的阻塞时间来轮询信号量。如果 任务已有信号量,则无论 xTicksToWait 的值是多少, xSemaphoreTakeRecursive() 都将立即返回。
返回:
如果获得信号量,则返回 pdTRUE;如果 xTicksToWait 过期,信号量不可用,则返回 pdFALSE。
1 | if( xSemaphoreTakeRecursive( xMutex, ( TickType_t ) 10 ) == pdTRUE ) { |
xSemaphoreGiveRecursive
递归地释放或“给出”一个互斥锁型信号量的宏。 此互斥锁必须已经事先通过调用 xSemaphoreCreateRecursiveMutex() 完成创建;
1 | xSemaphoreGiveRecursive( SemaphoreHandle_t xMutex ) |
必须在 FreeRTOSConfig.h 中将 configUSE_RECURSIVE_MUTEXES 设置为 1, 此宏才可用。
不得在使用 xSemaphoreCreateMutex() 创建的互斥锁上使用此宏。
所有者可以反复“获取”递归互斥锁。在所有者为每个成功的“获取”请求调用 xSemaphoreGiveRecursive() 之前,该互斥锁不会再次变得可用。
例如, 如果一个任务成功地“获取”了同一个互斥锁 5 次, 那么任何其他任务都无法使用此互斥锁, 直到任务也把这个互斥锁“解锁”5 次。
参数:
xMutex
正在释放或“给出”的互斥锁的句柄。这是由 xSemaphoreCreateRecursiveMutex() 返回的句柄。
返回:
如果成功给出信号量,则返回 pdTRUE。
实现
1 |
|
优先级继承
FreeRTOS 实现了基本的优先级继承机制,旨在优化 空间和执行周期。完全的优先级继承机制需要多得多的数据和处理器 周期来确定任何时刻的继承优先级,特别是在任务同时占用超过一个互斥锁时 。
请牢记优先级继承机制的这些特定行为:
- 如果一个任务在占用一个互斥锁时没有先释放它已占用的互斥锁, 则可以进一步提升其继承优先级。
- 任务在释放其占有的所有互斥锁之前,一直保持最高继承优先级。 这与释放互斥锁的顺序无关。
- 如果多个互斥锁被占用,无论在任何一个被占用的互斥锁上等待的任务是否完成等待(超时), 则任务将保持最高继承优先级 。