信号量

发布于 2023-08-11  743 次阅读


内容纲要

信号量笔记

信号量简介

​ 信号量是一种解决同步问题的机制,可以实现对共享资源的有序访问。其中,“同步”指的 是任务间的同步,即信号量可以使得一个任务等待另一个任务完成某件事情后,才继续执行; 而“有序访问”指的是对被多任务或中断访问的共享资源(如全局变量)的管理,当一个任务 在访问(读取或写入)一个共享资源时,信号量可以防止其他任务或中断在这期间访问(读取 或写入)这个共享资源。

​ 举一个例子,假设某个停车场有 100 个停车位(共享资源),这个 100 个停车位对所有人 (访问共享资源的任务或中断)开放。如果有一个人要在这个停车场停车,那么就需要先判断 这个停车场是否还有空车位(判断信号量是否有资源),如果此时停车场正好有空车位(信号量 有资源),那么就可以直接将车开入空车位进行停车(获取信号量成功),如果此时停车场已经 没有空车位了(信号量没有资源),那么这个人可以选择不停车(获取信号量失败),也可以选 择等待(任务阻塞)其他人将车开出停车场(释放信号量资源),让后再将车停入空车位。

​ 在上面的这个例子中,空车位的数量相当于信号量的资源数,获取信号量相当于占用了空 车位,而释放信号量就相当于让出了占用的空车位。信号量用于管理共享资源的场景相当于对 共享资源上了个锁,只有任务成功获取到了锁的钥匙,才能够访问这个共享资源,访问完共享 资源后还得归还钥匙,当然钥匙可以不只一把,即信号量可以有多个资源。

二值信号量

二值信号量简介

​ 前面说过,信号量是基于队列实现的,二值信号量也不例外,二值信号量实际上就是一个 队列长度为 1 的队列,在这种情况下,队列就只有空和满两种情况,这不就是二值情况吗?二 值信号量通常用于互斥访问或任务同步,与互斥信号量比较类似,但是二值信号量有可能会导致优先级翻转的问题。优先级翻转问题指的是,当一个高优先级任务因获取一个被低优先级任 务获取而处于没有资源状态的二值信号量时,这个高优先级的任务将被阻塞,直到低优先级的 任务释放二值信号量,而在这之前,如果有一个优先级介于这个高优先级任务和低优先级任务之间的任务就绪,那么这个中等优先级的任务就会抢占低优先级任务的运行,这么一来,这三 个任务中优先级最高的任务反而要最后才运行,这就是二值信号量带来的优先级翻转问题,用户在实际开发中要注意这种问题。 和队列一样,在获取二值信号量的时候,允许设置一个阻塞超时时间,阻塞超时时间是当任务获取二值信号量时,由于二值信号量处于没有资源的状态,而导致任务进入阻塞状态的最大系统时钟节拍数。如果多个任务同时因获取同一个处于没有资源状态的二值信号量而被阻塞, 那么在二值信号量有资源的时候,这些阻塞任务中优先级高的任务将优先获得二值信号量的资源并解除阻塞。

二值信号量相关API函数

函数 xSemaphoreCreateBinary()

​ 此函数用于使用动态方式创建二值信号量,创建二值信号量所需的内存,由 FreeRTOS 从 FreeRTOS 管理的堆中进行分配。该函数实际上是一个宏定义,在 semphr.h 文件中有定义,具体的代码如下所示:

#define xSemaphoreCreateBinary() \
 xQueueGenericCreate( ( UBaseType_t ) 1, \
 semSEMAPHORE_QUEUE_ITEM_LENGTH, \
 queueQUEUE_TYPE_BINARY_SEMAPHORE)

​ 从上面的代码中可以看出,函数 xSemaphoreCreateBinary() 实 际 上 是 调 用 了 函 数 xQueueGenericCreate()创建了一个队列长度为 1 且队列项目大小为信号量队列项目大小的二值 信号量类型队列。

函数 xSemaphoreCreateBinaryStatic()

​ 此函数用于使用静态方式创建二值信号量,创建二值信号量所需的内存,需要由用户手动 分配并提供。该函数实际上是一个宏定义,在 semphr.h 文件中有定义,具体的代码如下所示:

#define xSemaphoreCreateBinaryStatic(pxStaticSemaphore) \
 xQueueGenericCreateStatic( ( UBaseType_t ) 1, \
 semSEMAPHORE_QUEUE_ITEM_LENGTH, \
 NULL, \
 pxStaticSemaphore, \
 queueQUEUE_TYPE_BINARY_SEMAPHORE)

