一、信号量的概念
1、信号量的基本概念
消息队列是实现任务与任务或任务与中断间通信的数据结构,可类比裸机编程中的数组
信号量是实现任务与任务或任务与中断间通信的机制,可以类比裸机编程中的标志位
信号量 (semaphore) 可以实现任务与任务或任务与中断间的同步功能 (二值信号量)、资源管理(计数信号量)、临界资源的互斥访问(互斥信号量) 等
信号量是一个非负正数,二值信号量与互斥信号量取值范围为 0-1,计数信号量取值范围是 0-N(N>1)
0: 信号量为空,所有试图获取它的任务都将处于阻塞状态,直到超时退出或其他任务释放信号量
正数: 表示有一个或多个信号量供获取
2、信号量的分类
- 二值信号量 (重点讲解同步应用)
- 计数信号量 (重点讲解资源管理)
- 互斥信号量 (重点讲解互斥访问)
- 递归互斥信号量 (简要了解即可)
二、互斥信号量的定义与应用
1、互斥信号量的定义
前面学过,取值只有 0 与 1 两种状态的信号量称之为二值信号量。 而互斥信号量是一种特殊的二值信号量,具有防止优先级翻转的特性。
创建互斥信号量时,系统会为创建的互斥信号量分配内存,互斥信号量创建完成后的示意图如下:
从上图可以看出,互斥信号量是一种长度为 1,消息大小为 0 的特殊消息队列。
因为这个队列只有空或满两种状态,而且消息大小为 0,因此在运用时,只需要知道队列中是否有消息即可,而无需关注消息是什么。
2、互斥信号量的应用
在嵌入式操作系统中,互斥信号量用于临界资源的独占式访问,只能用于任务与任务间,因为其特有的优先级继承机制只能在任务中起作用,在中断的上下文环境毫无意义。
应用场景:
比如有两个任务需要通过同一串口发送数据,其硬件资源只有一个,那么两个任务不能同时发送,否则会导致数据错误。此时就可以用互斥信号量对串口资源进行保护,当任务 1 正在使用串口发送数据时,互斥信号量变为无效,任务 2 无法使用串口,任务 2 必须等待互斥信号量有效 (任务 1 释放信号量),才能获得串口使用权,进而发送数据。
3、简要了解递归互斥信号量
递归互斥信号量是一种特殊的互斥信号量,支持拥有该信号量使用权的任务重复多次获取,而不会死锁。
任务成功获取几次递归互斥信号量,就要返还几次,在此之前,递归互斥信号量都处于无效状态。
递归互斥信号量应用很少,简要了解即可。
三、优先级翻转问题
下面我们通过如下的框图来说明一下优先级翻转的问题,让大家有一个形象的认识。
运行条件:
- 创建 3 个任务 Task1,Task2 和 Task3,优先级分别为 3,2,1。也就是 Task1 的优先级最高。
- 任务 Task1 和 Task3 互斥访问串口打印 printf,采用二值信号实现互斥访问。
- 起初 Task3 通过二值信号量正在调用 printf,被任务 Task1 抢占,开始执行任务 Task1,也就是上图的起始位置。
- 任务 Task1 运行的过程需要调用函数 printf,发现任务 Task3 正在调用,任务 Task1 会被挂起,等待 Task3 释放函数 printf。
- 在调度器的作用下,任务 Task3 得到运行,Task3 运行的过程中,由于任务 Task2 就绪,抢占了 Task3 的运行。优先级翻转问题就出在这里了,从任务执行的现象上看,任务 Task1 需要等待 Task2 执行完毕才有机会得到执行,这个与抢占式调度正好反了,正常情况下应该是高优先级任务抢占低优先级任务的执行,这里成了高优先级任务 Task1 等待低优先级任务 Task2 完成。这种情况被称之为优先级翻转问题。
- 任务 Task2 执行完毕后,任务 Task3 恢复执行,Task3 释放互斥资源后,任务 Task1 得到互斥资源,从而可以继续执行。
上面就是一个产生优先级翻转问题的现象,想象一下,如果介于任务 1 与任务 3 之间的任务特别多,对于抢占式实时操作系统来说是致命的危害,有可能导致系统崩溃 ,下一讲通过编程进行验证。
不过也不用担心,使用互斥信号量,可以有效的防止优先级翻转问题,后面会讲解。
四、优先级翻转编程测试
视频讲解
串口输出信息:
五、互斥信号量的运作机制
互斥量处理不同任务对临界资源的访问时,任务要获得互斥量才能进行资源访问。一旦有任务成功获得了互斥量,则互斥量立即变为闭锁状态,此时其他任务会因为获取不到互斥量而不能访问这个资源。任务会根据用户自定义的等待时间进行等待,直到互斥量被持有的任务释放,其他任务才能获取互斥量从而得以访问该临界资源。此时互斥量再次上锁,如此一来就可以确保每个时刻只有一个任务正在访问这个临界资源,保证了临界资源操作的安全性,具体如下图所示。
①:因为互斥量具有优先级继承机制,一般选择使用互斥量对资源进行保护。当采用互斥量保护的资源被占用时,无论是什么优先级的任务,想要使用该资源都会被阻塞
②:假如正在使用该资源的任务 1 比阻塞中的任务 2 的优先级低,那么任务 1 的优先级将被系统临时提升到与高优先级任务 2 相等 (任务 1 的优先级从 L 变成 H),这个就是所谓的优先级继承,这样就有效地防止了优先级翻转问题,因为此时优先级介于任务 1 与任务 2 之间的任务,抢占不了 CPU。
③:当任务 1 使用完资源之后,释放互斥量,此时任务 1 的优先级会从 H 变回原来的 L
④~⑤:任务 2 此时可以获得互斥量,然后进行资源的访问,当任务 2 访问了资源时,该互斥量的状态又变为闭锁状态,其他任务无法获取互斥量。
六、互斥信号量常用的 API 函数
1、使用互斥信号量的典型流程如下:
- 创建互斥信号量
- 释放互斥信号量
- 获取互斥信号量
- 删除互斥信号量
2、常用 API 函数如下:
- xSemaphoreCreateMutex()
- xSemaphoreGive()
- xSemaphoreTake()
- vSemaphoreDelete()
3、互斥信号量创建与删除
互斥信号量控制块 (句柄)
如下图:二值信号量的句柄为消息队列的句柄,因为二值信号量是一种长度为 1,消息大小为 0 的特殊消息队列
互斥信号量创建
函数原型:
SemaphoreHandle_t xSemaphoreCreateMutex(void)
函数描述:
函数 xSemaphoreCreateMutex 用于创建互斥信号量。
- 返回值,如果创建成功会返回互斥信号量的句柄,如果由于 FreeRTOSConfig.h 文件中 heap 大小不足,无法为此互斥信号量提供所需的空间会返回 NULL。
使用这个函数要注意以下问题
- 使用此函数要在 FreeRTOSConfig.h 文件中使能宏定义:#define configUSE_MUTEXES 1
说明:此函数基于消息队列函数实现:
应用举例:
互斥信号量删除
函数原型:
void vSemaphoreDelete(SemaphoreHandle_t xSemaphore); /* 信号量句柄 */
函数描述:
函数 vSemaphoreDelete 可用于删除互斥信号量。
4、互斥信号量释放
函数原型:
xSemaphoreGive(SemaphoreHandle_t xSemaphore); /* 信号量句柄 */
函数描述:
函数 xSemaphoreGive 用于在任务代码中释放信号量。
- 第 1 个参数是信号量句柄。
- 返回值,如果信号量释放成功返回 pdTRUE,否则返回 pdFALSE,因为信号量的实现是基于消息队列,返回失败的主要原因是消息队列已经满了。
使用这个函数要注意以下问题:
- 此函数是用于任务代码中调用的,不可以在中断服务程序中调用此函数。
- 使用此函数前,一定要保证用函数 xSemaphoreCreateBinary(), xSemaphoreCreateMutex() 或者 xSemaphoreCreateCounting() 创建了信号量。
- 此函数不支持使用 xSemaphoreCreateRecursiveMutex() 创建的信号量。
应用举例:
5、互斥信号量获取
函数原型:
xSemaphoreTake( SemaphoreHandle_t xSemaphore, /* 信号量句柄 */
TickType_t xTicksToWait ); /* 等待信号量可用的最大等待时间 */
函数描述:
函数 xSemaphoreTake 用于在任务代码中获取信号量。
- 第 1 个参数是信号量句柄。
- 第 2 个参数是没有信号量可用时,等待信号量可用的最大等待时间,单位系统时钟节拍。 返回值,如果创建成功会获取信号量返回 pdTRUE,否则返回 pdFALSE。
使用这个函数要注意以下问题:
- 此函数是用于任务代码中调用的,不可以在中断服务程序中调用此函数。
- 如果消息队列为空且第 2 个参数为 0,那么此函数会立即返回。
- 如果用户将 FreeRTOSConfig.h 文件中的宏定义 INCLUDE_vTaskSuspend 配置为 1 且第 2 个参数配置为 portMAX_DELAY,那么此函数会永久等待直到信号量可用。
应用举例:
七、互斥信号量的应用编程
视频讲解,互斥信号量应用于临界资源管理,解决优先级翻转问题
串口输出信息:
STM32cubeMX 配置:
代码: