信号量常用于控制对共享资源的访问和任务同步。
其中控制共享资源可以从停车场的例子去理解。比如现在这个停车场最大容量为100。这个100就是共享资源。假如要把车停进去这个停车场,就需要查看当前停车场中的数量。当前的停车数量就是信号量。信号量的增加对应停车场的车开出停车场。信号量减少代表新的车进入了停车场。停车场这个例子使用的就是计数型信号量。
再来看一个例子,比如公共电话的使用只能同时一个人使用。这时候公共电话的状态就只有2种,使用和未使用,如果用电话的这两种作为信号量的话,这个就是二值信号量。
信号量用于控制共享资源访问的场景就是相当于上锁机制,代码只有获得了这个锁的钥匙才能能够执行。换句话来说,就是信号量(Semaphore)是一种广泛用于操作系统和多线程程序设计中的同步机制,用于控制对共享资源的访问。你可以将信号量想象成一个管理共享资源的“看门人”,确保同一时间内,只有有限的线程(或进程)可以访问这些资源。这种机制类似于现实生活中的“上锁”,其中“钥匙”的数量决定了同时能访问资源的线程数量。
信号量可用于任务同步
1:确保任务顺序:可以使用信号量来确保特定的任务或操作按照预定的顺序执行。例如,如果任务B依赖于任务A的结果,你可以在任务A完成时使用signal操作,并在任务B开始时使用wait操作,从而确保任务B在任务A之后执行。
2:条件同步:在某些情况下,你可能希望等待多个条件都满足后才执行某个任务。通过适当地初始化信号量的计数值和在关键点使用wait和signal操作,可以实现这种复杂的同步逻辑。
3:屏障(Barrier):信号量可以用来实现一个同步点,所有的线程或进程必须到达这一点后才能继续执行。这通常用于分阶段执行的场景,其中每个阶段需要所有参与者的结果。
信号量还可以用于中断服务函数,因为中断服务函数中不能放进去太多的代码。所以可以在中断函数中释放信号量,中断函数不做具体的处理,具体的处理过程做成一个任务,这个任务就会获取信号量,如果获取信号量就说明中断发生了。就开始进行对应的处理。
二值信号量
二值信号量常用于互斥访问或者同步,互斥访问这里用python中多线程举例子:
from threading import Semaphore, Thread
# 初始化二值信号量,起始值为1,表示资源可用
mutex = Semaphore(1)
# 共享数据
shared_data = []
# 定义一个操作共享数据的函数
def modify_shared_data(thread_name):
mutex.wait() # 请求访问共享资源
print(f"{thread_name} is modifying shared data.")
shared_data.append(thread_name) # 修改共享数据
print(f"Current shared data: {shared_data}")
mutex.signal() # 释放访问权
# 创建并启动多个线程
threads = [Thread(target=modify_shared_data, args=(f"Thread-{i}",)) for i in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
在这个例子中,每个线程在修改共享数据shared_data之前,必须通过wait()操作获取二值信号量mutex。由于mutex是一个二值信号量,这确保了同一时刻只有一个线程可以修改shared_data。完成修改后,线程通过signal()操作释放信号量,允许其他线程访问共享资源。
用于任务同步就是,同步的目标是协调多个线程或进程的执行顺序,确保它们在正确的时间点上执行特定的操作。二值信号量在这里用于实现线程之间的同步,确保特定的执行顺序或条件被满足。
# 初始化二值信号量,起始值为0,表示任务A尚未完成
sem = Semaphore(0)
def taskA():
# 执行任务A的操作...
print("Task A completed.")
sem.signal() # 通知任务A已完成
def taskB():
sem.wait() # 等待任务A完成
# 执行任务B的操作...
print("Task B started after Task A.")
# 启动任务A和任务B的线程
Thread(target=taskA).start()
Thread(target=taskB).start()
在这个例子中,任务B通过wait()操作等待二值信号量sem,这保证了它只有在任务A调用signal()操作后才会执行。这样,我们就通过二值信号量实现了任务之间的同步。
和队列类似,信号量API函数允许设置一个阻塞时间,阻塞时间是当前任务获取信号量的时候由于信号量无效从而导致任务进入阻塞态的最大时钟节拍数。如果多个任务同时阻塞在同一个信号量上那么优先级最高的哪个任务优先获得信号量,这么当信号量有效的时候高优先级的任务就会解除阻塞状态。
原子文档中举了这样一个例子:
原子文档总结了二值信号量使用的过程:
二值信号量创建函数如下:
函数 | 描述 |
---|---|
vSemaphoreCreateBinary () | 动态创建 二 值 信 号 量 , 这 个 是 老 版 本FreeRTOS 中使用的创建二值信号量的 API 函数。 |
xSemaphoreCreateBinary() | 动态创建二值信号量,新版 FreeRTOS 使用此函数创建二值信号量。 |
xSemaphoreCreateBinaryStatic() | 静态创建二值信号量 。 |
函数 xSemaphoreCreateBinary()
此函数是 vSemaphoreCreateBinary()的新版本,新版本的 FreeRTOS 中统一用此函数来创建二值信号量。使用此函数创建二值信号量的话信号量所需要的 RAM 是由 FreeRTOS 的内存管理部分来动态分配的。此函数创建好的二值信号量默认是空的,也就是说刚创建好的二值信号量使用函数 xSemaphoreTake()是获取不到的,此函数也是个宏,具体创建过程是由函数xQueueGenericCreate()来完成的,函数原型如下:SemaphoreHandle_t xSemaphoreCreateBinary( void )
返回值:
NULL: 二值信号量创建失败。
其他值: 创建成功的二值信号量的句柄。
函数 xSemaphoreCreateBinaryStatic()
此函数也是创建二值信号量的,只不过使用此函数创建二值信号量的话信号量所需要的
RAM 需要由用户来分配,此函数是个宏,具体创建过程是通过函数 xQueueGenericCreateStatic()
来完成的,函数原型如下:
SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t *pxSemaphoreBuffer )
参数:
pxSemaphoreBuffer:此参数指向一个 StaticSemaphore_t 类型的变量,用来保存信号量结构体。
返回值:
NULL: 二值信号量创建失败。
其他值: 创建成功的二值信号量句柄。
由于信号量不需要像队列那样需要的队列项,所以底下这个参数对应的宏定义为0
释放信号量
函数 | 描述 |
---|---|
xSemaphoreGive() | 任务级信号量释放函数 |
xSemaphoreGiveFromISR() | 中断级信号量释放函数。 |
1、函数 xSemaphoreGive()
此函数用于释放二值信号量、计数型信号量或互斥信号量,此函数是一个宏,真正释放信号量的过程是由函数 xQueueGenericSend()来完成的,函数原型如下:BaseType_t xSemaphoreGive( xSemaphore )
参数:
xSemaphore:要释放的信号量句柄。
返回值:
pdPASS: 释放信号量成功。
errQUEUE_FULL: 释放信号量失败。
#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()。
获取信号量
获取信号量也有两个函数,如表所示:
函数 | 描述 |
---|---|
xSemaphoreTake() | 任务级获取信号量函数 |
xSemaphoreTakeFromISR() | 中断级获取信号量函数 |
同释放信号量的 API 函数一样,不管是二值信号量、计数型信号量还是互斥信号量,它们都使用上表中的函数获取信号量。
1、函数 xSemaphoreTake()
此函数用于获取二值信号量、计数型信号量或互斥信号量,此函数是一个宏,真正获取信号量的过程是由函数 xQueueGenericReceive ()来完成的,函数原型如下:
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore,
TickType_t xBlockTime)
xSemaphore:要获取的信号量句柄。
xBlockTime: 阻塞时间。
再来看一下函数 xSemaphoreTake ()的具体内容,此函数在文件 semphr.h 中有如下定义:
#define xSemaphoreTake( xSemaphore, xBlockTime ) \
xQueueGenericReceive( ( QueueHandle_t ) ( xSemaphore ), \
NULL, \
( xBlockTime ), \
pdFALSE ) \
2、函数 xSemaphoreTakeFromISR ()
此函数用于在中断服务函数中获取信号量,此函数用于获取二值信号量和计数型信号量,绝 对 不 能 使 用 此 函 数 来 获 取 互 斥 信 号 量 ! 此 函 数 是 一 个 宏 , 真 正 执 行 的 是 函 数
xQueueReceiveFromISR (),此函数原型如下:
BaseType_t xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore,
BaseType_t * pxHigherPriorityTaskWoken)
返回值:
pdPASS: 获取信号量成功。
pdFALSE: 获取信号量失败。
在中断中获取信号量真正使用的是函数 xQueueReceiveFromISR (),这个函数就是中断级出队函数!当队列不为空的时候就拷贝队列中的数据(用于信号量的时候不需要这一步),然后将队列结构体中的成员变量 uxMessagesWaiting 减一,如果有任务因为入队而阻塞的话就解除阻塞态,当解除阻塞的任务拥有更高优先级的话就将参数 pxHigherPriorityTaskWoken 设置为pdTRUE,最后返回 pdPASS 表示出队成功。如果队列为空的话就直接返回 pdFAIL 表示出队失败!这个函数还是很简单的。
至于为啥要在中断中进行任务切换。在 FreeRTOS 这样的实时操作系统中,中断服务例程(ISR)的执行具有高优先级,意味着它们能够打断大多数正在执行的任务。这是为了确保对紧急事件能够快速响应。然而,这也带来了一个挑战:如何在处理完一个中断后,高效地回到最应该运行的任务上。当中断发生时,当前正在运行的任务会被暂停,以便中断服务例程(ISR)可以执行。如果在 ISR 中释放了一个信号量,有可能会唤醒一个因等待该信号量而被阻塞的任务。如果这个被唤醒的任务的优先级高于当前被中断的任务,那么理论上,我们应该立即切换到这个更高优先级的任务上去执行。这种切换是为了保证系统的实时性,确保高优先级的任务能够尽快执行。
计数型信号量简介
有些资料中也将计数型信号量叫做数值信号量,二值信号量相当于长度为 1 的队列,那么计数型信号量就是长度大于 1 的队列。同二值信号量一样,用户不需要关心队列中存储了什么数据,只需要关心队列是否为空即可。计数型信号量通常用于如下两个场合:
1、事件计数
在这个场合中,每次事件发生的时候就在事件处理函数中释放信号量(增加信号量的计数值),其他任务会获取信号量(信号量计数值减一,信号量值就是队列结构体成员变量uxMessagesWaiting)来处理事件。在这种场合中创建的计数型信号量初始计数值为 0。
2、资源管理
在这个场合中,信号量值代表当前资源的可用数量,比如停车场当前剩余的停车位数量。一个任务要想获得资源的使用权,首先必须获取信号量,信号量获取成功以后信号量值就会减一。当信号量值为 0 的时候说明没有资源了。当一个任务使用完资源以后一定要释放信号量,释放信号量以后信号量值会加一。在这个场合中创建的计数型信号量初始值应该是资源的数量,比如停车场一共有 100 个停车位,那么创建信号量的时候信号量值就应该初始化为 100。
FreeRTOS 提供了两个计数型信号量创建函数,如表 所示:
1、函数 xSemaphoreCreateCounting()
此函数用于创建一个计数型信号量,所需要的内存通过动态内存管理方法分配。此函数本质是一个宏,真正完成信号量创建的是函数 xQueueCreateCountingSemaphore(),此函数原型如下:
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount )
参数:
uxMaxCount: 计数信号量最大计数值,当信号量值等于此值的时候释放信号量就会失败。
uxInitialCount: 计数信号量初始值。
2、函数 xSemaphoreCreateCountingStatic()
此函数也是用来创建计数型信号量的,使用此函数创建计数型信号量的时候所需要的内存需要由用户分配。此函数也是一个宏,真正执行的是函数xQueueCreateCountingSemaphoreStatic(),
函数原型如下:
SemaphoreHandle_t xSemaphoreCreateCountingStatic( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount,
StaticSemaphore_t * pxSemaphoreBuffer )
参数:
uxMaxCount: 计数信号量最大计数值,当信号量值等于此值的时候释放信号量就会失败。
uxInitialCount: 计数信号量初始值。
pxSemaphoreBuffer:指向一个 StaticSemaphore_t 类型的变量,用来保存信号量结构体。
返回值:
NULL: 计数型信号量创建失败。
其他值: 计数型号量创建成功,返回计数型信号量句柄。
计数型信号量的释放和获取与二值信号量相同,