​ 从 上 面 的 代 码 中 可 以 看 出 , 函 数 xSemaphoreCreateStatic() 实 际 上 是 调 用 了 函 数 xQueueGenericCreateStatic()创建了一个队列长度为 1 且队列项目大小为信号量队列项目大小的 二值信号量类型队列。

函数 xSemaphoreTake()

​ 此函数用于获取信号量,如果信号量处于没有资源的状态,那么此函数可以选择将任务进 行阻塞,如果成功获取了信号量,那信号量的资源数将会减 1。该函数实际上是一个宏定义, 在 semphr.h 文件中有定义,具体的代码如下所示:

#define xSemaphoreTake( xSemaphore, \
 xBlockTime) \
 xQueueSemaphoreTake( ( xSemaphore ), \
 ( xBlockTime ))

​ 从 上 面 的 代 码 中 可 以 看 出 , 函 数 xSemaphoreTake() 实 际 上 是 调 用 了 函 数 xQueueSemaphoreTake()来获取信号量,函数 xQueueSemaphoreTake()在 queue.c 文件中有定义。

函数 xSemaphoreTakeFromISR()

​ 此函数用于在中断中获取信号量。该函数实际上是一个宏定义,在 semphr.h 文件中有定义, 具体的代码如下所示:

#define xSemaphoreTakeFromISR( xSemaphore, \
 pxHigherPriorityTaskWoken) \
 xQueueReceiveFromISR( ( QueueHandle_t ) ( xSemaphore ), \
 NULL, \
 ( pxHigherPriorityTaskWoken ))

​ 从 上 面 的 代 码 中 可 以 看 出 , 函 数 xSemaphoreTakeFromISR() 实 际 上 是 调 用 了 函 数 xQueueReceiveFromISR()来获取信号量。要特别注意的是,函数 xSemaphoreTakeFromISR()于函 数 xSemaphoreTake()不同,函数 xSemaphoreTakeFromISR()只能用于获取二值信号量和计数型信 号量,而不能用于获取互斥信号量。

函数 xSemaphoreGive()

​ 此函数用于释放信号量,如果信号量处于资源满的状态,那么此函数可续选择将任务进行 阻塞,如果成功释放了信号量,那信号量的资源数将会加 1。该函数实际上是一个宏定义,在 semphr.h 文件中有定义,具体的代码如下所示:

#define xSemaphoreGive( xSemaphore) \
 xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), \
 NULL, \
 semGIVE_BLOCK_TIME, \
 queueSEND_TO_BACK)

​ 从 上 面 的 代 码 中 可 以 看 出 , 函 数 xSemaphoreGive() 实 际 上 是 调 用 了 函 数 xQueueGenericSend()

函数 xSemaphoreGiveFromISR()

​ 此函数用于在中断中释放信号量。该函数实际上是一个宏定义,在 semphr.h 文件中有定义, 具体的代码如下所示:

#define xSemaphoreGiveFromISR( xSemaphore, \
 pxHigherPriorityTaskWoken) \
 xQueueGiveFromISR( ( QueueHandle_t ) ( xSemaphore ), \
 ( pxHigherPriorityTaskWoken ))

​ 从 上 面 的 代 码 中 可 以 看 出 , 函 数 xSemaphoreGiveFromISR() 实 际 上 是 调 用 了 函 数 xQueueGiveFromISR()。函数 xQueueGiveFromISR()在 queue.c 文件中有定义。

要特别注意的是,函数 xQueueGiveFromISR()只能用于释放二值信号量和计数型信号量, 而不能用于获取互斥信号量,因为互斥信号量会有优先级继承的处理,而中断不属于任务,没法进行优先级继承。

函数 vSemaphoreDelete()

​ 此函数用于删除已创建二值信号量。该函数实际上是一个宏定义,在 semphr.h 文件中有定 义,具体的代码如下所示:

#define vSemaphoreDelete(xSemaphore) \
 vQueueDelete ( QueueHandle_t ) \
 ( xSemaphore ))

​ 从上面的代码中可以看出,函数 vSemaphoreDelete()实际上是调用了函数 vQueueDelete()删除已创建的二值信号量队列。

计数型信号量

计数型信号量简介

