14.2.1 信号量的基础知识

更新于 2026年10月10日 版权声明
14.2.1 信号量的基础知识

在操作系统的运行过程中,任务与任务之间有时需要共享资源。为了保证执行结果的正确性,操作系统必须为这种协作的任务提供一种通信机制,这种机制就是同步与互斥。

FreeRTOS用信号量来实现任务间对共享资源的同步与互斥。FreeRTOS中信号量类型分为二值信号量、计数信号量、互斥信号量和递归互斥信号量。不同的信号量,其应用场景也不同,但有些应用场景是可以互换使用的。

所有的信号量函数的使用方法与队列函数的使用方法相同,只是功能不一样,其步骤如下。

(1)创建信号量,如创建二值信号量vSemaphoreCreateBinary()。

(2)释放信号量,使用任务和中断信号量释放函数xSemaphoreGive()、xSemaphoreGiveFromISR()。

(3)获取信号量,使用任务和中断信号量获取函数xSemaphoreTake()、xSemaphoreTakeFromISR()。

1.二值信号量

二值信号量其实就是一个只有一个队列项的队列,这个特殊的队列要么是满的,要么是空的,只有两个值,所以被称为二值信号量。使用队列的任务和中断不关心队列包含什么信息,只需要知道队列是空还是满。二值信号量通信示意图如图14.7所示。

图示

图14.7 二值信号量通信示意图

创建二值信号量后,计数值为空;TaskB通过xSemaphoreTake()函数获取信号量,因此TaskB无法完成计数值减1,从而被阻塞。当TaskA满足某条件,运行xSemaphoreGive()函数释放信号量,此时计数值加1,表示队列已满;由于信号量已经有效,所以任务TaskB获取信号量成功,任务从阻塞态解除,开始执行相关的处理过程。如此反复,实现TaskA和TaskB两个任务同步运行。

2.计数信号量

计数信号量相当于只有一个队列项的队列,且队列项的值可以计数。同二值信号量一样,用户不需要关心队列中存储了什么数据,只需要关心队列是否为空和满即可。计数信号量通常用于如下两个场景。

(1)事件计数。

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

(2)资源管理。

在资源管理场景中,信号量值代表当前资源的可用数量,如停车场当前剩余的停车位数量。一个任务要想获得资源的使用权,首先必须获取信号量,信号量获取成功以后,信号量值就会减一。当信号量值为0的时候,说明没有资源了。当一个任务使用完资源以后,一定要释放信号量。释放信号量以后,信号量值会加1。在这个场景中创建的计数信号量初始值应该是资源的数量,例如,如停车场一共有10个停车位,那么创建信号量的时候,信号量值就应该初始化为10。计数信号量示意图如图14.8所示。

图示

图14.8 计数信号量示意图

3.互斥信号量

互斥信号量的主要作用是对资源实现互斥访问,它与二值信号量的区别是可以解决优先级翻转问题。在使用二值信号量时会出现优先级翻转,即低优先级任务长时间占用高优先级资源,让高优先级任务无法运行。优先级翻转示意图如图14.9所示。

图示

图14.9 优先级翻转示意图

在图14.9中,TaskL执行到某处需使用信号量处理共享资源,但是它还没有执行完,就被高优先级任务TaskH剥夺了CPU使用权;此时,TaskH也需要马上处理共享资源,由于共享资源上锁而无法执行,被迫释放,TaskL续续执行;此时,它又被优先级更高的TaskM剥夺了CPU使用权,TaskM执行完后再继续执行TaskL剩下的部分,TaskL执行完后释放信号量,此时TaskH才继续执行。这样就出现了高优先级任务始终被低优先级任务剥夺CPU使用权的情况,造成优先级翻转。在可剥夺实时系统中不允许出现这种现象,这样会破坏任务的预期顺序,可能会导致严重的后果。(https://www.daowen.com)

在互斥信号量中,要解决优先级翻转问题,应采用优先级继承的方式,其示意图如图14.10所示。

图示

图14.10 优先级继承示意图

低优先级任务TaskL执行过程中先获得互斥资源,执行一段时间后,任务TaskH抢占了CPU,任务TaskL被挂起,任务TaskH得到执行。

任务TaskH执行时也需要调用互斥资源,但是发现任务TaskL正在访问此资源,此时任务TaskL的优先级会被提升到与TaskH同样的等级,这就是所谓的优先级继承,这样就有效地防止了优先级翻转问题。任务TaskH被挂起,任务TaskL因为有新的优先级所以继续执行。

任务TaskL执行完毕并释放互斥资源后,优先级恢复到原来的水平。由于互斥资源可以使用,任务TaskH获得互斥资源后开始执行。TaskH执行完毕,再执行其他任务,如TaskM。

综上所述,互斥信号量可以有效解决优先级翻转问题。

4.递归互斥信号量

(1)死锁。

在互斥信号量的应用中会出现死锁现象,即多个任务各自获得共享资源不释放,但相互又需要对方的共享资源。死锁示意图如图14.11所示。

设有两个互斥信号量Mutex1和Mutex2,两个任务TaskA、TaskB。

第一步,TaskA获得了互斥信号量Mutex1。

第二步,TaskB获得了互斥信号量Mutex2。

第三步,TaskA还要获得互斥信号量Mutex2才能运行,结果TaskA被阻塞了。

图示

图14.11 死锁示意图

第四步,TaskB还要获得互斥信号量Mutex1才能运行,结果TaskB被阻塞了。

现在TaskA、TaskB都被阻塞,无法释放它们持有的互斥信号量,这时就发生了死锁现象。要解决这一问题,就需要用到递归互斥信号量。

(2)递归互斥信号量的使用。

递归互斥信号量是一个特殊的互斥信号量。由前文可知,已经获取互斥信号量的任务就不能再次获取这个互斥信号量,但是递归互斥信号量不同,已经获取递归互斥信号量的任务可以再次获取这个递归互斥信号量,且次数不限。

任务使用函数xSemaphoreTakeRecursive()成功地获取了多少次递归互斥信号量,就得使用函数xSemaphoreGiveRecursive()释放多少次递归互斥信号量。比如某个任务成功地获取了3次递归互斥信号量,那么这个任务也得释放3次递归互斥信号量。

递归互斥信号量也有优先级继承的机制,当任务使用完递归互斥信号量以后,一定要记得释放。同互斥信号量一样,递归互斥信号量不能用在中断服务函数中。这是因为优先级继承的存在就限定了递归互斥信号量只能用在任务中,中断服务函数中没有优先级。另外,在中断服务函数中也不能因为要等待互斥信号量,而设置阻塞时间来进入阻塞态。

若要使用递归互斥信号量,宏configUSE_RECURSIVE_MUTEXES必须为1。

↑上一章 ↓下一章
关注公众号获取验证码
复制内容需要验证码(7.99元/天)