信号量 (Semaphore)
是一种实现任务间通信的机制,可以实现任务之间同步或临界资源的互斥访问,常用于协助一组相互竞争的任务来访问临界资源。
在多任务系统中,各任务之间需要同步或互斥实现临界资源的保护,信号量功能可以为用户提供这方面的支持。
抽象的来讲,信号量是一个非负整数,所有获取它的任务都会将该整数减 1
,当该整数值为零时,所有试图获取它的任务都将处于阻塞状态。
通常一个信号量的计数值用于对应有效的资源数,表示剩下的可被占用的互斥资源数。其值的含义分两种情况:
二值信号量既可以用于临界资源访问也可以用于同步功能。
二值信号量和互斥信号量(以下使用互斥量表示互斥信号量)非常相似,但是有一些细微差别:互斥量有优先级继承机制,二值信号量则没有这个机制。这使得二值信号量更偏向应用于同步功能(任务与任务间的同步或任务和中断间同步),而互斥量更偏向应用于临界资源的访问。
用作同步时,信号量在创建后应被置为空,任务 1
获取信号量而进入阻塞,任务 2
在某种条件发生后,释放信号量,于是任务 1
获得信号量得以进入就绪态,如果任务 1
的优先级是最高的,那么就会立即切换任务,从而达到了两个任务间的同步。同样的,在中断服务函数中释放信号量,任务 1
也会得到信号量,从而达到任务与中断间的同步。
我们知道中断要快进快出,因此在裸机开发中我们经常是在中断中做一个标
记,然后在退出的时候进行轮询处理,这个就是类似我们使用信号量进行同步的,当标记发生了,我们再做其他事情。在 FreeRTOS
中我们用信号量用于同步,任务与任务的同步、中断与任务的同步,可以大大提高效率。
可以将二值信号量看作只有一个消息的队列,因此这个队列只能为空或满(因此称为二值),我们在运用的时候只需要知道队列中是否有消息即可,而无需关注消息是什么。
二进制信号量可以被认为是长度为 1
的队列,而计数信号量则可以被认为长度大于 1
的队列,信号量使用者依然不必关心存储在队列中的消息,只需关心队列是否有消息即可。
顾名思义,计数信号量肯定是用于计数的,在实际的使用中,我们常将计数信号量用于事件计数与资源管理。每当某个事件发生时,任务或者中断将释放一个信号量(信号量计数值加 1
),当处理事件时(一般在任务中处理),处理任务时取走该信号量(信号量计数值减 1
),信号量的计数值则表示还有多少个事件没被处理。此外,系统还有很多资源,我们也可以使用计数信号量进行资源管理,信号量的计数值表示系统中可用的资源数目,任务必须先获取到信号量才能获取资源访问权,当信号量的计数值为零时表示系统没有可用的资源,但是要注意,在使用完资源的时候必须归还信号量,否则当计数值为 0
的时候任务就无法访问该资源了。
计数型信号量允许多个任务对其进行操作,但限制了任务的数量。比如有一个停车场,里面只有 100
个车位,那么能停的车只有 100
辆,也相当于我们的信号量有 100
个,假如一开始停车场的车位还有 100
个,那么每进去一辆车就要消耗一个停车位,车位的数量就要减一,对应的,我们的信号量在使用之后也需要减一,当停车场停满了100 辆车的时候,此时的停车位为 0
,再来的车就不能停进去了,否则将造成事故,也相当于我们的信号量为 0
,后面的任务对这个停车场资源的访问也无法进行,当有车从停车场离开的时候,车位又空余出来了,那么,后面的车就能停进去了,我们信号量的操作也是一样的,当我们释放了这个资源,后面的任务才能对这个资源进行访问。
互斥信号量其实是特殊的二值信号量,由于其特有的优先级继承机制从而使它更适用于简单互锁,也就是保护临界资源。
用作互斥时,信号量创建后可用信号量个数应该是满的,任务在需要使用临界资源时,(临界资源是指任何时刻只能被一个任务访问的资源),先获取互斥信号量,使其变空,这样其他任务需要使用临界资源时就会因为无法获取信号量而进入阻塞,从而保证了临界资源的安全。
在操作系统中,我们使用信号量的很多时候是为了给临界资源建立一个标志,信号量表示了该临界资源被占用情况。这样,当一个任务在访问临界资源的时候,就会先对这个资源信息进行查询,从而在了解资源被占用的情况之后,再做处理,从而使得临界资源得到有效的保护。
递归信号量,见文知义,递归嘛,就是可以重复获取调用的,本来按照信号量的特性,每获取一次可用信号量个数就会减少一个,但是递归则不然,对于已经获取递归互斥量的任务可以重复获取该递归互斥量,该任务拥有递归信号量的所有权。任务成功获取几次递归互斥量,就要返还几次,在此之前递归互斥量都处于无效状态,其他任务无法获取,只有持有递归信号量的任务才能获取与释放。
创建信号量时,系统会为创建的信号量对象分配内存,并把可用信号量初始化为用户自定义的个数, 二值信号量的最大可用信号量个数为 1
。
任何任务都可以从创建的二值信号量资源中获取一个二值信号量,获取成功则返回正确,否则任务会根据用户指定的阻塞超时时间来等待其它任务/中断释放信号量。在等待这段时间,系统将任务变成阻塞态,任务将被挂到该信号量的阻塞等待列表中。
在二值信号量无效的时候,假如此时有任务获取该信号量的话,那么任务将进入阻塞状态。
假如某个时间中断/任务释放了信号量,其过程如下图所示。
由于获取无效信号量而进入阻塞态的任务将获得信号量并且恢复为就绪态,二值信号量运作机制如下图所示。
计数信号量可以用于资源管理,允许多个任务获取信号量访问共享资源,但会限制任务的最大数目。访问的任务数达到可支持的最大数目时,会阻塞其他试图获取该信号量的任务,直到有任务释放了信号量。这就是计数型信号量的运作机制,虽然计数信号量允许多个任务访问同一个资源,但是也有限定,比如某个资源限定只能有3 个任务访问,那么第4 个任务访问的时候,会因为获取不到信号量而进入阻塞,等到有任务(比如任务1)释放掉该资源的时候,第4 个任务才能获取到信号量从而进行资源的访问,其运作的机制如下图所示。
信号量 API
函数实际上都是宏,它使用现有的队列机制,这些宏定义在 semphr.h
文件中,如果使用信号量或者互斥量,需要包含 semphr.h
头文件。所以 FreeRTOS
的信号量控制块结构体与消息队列结构体是一模一样的,只不过结构体中某些成员变量代表的含义不一样而已。
typedef struct QueueDefinition
{
int8_t *pcHead;
int8_t *pcTail;
int8_t *pcWriteTo;
union
{
int8_t *pcReadFrom;
UBaseType_t uxRecursiveCallCount;
} u;
List_t xTasksWaitingToSend;
List_t xTasksWaitingToReceive;
volatile UBaseType_t uxMessagesWaiting;
UBaseType_t uxLength;
UBaseType_t uxItemSize;
volatile int8_t cRxLock;
volatile int8_t cTxLock;
#if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
uint8_t ucStaticallyAllocated;
#endif
#if ( configUSE_QUEUE_SETS == 1 )
struct QueueDefinition *pxQueueSetContainer;
#endif
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxQueueNumber;
uint8_t ucQueueType;
#endif
} xQUEUE;
typedef xQUEUE Queue_t;
volatile UBaseType_t uxMessagesWaiting;
:如果控制块结构体是用于消息队列: uxMessagesWaiting
用来记录当前消息队列的消息个数;如果控制块结构体被用于信号量的时候,这个值就表示有效信号量个数,有以下两种情况:
如果信号量是二值信号量、互斥信号量,这个值是 1
则表示有可用信号量,如果是 0
则表示没有可用信号量。
如果是计数信号量,这个值表示可用的信号量个数,在创建计数信号量的时候会被初始化一个可用信号量个数 uxInitialCount
,最大不允许超过创建信号量的初始值 uxMaxCount
。
UBaseType_t uxLength;
:如果控制块结构体是用于消息队列:uxLength
表示队列的长度,也就是能存放多少消息;如果控制块结构体被用于信号量的时候,·uxLength· 表示最大的信号量可用个数,会有以下两种情况:
uxLength
最大为 1
,因为信号量要么是有效的,要么是无效的。uxMaxCount
。UBaseType_t uxItemSize;
:如果控制块结构体是用于消息队列:uxItemSize
表示单个消息的大小;如果控制块结构体被用于信号量的时候,则无需存储空间,为 0
即可。