FreeRTOS 15:FreeRTOS信号量
信号量基本概念
信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务之间同步或临界资源的互斥访问, 常用于协助一组相互竞争的任务来访问临界资源。在多任务系统中,各任务之间需要同步或互斥实现临界资源的保护,信号量功能可以为用户提供这方面的支持。抽象的来讲,信号量是一个非负整数,所有获取它的任务都会将该整数减一(获取它当然是为了使用资源),当该整数值为零时,所有试图获取它的任务都将处于阻塞状态。通常一个信号量的计数值用于对应有效的资源数,表示剩下的可被占用的互斥资源数。其值的含义分两种情况:
-
0:表示没有积累下来的释放信号量操作,且有可能有在此信号量上阻塞的任务。
-
正值,表示有一个或多个释放信号量操作。
二值信号量
二值信号量既可以用于临界资源访问也可以用于同步功能。
二值信号量和互斥信号量(以下使用互斥量表示互斥信号量) 非常相似,但是有一些细微差别:互斥量有优先级继承机制, 二值信号量则没有这个机制。这使得二值信号量更偏向应用于同步功能(任务与任务间的同步或任务和中断间同步), 而互斥量更偏向应用于临界资源的访问。
用作同步时,信号量在创建后应被置为空,任务 1 获取信号量而进入阻塞,任务 2 在某种条件发生后,释放信号量,于是任务 1 获得信号量得以进入就绪态,如果任务 1 的优先级是最高的,那么就会立即切换任务,从而达到了两个任务间的同步。同样的,在中断服务函数中释放信号量, 任务 1 也会得到信号量,从而达到任务与中断间的同步。
还记得我们经常说的中断要快进快出吗,在裸机开发中我们经常是在中断中做一个标记,然后在退出的时候进行轮询处理,这个就是类似我们使用信号量进行同步的,当标记发生了,我们再做其他事情。在 FreeRTOS 中我们用信号量用于同步,任务与任务的同步,中断与任务的同步,可以大大提高效率。
可以将二值信号量看作只有一个消息的队列, 因此这个队列只能为空或满(因此称为二值),我们在运用的时候只需要知道队列中是否有消息即可,而无需关注消息是什么。
计数信号量
二进制信号量可以被认为是长度为 1 的队列,而计数信号量则可以被认为长度大于 1的队列,信号量使用者依然不必关心存储在队列中的消息,只需关心队列是否有消息即可。顾名思义,计数信号量肯定是用于计数的,在实际的使用中,我们常将计数信号量用于事件计数与资源管理。每当某个事件发生时,任务或者中断将释放一个信号量(信号量计数值加 1),当处理被事件时(一般在任务中处理),处理任务会取走该信号量(信号量计数值减 1),信号量的计数值则表示还有多少个事件没被处理。此外,系统还有很多资源,我们也可以使用计数信号量进行资源管理,信号量的计数值表示系统中可用的资源数目,任务必须先获取到信号量才能获取资源访问权,当信号量的计数值为零时表示系统没有可用的资源,但是要注意,在使用完资源的时候必须归还信号量,否则当计数值为 0的时候任务就无法访问该资源了。
计数型信号量允许多个任务对其进行操作,但限制了任务的数量。比如有一个停车场,里面只有 100 个车位,那么能停的车只有 100 辆,也相当于我们的信号量有 100 个,假如一开始停车场的车位还有 100 个,那么每进去一辆车就要消耗一个停车位,车位的数量就要减一,对应的,我们的信号量在使用之后也需要减一,当停车场停满了 100 辆车的时候,此时的停车位为 0,再来的车就不能停进去了,否则将造成事故,也相当于我们的信号量为 0,后面的任务对这个停车场资源的访问也无法进行,当有车从停车场离开的时候,车位又空余出来了,那么,后面的车就能停进去了,我们信号量的操作也是一样的,当我们释放了这个资源,后面的任务才能对这个资源进行访问。
二进制信号量跟计数型的唯一差别,就是计数值的最大值被限定为 1。
信号量的计数值都有限制:限定了最大值。如果最大值被限定为 1,那么它就是二进
制信号量;如果最大值不是 1,它就是计数型信号量。
互斥信号量
互斥信号量其实是特殊的二值信号量,由于其特有的优先级继承机制从而使它更适用于简单互锁,也就是保护临界资源(什么是优先级继承在后续相信讲解) 。
用作互斥时,信号量创建后可用信号量个数应该是满的, 任务在需要使用临界资源时,(临界资源是指任何时刻只能被一个任务访问的资源) ,先获取互斥信号量,使其变空,这样其他任务需要使用临界资源时就会因为无法获取信号量而进入阻塞,从而保证了临界资源的安全。
在操作系统中,我们使用信号量的很多时候是为了给临界资源建立一个标志,信号量表示了该临界资源被占用情况。这样,当一个任务在访问临界资源的时候,就会先对这个资源信息进行查询,从而在了解资源被占用的情况之后,再做处理,从而使得临界资源得到有效的保护。
递归信号量
递归信号量,见文知义,递归嘛,就是可以重复获取调用的,本来按照信号量的特性,每获取一次可用信号量个数就会减少一个,但是递归则然, 对于已经获取递归互斥量的 任务可以重复获取该递归互斥量, 该任务拥有递归信号量的所有权。 任务成功获取几次递归互斥量, 就要返还几次,在此之前递归互斥量都处于无效状态, 其他任务无法获取, 只有持有递归信号量的任务才能获取与释放。
信号量数据结构
信号量 API 函数实际上都是宏,它使用现有的队列机制, 这些宏定义在 semphr.h 文件
中, 如果使用信号量或者互斥量,需要包含 semphr.h 头文件。 所以 FreeRTOS 的信号量控制块结构体与消息队列结构体是一模一样的, 只不过结构体中某些成员变量代表的含义不一样而已 。
typedef struct QueueDefinition /* 保留旧的命名约定以防止破坏内核感知调试器。 */
{int8_t * pcHead; /*< 指向队列存储区域的起始位置。 */int8_t * pcWriteTo; /*< 指向存储区域中的下一个空闲位置。 */union{QueuePointers_t xQueue; /*< 当此结构用作队列时所需的数据。 */SemaphoreData_t xSemaphore; /*< 当此结构用作信号量时所需的数据。 */} u;List_t xTasksWaitingToSend; /*< 等待向此队列发送数据的任务列表。按优先级顺序存储。 */List_t xTasksWaitingToReceive; /*< 等待从队列读取数据的任务列表。按优先级顺序存储。 */volatile UBaseType_t uxMessagesWaiting; /*< 有效信号量个数。 */UBaseType_t uxLength; /*< 最大的信号量可用个数。 */UBaseType_t uxItemSize; /*< 0 */volatile int8_t cRxLock; /*< 存储在队列锁定期间从队列接收(移除)的项目数量。当队列未锁定时设置为queueUNLOCKED。 */volatile int8_t cTxLock; /*< 存储在队列锁定期间向队列传输(添加)的项目数量。当队列未锁定时设置为queueUNLOCKED。 */#if ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )uint8_t ucStaticallyAllocated; /*< 如果队列使用的内存是静态分配的,则设置为pdTRUE,以确保不会尝试释放内存。 */#endif#if ( configUSE_QUEUE_SETS == 1 )struct QueueDefinition * pxQueueSetContainer;#endif#if ( configUSE_TRACE_FACILITY == 1 )UBaseType_t uxQueueNumber;uint8_t ucQueueType;#endif
} xQUEUE;/* 上面保留了旧的 xQUEUE 名称,然后在此处 typedef 为新的 Queue_t 名称,以便使用较旧的内核感知调试器。 */
typedef xQUEUE Queue_t;
如果控制块结构体是用于消息队列: uxMessagesWaiting 用来记录当前消息队列的消息个数; 如果控制块结构体被用于信号量的时候, 这个值就表示有效信号量个数,有以下两种情况:
-
如果信号量是二值信号量、互斥信号量,这个值是 1 则表示有可用信号量,如果
是 0 则表示没有可用信号量。 -
如果是计数信号量,这个值表示可用的信号量个数,在创建计数信号量的时候会
被初始化一个可用信号量个数 uxInitialCount,最大不允许超过创建信号量的初始
值 uxMaxCount。
如果控制块结构体是用于消息队列: uxLength 表示队列的长度,也就是能存放多少消息; 如果控制块结构体被用于信号量的时候, uxLength 表示最大的信号量可用个数, 会有以下两种情况:
-
如果信号量是二值信号量、互斥信号量, uxLength 最大为 1,因为信号量要么是
有效的,要么是无效的。 -
如果是计数信号量,这个值表示最大的信号量个数,在创建计数信号量的时候将
由用户指定这个值 uxMaxCount。
如果控制块结构体是用于消息队列: uxItemSize 表示单个消息的大
小; 如果控制块结构体被用于信号量的时候,则无需存储空间,为 0 即可。
FreeRTOS 基于队列实现了互斥信号量和递归互斥信号量功能,在队列的结构体中,就包含了一个联合体 u,当队列结构体用作队列时,使用联合体 u 中的 xQueue,其数据类 型为 QueuePointers_t,在 queue.c 文件中有定义,具体的代码如下QueuePointers_t所示;而当队列结构体用于互斥信号量和递归互斥信号量时,则是使用联合体 u 中的 xSemaphore,其数据类型为 SemaphoreData_t,在 queue.c 文件中有定义,具体的代码如下SemaphoreData_t所示:
typedef struct QueuePointers
{int8_t * pcTail; /*< 指向队列存储区域末尾的字节。分配的字节数比存储队列项目所需的多一个,用作标记。 */int8_t * pcReadFrom; /*< 当此结构用作队列时,指向最后一个从队列中读取项目的地址。 */
} QueuePointers_t;typedef struct SemaphoreData
{TaskHandle_t xMutexHolder; /*< 持有互斥锁的任务句柄。 */UBaseType_t uxRecursiveCallCount; /*< 当此结构用作互斥锁时,维护递归互斥锁被递归“获取”的次数。 */
} SemaphoreData_t;
-
QueuePointers 结构体:
-
pcTail
: 指向队列存储区域末尾的字节,用于标记队列的结束位置。 -
pcReadFrom
: 指向最后一个从队列中读取项目的地址,用于记录读取位置。
-
-
SemaphoreData 结构体:
-
xMutexHolder
: 持有互斥锁的任务句柄,用于标识当前持有互斥锁的任务。 -
uxRecursiveCallCount
: 维护递归互斥锁被递归“获取”的次数,用于支持递归锁功能。
-