​ 计数型信号量与二值信号量类似,二值信号量相当于队列长度为 1 的队列,因此二值信号 量只能容纳一个资源,这也是为什么命名为二值信号量,而计数型信号量相当于队列长度大于 0 的队列,因此计数型信号量能够容纳多个资源,这是在计数型信号量被创建的时候确定的。 计数型信号量通常用于一下两种场合:

事件计数

​ 在这种场合下,每次事件发生后,在事件处理函数中释放计数型信号量(计数型信号量的 资源数加 1),其他等待事件发生的任务获取计数型信号量(计数型信号量的资源数减 1),这么 一来等待事件发生的任务就可以在成功获取到计数型信号量之后执行相应的操作。在这种场合 下,计数型信号量的资源数一般在创建时设置为 0。

资源管理

​ 在这种场合下,计数型信号量的资源数代表着共享资源的可用数量,例如前面举例中停车 场中的空车位。一个任务想要访问共享资源,就必须先获取这个共享资源的计数型信号量,之 后在成功获取了计数型信号量之后,才可以对这个共享资源进行访问操作,当然,在使用完共 享资源后也要释放这个共享资源的计数型信号量。在这种场合下,计数型信号量的资源数一般 在创建时设置为受其管理的共享资源的最大可用数量。

计数型信号量相关 API 函数

​ 从上面中可以看出,计数型信号量除了创建函数之外,其余的获取、释放等信号量操作函 数,都与二值信号量相同,因此这里重点讲解计数型信号量的创建函数。

函数 xSemaphoreCreateCounting()

​ 此函数用于使用动态方式创建计数型信号量,创建计数型信号量所需的内存,由 FreeRTOS 从 FreeRTOS 管理的堆中进行分配。该函数实际上是一个宏定义,在 semphr.h 中有定义,具体 的代码如下所示:

#define xSemaphoreCreateCounting( uxMaxCount, \
 uxInitialCount ) \
 xQueueCreateCountingSemaphore( ( uxMaxCount ), \
 ( uxInitialCount ))

​ 从上面的代码中可以看出,函数 xSemaphoreCreateCounting()实际上是调用了函数

xQueueCreateCountingSemaphore(),函数 xQueueCreateCountingSemaphore()在 queue.c 文件中有 定义。

函数 xSemaphoreCreateCountingStatic()

​ 此函数用于使用静态方式创建计数型信号量,创建计数型信号量所需的内存,需要由用户 手动分配并提供。该函数实际上是一个宏定义,在 semphr.h 中有定义,具体的代码如下所示:

#define xSemaphoreCreateCountingStatic( uxMaxCount, \
 uxInitialCount, \
 pxSemaphoreBuffer) \
 xQueueCreateCountingSemaphoreStatic( ( uxMaxCount ), \
 ( uxInitialCount ), \
 ( pxSemaphoreBuffer ))

​ 从上面的代码中可以看出,函数 xSemaphoreCreateCountingStatic()实际上是调用了函数 xQueueCreateCountingSemaphoreStatic(),函数 xQueueCreateCountingSemaphoreStatic()在 queue.c 文件中有定义,其函数内容与函数 xQueueCreateCountingSemaphore()类似,只是动态创建队列 的函数替换成了静态创建队列的函数。

优先级翻转

​ 在使用二值信号量和计数型信号量的时候,经常会遇到优先级翻转的问题,优先级在抢占 式内核中是非常常见的,但是在实时操作系统中是不允许出现优先级翻转的,因为优先级翻转 会破坏任务的预期顺序,可能会导致未知的严重后果,下面展示了一个优先级翻转的例子,如下图所示:

​ 优先级翻转示意图,如上图所示,定义:任务 H 为优先级最高的任务,任务 L 为优先级中 最低的任务,任务 M 为优先级在任务 H 与任务 L 之间的任务。

​ (1) 任务 H 和任务 M 为阻塞状态,等待某一事件发生,此时任务 L 正在运行。

​ (2) 此时任务 L 要访问共享资源,因此需要获取信号量。

​ (3) 任务 L 成功获取信号量,并且此时信号量已无资源,任务 L 开始访问共享资源。

​ (4) 此时任务 H 就绪,抢占任务 L 运行。

​ (5) 任务 H 开始运行。

​ (6) 此时任务 H 要访问共享资源,因此需要获取信号量,但信号量已无资源,因此任务 H 阻塞等待信号量资源。

