信号量的简介
信号量是进程间用于通信的一种手段,其是基于队列实现的,信号量更适用于进程间同步,信号量包括二值信号量(Binary Semaphores)和计数信号量(Counting Semaphores)
任务的同步和互斥
《RTOS 中的同步与互斥》 在实时操作系统(RTOS)中,同步是不同任务之间或者任务与外部事件之间的协同工作方式,确保多个并发执行的任务按照预期的顺序或时机执行。同步涉及到线程或任务间的通信和协调机制,其目的在于避免数据竞争、解决竞态条件,并确保系统的正确行为。 而互斥则是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。
当计数值大于0,代表有信号量资源
当释放信号量,信号量计数值(资源数)加一
当获取信号量,信号量计数值(资源数)减一
信号量的计数值都有限制:限定最大值。
如果最大值被限定为1,那么它就是二值信号量
;
如果最大值不是1,它就是计数型信号量
。
信号量:用于传递状态
队列与信号量的对比
队列 | 信号量 |
---|---|
可以容纳多个数据; 创建队列有两部分内存:队列结构体+队列项存储空间 | 仅存放计数值,无法存放其他数据; 创建信号量,只需分配信号量结构体 |
写入队列:当队列满时,可阻塞; | 释放信号量:不可阻塞,计数值++, 当计数值为最大值时,返回失败 |
读取队列:当队列为空时,可阻塞; | 获取信号量:计数值–, 当没有资源时,可阻塞 |
二值信号量和计数型信号量
二值信号量就是只有一个项的队列,该队列不为空则为满(所谓二值),二值信号量就像一个标志,适和用于进程间同步的通信
二值信号量通常用于互斥访问或任务同步, 与互斥信号量比较类似,但是二值信号量有可能会导致优先级翻转的问题 ,所以二值信号量更适合用于同步!
计数信号量就是有固定长度的队列,队列中每个单元都是一个标志,其通常用于对多个共享资源的访问进行控制
创建信号量
信号量在使用之前也必须先创建,信号量被创建完之后是无效的,也即为 0 ,而由于信号量分为二值信号量和计数信号量两种,因此FreeRTOS也提供了不同的API函数,具体如下所述
/**
* @brief 动态分配内存创建二值信号量函数
* @param xSemaphore:创建的二值信号量句柄
* @retval None
*/
void vSemaphoreCreateBinary(SemaphoreHandle_t xSemaphore);
/**
* @brief 静态分配内存创建二值信号量函数
* @param pxSemaphoreBuffer:指向一个StaticSemaphore_t类型的变量,该变量将用于保存信号量的状态
* @retval 返回创建成功的信号量句柄,如果返回NULL则表示因为pxSemaphoreBuffer为空无法创建
*/
SemaphoreHandle_t xSemaphoreCreateBinaryStatic(
StaticSemaphore_t *pxSemaphoreBuffer);
/**
* @brief 动态分配内存创建计数信号量函数
* @param uxMaxCount:可以达到的最大计数值
* @param uxInitialCount:创建信号量时分配给信号量的计数值
* @retval 返回创建成功的信号量句柄,如果返回NULL则表示内存不足无法创建
*/
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount);
/**
* @brief 静态分配内存创建计数信号量函数
* @param uxMaxCount:可以达到的最大计数值
* @param uxInitialCount:创建信号量时分配给信号量的计数值
* @param pxSempahoreBuffer:指向StaticSemaphore_t类型的变量,该变量然后用于保存信号量的数据结构体
* @retval 返回创建成功的信号量句柄,如果返回NULL则表示因为pxSemaphoreBuffer为空无法创建
*/
SemaphoreHandle_t xSemaphoreCreateCountingStatic(
UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount,
StaticSemaphore_t pxSempahoreBuffer);
释放信号量
以下两个函数不仅仅可以用于释放二值信号量,还可以用于释放计数信号量和互斥量,具体如下所示
/**
* @brief 释放信号量函数
* @param xSemaphore:要释放的信号量的句柄
* @retval 如果信号量释放成功,则返回pdTRUE;如果发生错误,则返回pdFALSE
*/
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
/**
* @brief 释放信号量的中断安全版本函数
* @param pxHigherPriorityTaskWoken:用于通知应用程序编写者是否应该执行上下文切换
* @retval 如果成功给出信号量,则返回pdTRUE,否则errQUEUE_FULL
*/
BaseType_t xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken);
获取信号量
/**
* @brief 获取信号量函数
* @param xSemaphore:正在获取的信号量的句柄
* @param xTicksToWait:等待信号量变为可用的时间
* @retval 成功获得信号量则返回pdTRUE;如果xTicksToWait过期,信号量不可用,则返回pdFALSE
*/
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);
/**
* @brief 获取信号量的中断安全版本函数
* @param xSemaphore:正在获取的信号量的句柄
* @param pxHigherPriorityTaskWoken:用于通知应用程序编写者是否应该执行上下文切换
* @retval 成功获取则返回pdTRUE,未成功获取则返回pdFALSE
*/
BaseType_t xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore,
signed BaseType_t *pxHigherPriorityTaskWoken);
删除信号量
/**
* @brief 删除信号量,包括互斥锁型信号量和递归信号量
* @param xSemaphore:被删除的信号量的句柄
* @retval None
*/
void vSemaphoreDelete(SemaphoreHandle_t xSemaphore);
获取信号量计数
/**
* @brief 获取信号量计数
* @param xSemaphore:正在查询的信号量的句柄
* @retval 如果信号量是计数信号量,则返回信号量的当前计数值。如果信号量是二进制信号量,则当信号量可用时,返回1,当信号量不可用时,返回 0
*/
UBaseType_t uxSemaphoreGetCount(SemaphoreHandle_t xSemaphore);
互斥量
优先级翻转简介
使用二值信号量用于进程间同步时可能会出现优先级翻转的问题,什么是“优先级翻转”问题呢?
- 在 t1 时刻,低优先级的任务 TaskLP 切入运行状态,并且获取到了一个二值信号量 Binary Semaphores
- 在 t2 时刻,高优先级的任务 TaskHP 请求获取二值信号量 Binary Semaphores ,但是由于 TaskLP 还未释放该二值信号量,所以在 t3 时刻,任务 TaskHP 进入阻塞状态等待二值信号量被释放
- 在 t4 时刻,中等优先级的任务 TaskMP 进入就绪状态,由于不需要获取二值信号量,因此抢占低优先级任务任务 TaskLP 切入运行状态
- 在 t5 时刻,任务 TaskMP 运行结束,任务 TaskLP 再次切入运行状态
- 在 t6 时刻,任务 TaskLP 运行结束,释放二值信号量 Binary Semaphores,此时任务 TaskHP 从等待二值信号量的阻塞状态切入运行状态
- 在t7时刻,任务 TaskHP 运行结束
根据上述流程读者可以发现一个问题,即在 t4 时刻中等优先级的任务 TaskMP 先于高优先级的任务 TaskHP 抢占了处理器,这破坏了 FreeRTOS 基于优先级抢占式执行的原则,我们将这种情况称为优先级翻转问题,上述描述的任务运行过程具体时刻流程图如下图所示:
优先级继承和互斥信号量
为了解决使用二值信号量可能会出现的优先级翻转问题,对二值信号量做了改进,增加了一种名为 “优先级继承” 的机制,改进后的实例称为了互斥量,注意虽然互斥量可以减缓优先级翻转问题的出现,但是并不能完全杜绝
接下来我们来通过例子介绍什么是优先级继承?
仍然考虑由 “优先级翻转问题” 小节中提出的任务运行过程的例子,具体流程如下所述,读者可以细心理解其中的不同之处
- 在 t1 时刻,低优先级的任务 TaskLP 切入运行状态,并且获取到了一个互斥量 Mutexes
- 在 t2 时刻,高优先级的任务 TaskHP 请求获取互斥量 Mutexes ,但是由于 TaskLP 还未释放该互斥量,所以在 t3 时刻,任务 TaskHP 进入阻塞状态等待互斥量被释放,但是与二值信号量不同的是,此时 FreeRTOS 将任务 TaskLP 的优先级临时提高到与任务 TaskHP 一致的优先级,也即高优先级
- 在 t4 时刻,中等优先级的任务 TaskMP 进入就绪状态发生任务调度,但是由于任务 TaskLP 此时优先级被提高到了高优先级,因此任务 TaskMP 仍然保持就绪状态等待优先级较高的任务执行完毕
- 在 t5 时刻,任务 TaskLP 执行完毕释放互斥量 Mutexes,此时任务 TaskHP 抢占处理器切入运行状态,并恢复任务 TaskLP 原来的优先级
- 在 t6 时刻,任务 TaskHP 执行完毕,此时轮到任务 TaskMP 执行
- 在 t7 时刻,任务 TaskMP 运行结束
根据互斥量的上述任务流程读者可以发现与二值信号量的不同之处,上述描述的任务运行过程具体时刻流程图如下图所示
互斥信号量
互斥量/互斥锁是一种特殊类型的二进制信号量,用于控制对在两个或多个任务之间共享资源的访问
互斥锁可以被视为一个与正在共享的资源相关联的令牌,对于合法访问资源的任务,它必须首先成功 “获取” 令牌,成为资源的持有者,当持有者完成对资源的访问之后,其需要 ”归还” 令牌,只有 “归还” 令牌之后,该令牌才可以再次被其他任务所 “获取” ,这样保证了互斥的对共享资源的访问,上述机制如下图所示
死锁现象
“死锁” 是使用互斥锁进行互斥的另一个潜在陷阱,当两个任务因为都在等待对方占用的资源而无法继续进行时,就会发生死锁,考虑如下所述的情况
- 任务 A 执行并成功获取互斥量 X
- 任务 A 被任务 B 抢占
- 任务 B 在尝试获取互斥量 X 之前成功获取互斥量 Y,但互斥量 X 由任务 A 持有,因此对任务 B 不可用,任务 B 选择进入阻塞状态等待互斥量 X 被释放
- 任务 A 继续执行,它尝试获取互斥量 Y,但互斥量 Y 由任务 B 持有,所以对于任务 A 来说是不可用的,任务 A 选择进入阻塞状态等待待释放的互斥量 Y
经过上述的这样一个过程,读者可以发现任务 A 在等待任务 B 释放互斥量 Y ,而任务 B 在等待任务 A 释放互斥量 X ,两个任务都在阻塞状态无法执行,从而导致 ”死锁“ 现象的发生,与优先级翻转一样,避免 “死锁” 的最佳方法是在设计时考虑其潜在影响,并设计系统以确保不会发生死锁
递归互斥量
任务也有可能与自身发生死锁,如果任务尝试多次获取相同的互斥体而不首先返回互斥体,就会发生这种情况,考虑以下设想:
- 任务成功获取互斥锁
- 在持有互斥体的同时,任务调用库函数
- 库函数的实现尝试获取相同的互斥锁,并进入阻塞状态等待互斥锁变得可用
在此场景结束时,任务处于阻塞状态以等待互斥体返回,但任务已经是互斥体持有者。 由于任务处于阻塞状态等待自身,因此发生了死锁
通过使用递归互斥体代替标准互斥体可以避免这种类型的死锁,同一任务可以多次 “获取” 递归互斥锁,并且只有在每次 “获取” 递归互斥锁之后都调用一次 “释放” 递归互斥锁,才会返回该互斥锁
因此递归互斥量可以视为特殊的互斥量,一个互斥量被一个任务获取之后就不能再次获取,其他任务想要获取该互斥量必须等待这个任务释放该互斥连,但是递归互斥量可以被一个任务重复获取多次,当然每次获取必须与一次释放配对使用
注意不管是互斥量,还是递归互斥量均存在优先级继承机制,但是由于 ISR 并不是任务,因此互斥量和递归互斥量不能在中断中使用
互斥信号量相关API函数
使用互斥信号量:首先将宏configUSE_MUTEXES
置1
注意:创建互斥信号量时,会主动释放一次信号量
创建互斥量
互斥量在使用之前必须先创建,因为互斥量分为互斥量和递归互斥量两种,所以 FreeRTOS 也提供了不同的 API 函数,具体如下所述
/**
* @brief 动态分配内存创建互斥信号量函数
* @retval 创建互斥信号量的句柄
*/
SemaphoreHandle_t xSemaphoreCreateMutex(void);
/**
* @brief 静态分配内存创建互斥信号量函数
* @param pxMutexBuffer:指向StaticSemaphore_t类型的变量,该变量将用于保存互斥锁型信号量的状态
* @retval 返回成功创建后的互斥锁的句柄,如果返回NULL则表示内存不足创建失败
*/
SemaphoreHandle_t xSemaphoreCreateMutexStatic(StaticSemaphore_t *pxMutexBuffer);
/**
* @brief 动态分配内存创建递归互斥信号量函数
* @retval 创建递归互斥信号量的句柄,如果返回NULL则表示内存不足创建失败
*/
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex(void);
/**
* @brief 动态分配内存创建二值信号量函数
* @param pxMutexBuffer:指向StaticSemaphore_t类型的变量,该变量将用于保存互斥锁型信号量的状态
*/
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex(
StaticSemaphore_t pxMutexBuffer);
获取互斥量
获取互斥量直接使用获取信号量的函数即可,但对于递归互斥量需要专门的获取函数,具体如下所述
/**
* @brief 获取信号量函数
* @param xSemaphore:正在获取的信号量的句柄
* @param xTicksToWait:等待信号量变为可用的时间
* @retval 成功获取信号量则返回pdTRUE, xTicksToWait过期,信号量不可用,则返回pdFALSE
*/
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);
/**
* @brief 获取递归互斥量
* @param xMutex:正在获得的互斥锁的句柄
* @param xTicksToWait:等待信号量变为可用的时间
* @retval 成功获取信号量则返回pdTRUE, xTicksToWait过期,信号量不可用,则返回pdFALSE
*/
BaseType_t xSemaphoreTakeRecursive(SemaphoreHandle_t xMutex,
TickType_t xTicksToWait);
释放互斥量
释放互斥量直接使用释放信号量的函数即可,但对于递归互斥量需要专门的释放函数,具体如下所述
/**
* @brief 释放信号量函数
* @param xSemaphore:要释放的信号量的句柄
* @retval 成功释放信号量则返回pdTRUE, 若发生错误,则返回pdFALSE
*/
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
/**
* @brief 释放递归互斥量
* @param xMutex:正在释放或“给出”的互斥锁的句柄
* @retval 成功释放递归互斥量后返回pdTRUE
*/
BaseType_t xSemaphoreGiveRecursive(SemaphoreHandle_t xMutex);
删除互斥量
直接使用信号量的删除函数即可,具体如下所述
/**
* @brief 获取信号量函数
* @param xSemaphore:要删除的信号量的句柄
* @retval None
*/
void vSemaphoreDelete(SemaphoreHandle_t xSemaphore);