​ (7) 任务 L 继续运行。

​ (8) 此时任务 M 就绪,抢占任务 L 运行。

​ (9) 任务 M 正在运行。

​ (10) 任务 M 运行完毕,继续阻塞。

​ (11) 任务 L 继续运行。

​ (12) 此时任务 L 对共享资源的访问操作完成,释放信号量,虽有任务 H 因成功获取信号 量,解除阻塞并抢占任务 L 运行。

​ (13) 任务 H 得以运行。 从上面优先级翻转的示例中,可以看出,任务 H 为最高优先级的任务,因此任务 H 执行的 操作需要有较高的实时性,但是由于优先级翻转的问题,导致了任务 H 需要等到任务 L 释放信 号量才能够运行,并且,任务 L 还会被其他介于任务 H 与任务 L 任务优先级之间的任务 M 抢 占,因此任务 H 还需等待任务 M 运行完毕,这显然不符合任务 H 需要的高实时性要求。

互斥信号量

互斥信号量简介

​ 互斥信号量其实就是一个拥有优先级继承的二值信号量,在同步的应用中(任务与任务或 中断与任务之间的同步)二值信号量最适合。互斥信号量适合用于那些需要互斥访问的应用中。 在互斥访问中互斥信号量相当于一把钥匙,当任务想要访问共享资源的时候就必须先获得这把 钥匙,当访问完共享资源以后就必须归还这把钥匙,这样其他的任务就可以拿着这把钥匙去访 问资源。

​ 互斥信号量使用和二值信号量相同的 API 操作函数,所以互斥信号量也可以设置阻塞时间, 不同于二值信号量的是互斥信号量具有优先级继承的机制。当一个互斥信号量正在被一个低优 先级的任务持有时,如果此时有个高优先级的任务也尝试获取这个互斥信号量,那么这个高优 先级的任务就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与自己相 同的优先级,这个过程就是优先级继承。优先级继承尽可能的减少了高优先级任务处于阻塞态 的时间,并且将“优先级翻转”的影响降到最低。

​ 优先级继承并不能完全的消除优先级翻转的问题,它只是尽可能的降低优先级翻转带来的 影响。实时应用应该在设计之初就要避免优先级翻转的发生。互斥信号量不能用于中断服务函 数中,原因如下:

​ (1) 互斥信号量有任务优先级继承的机制,但是中断不是任务,没有任务优先级,所以互斥 信号量只能用与任务中,不能用于中断服务函数。

​ (2) 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态。

互斥信号量相关 API 函数

​ 从上面中可以看出,互斥信号量除了创建函数之外,其余的获取、释放等信号量操作函数, 都与二值信号量相同,因此这里重点讲解互斥信号量的创建函数。

函数 xSemaphoreCreateMutex()

​ 此函数用于使用动态方式创建互斥信号量,创建互斥信号量所需的内存,由 FreeRTOS 从 FreeRTOS 管理的堆中进行分配。该函数实际上是一个宏定义,在 semphr.h 中有定义,具体的 代码如下所示:

#define xSemaphoreCreateMutex() xQueueCreateMutex(queueQUEUE_TYPE_MUTEX)

​ 从 上 面 的 代 码 中 可 以 看 出 , 函 数 xSemaphoreCreateMutex() 实 际 上 是 调 用 了 函 数 xQueueCreateMutex(),函数 xQueueCreateMutex()在 queue.c 文件中有定义。

函数 xSemaphoreCreateMutexStatic()

​ 此函数用于使用静态方式创建互斥信号量,创建互斥信号量所需的内存,需要由用户手动 分配并提供。该函数实际上是一个宏定义,在 semphr.h 中有定义,具体的代码如下所示:

#define xSemaphoreCreateMutexStatic( pxMutexBuffer) \
 xQueueCreateMutexStatic( queueQUEUE_TYPE_MUTEX, \
 ( pxMutexBuffer ) )

​ 从上面的代码中可以看出,函数 xSemaphoreCreateMutexStatic()实际上是调用了函数 xQueueCreateMutexStatic(),而函数 xQueueCreateMutexStatic()在 queue.c 文件中有定义,其函数 内容与函数 xQueueCreateMutex()是类似的,只是将动态创建队列的函数替换成了静态创建队列 的函数

世界のネズミは彼らが望むものに依存し、彼らは彼ら自身から誰も求めません
最后更新于 2023-08-27