一、信号量
(一)信号量概括
信号量是操作系统中重要的一部分,信号量是一种解决同步问题 的机制,可以实现对共享资源的有序访问 。
FreeRTOS 提供了多种信号量,按信号量的功能可分为二值信号量、计数型信号量、互斥信号量和递归互斥信号量,不同类型的信号量有其不同的应用场景。
信号量(Semaphore)和互斥量(Mutex)都基于消息队列的基本数据结构,但是信号量和互斥量又有一些区别。
信号量与队列区别:
(二)二值信号量
1、二值信号量概述
二值信号量的本质是一个队列长度为 1 的队列 ,该队列就只有空和满两种情况。
二值信号量通常用于互斥访问或任务同步, 与互斥信号量比较类似,但是二值信号量有可能会导致优先级翻转的问题 ,所以二值信号量更适合用于同步 !
2、二值信号量使用
二值信号量(Binary Semaphore)就是只有一个项的队列。
二值信号量就像是一个标志,适合用于进程间同步的通信
(1)上图有两个进程,一个负责释放二值信号量,另外一个负责获取。ADC中断ISR读取ADC转换结果后写入数据缓冲区,数据处理任务负责读取缓冲区数据并进行处理。
(2)数据缓冲区是两个进程间同步访问的对象。
假设数据缓冲区只存储一次的转换结果数据,ADC中断ISR读取ADC转换结果后写入到缓冲区中,并且释放二值信号量,此时二值信号量有效,表示缓冲区有了新的数据(理解为自定义的标志变量,查询标志变量被置1表明有数据了可以下一步处理)
(3)数据处理任务总是获取(Take)二值信号量。
有效后(理解为裸机开发中标志变量置位),任务立刻进入就绪状态参与任务调度,就可以读取缓冲区的数据并进行处理。无效时(理解为裸机开发中,数据处理完毕,清空标志位),任务在阻塞状态等待;
使用二值信号量的过程:创建二值信号量 -> 释放二值信号量-> 获取二值信号量
(三)计数信号量
计数型信号量与二值信号量类似, 二值信号量相当于队列长度为 1 的队列,因此二值信号量只能容纳一个资源,这也是为什么命名为二值信号量,而计数型信号量相当于队列长度大于0 的队列,因此计数型信号量能够容纳多个资源,这是在计数型信号量被创建的时候确定的。
1、事件计数:
每次事件发生后,在事件处理函数中释放计数型信号量(计数型信号量的
资源数加 1),其他等待事件发生的任务获取计数型信号量(计数型信号量的资源数减 1),这么一来等待事件发生的任务就可以在成功获取到计数型信号量之后执行相应的操作。
2、资源管理:
在这种场合下,计数型信号量的资源数代表着共享资源的可用数量,例如前面举例中停车场中的空车位。一个任务想要访问共享资源,就必须先获取这个共享资源的计数型信号量,之后在成功获取了计数型信号量之后,才可以对这个共享资源进行访问操作,当然,在使用完共享资源后也要释放这个共享资源的计数型信号量。在这种场合下,计数型信号量的资源数一般在创建时设置为受其管理的共享资源的最大可用数量。
(1)一个计数型信号量被创建时设置了初值4,这个值只是个计数值。
(2)有1个客人进店时就是获取(Take)信号量,计数信号量的值减1。当计数信号量的值变为0时,再有客人要进店时就得等待。
(3)如果有1个客人用餐结束离开了就是释放(Give)信号量,计数信号量的值加1,表示可用资源数量增加了1个。
(三)互斥量
1、互斥量概述
使用二值信号量时可能会出现优先级翻转的问题。互斥量引入了优先级继承机制,可以减缓优先级翻转问题。
互斥信号量其实就是一个拥有优先级继承的二值信号量,在同步的应用中(任务与任务或中断与任务之间的同步)二值信号量最适合。互斥信号量适合用于那些需要互斥访问的应用中
在互斥访问中互斥信号量相当于一把钥匙, 当任务想要访问共享资源的时候就必须先获得这把钥匙,当访问完共享资源以后就必须归还这把钥匙,这样其他的任务就可以拿着这把钥匙去访问资源。
(1)两个任务互斥性地访问串口,即在任务A访问串口时,其他任务不能访问串口。
(2)互斥量相当于管理串口的一把钥匙。一个任务可以获取(Take)互斥量,获得互斥量后将独占对串口的访问,访问完后要释放(Give)互斥量。
(3)一个任务获得互斥量后,对资源进行访问时,其他想要获取互斥量的进程只能等待。
2、二值信号量出现的优先级翻转问题
二值信号量也可以用于互斥型资源访问控制,但是容易出现
优先级翻转(Priority Inversion)问题
例如:
高优先级任务被低优先级任务阻塞,导致高优先级任务迟迟得不到调度。但其他中等优先级的任务却能抢到CPU资源。从现象上看,就像是中优先级的任务比高优先级任务具有更高的优先权(即优先级翻转)
已知:
任务L、任务M、任务H任务优先级依次是低中高,任务L和任务H需要获取信号量才能运行,任务M不需要获取信号量。
任务运行过程:
(1)刚开始低优先级任务获取信号量后,任务L处于运行中,高优先级任务H随后处于就绪状态,出现了比运行任务L优先级更高的任务,应该抢断低优先级任务的cpu占有权进入运行态,但是高优先级任务只有获取到信号量才能进入运行态(任务L占有信号量并没有释放),由于没有有效的信号量,所以高优先任务H无奈只能进入阻塞态等待信号量的释放,此时处于运行中的还是低优先级任务。
(2)一段时间以后,中优先级任务M处于就绪态,中优先级任务显然优先级高于运行中的低优先级任务L,由于不需要获取信号量即可运行,所以此时中优先级任务M打断低优先级任务L的运行,中优先级任务得到了cpu的使用权限进入了运行态。
(3)当中优先级任务M运行完毕之后,由于任务H无法获取信号量,迫不得已运行权限还是给到了任务L,等任务L运行完毕释放信号量后此时信号量变为有效,高优先任务H获取到信号量才得以运行。
总之:
从上面优先级翻转的示例中,可以看出,任务 H 为最高优先级的任务,因此任务 H 执行的操作需要有较高的实时性,但是由于优先级翻转的问题,导致了任务 H 需要等到任务 L 释放信号量才能够运行,并且,任务 L 还会被其他介于任务 H 与任务 L 任务优先级之间的任务 M 抢占,因此任务 H 还需等待任务 M 运行完毕,这显然不符合任务 H 需要的高实时性要求。
黑色表示任务L运行的时间段,红色表示任务H运行,蓝色表示任务M运行时间
-
低优先级任务TaskLP在t1时刻开始处于运行状态,并且获取了一个二值信号量semp。
-
在时刻t2,高优先级任务TaskHP进入运行状态,它申请二值 信号量semp,但是二值信号量被任务TaskLP占用,所以TaskHP在时刻t3进入阻塞等待状态,TaskLP进入运行状态
-
在时刻t4,中等优先级任务TaskMP抢占了TaskLP的CPU使用权,TaskMP不使用二值信号量,所以它一直运行到时刻t5才进入阻塞状态。
-
从t5时刻开始TaskLP又进入运行状态,直到t6时刻释放二值信号量semp,TaskHP才能进入运行状态。
高优先级的任务TaskHP需要等待低优先级的任务TaskLP释放二值信号量之后才可以运行,这也是期望的运行效果。但是在t4时刻,虽然任务TaskMP的优先级比TaskHP低,
但是它先于TaskHP抢占了CPU的使用权,这破坏了基于优先级抢占式执行的原则,对系统的实时性是有不利影响的。
3、互斥信号量优先级继承改善优先级翻转问题
当一个互斥信号量正在被一个低优先级的任务持有时, 如果此时有个高优先级的任务也尝试获取这个互斥信号量,那么这个高优先级的任务就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级。
互斥信号量下任务的运行过程
- t1时刻:TaskLP处于运行状态,并且获得了一个互斥量mutex
- t2时刻:TaskHP进入运行状态
- t3时刻: TaskHP申请互斥量mutex,但是互斥量被TaskLP占用,TaskHP进入阻塞等待状态,TaskLP进入运行状态。但是在t3时刻,RTOS将TaskLP的优先级临时提高到与TaskHP相同的级别,这就是优先级继承。
- t4时刻,TaskMP进入就绪状态,但是因为TaskLP的临时优先级高于TaskMP,所以TaskMP无法获得CPU的使用权。
- t5时刻:TaskLP释放互斥量,任务TaskHP立刻抢占CPU的使用权,并恢复TaskLP原来的优先级。
- t6时刻:TaskHP进入阻塞状态后,TaskMP才进入运行状态。
二、信号量操作函数
信号量和互斥量的主要操作是释放和获取
(一)二值信号量
1、xSemaphoreCreateBinary() 创建信号量
二值信号量被创建后是无效的,相当于值为0。释放二值信号量就是使其有效,相当于使其变为1。(在创建二值信号量完毕,使用之前需要手动释放一次信号量让信号量变为有效)
关于二值信号量在创建后是否需要先释放一次,取决于使用场景和期望的行为:
- 不先释放的情况:
如果希望在信号量创建后立即表示资源是不可用的(例如,你可能希望在初始化资源或进行某些设置之后再允许其他线程访问),则不需要在创建后先释放信号量。 - 先释放的情况:
如果希望在信号量创建后立即表示资源是可用的,那么需要在创建后先释放一次信号量。
c
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
#define xSemaphoreCreateBinary() xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE )
#endif
//无参数、返回值是二值信号量句柄
官方例程:
(1)创建一个变量
(2)创建二值信号量将返回值赋值给创建的变量
(3)判断返回值是否为空,如果不为空表明创建成功
2、xSemaphoreGive()释放二值信号量
在释放信号量之前,需要先对要释放的信号量进行判断,判断信号量是否存在,存在的情况下才去执行相关的操作。
c
#define xSemaphoreGive( xSemaphore ) xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )
参数1:是要释放的信号量句柄
返回值:pdTRUE表明释放成功
3、xSemaphoreTake()获取二值信号量
c
#define xSemaphoreTake( xSemaphore, xBlockTime ) xQueueSemaphoreTake( ( xSemaphore ), ( xBlockTime ) )
参数1:信号量句柄
参数2:阻塞时间
返回值:pdTRUE表明获取信号量成功
(二)计数信号量
使用计数型信号量的过程:创建计数型信号量 ->释放信号量 -> 获取信号量
1、创建计数信号量
c
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
#define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ) xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) )
#endif
参数1:计数信号量最大值
参数2:创建计数信号量时的初始值(计数用就设为0,有效资源个数就设为最大值)
返回值:计数信号量的句柄(NULL表示创建失败)
2、释放获取信号量同二值信号量
3、获取计数信号量当前计数值
c
#define uxSemaphoreGetCount( xSemaphore ) uxQueueMessagesWaiting( ( QueueHandle_t ) ( xSemaphore ) )
参数1:计数信号量句柄
返回值:当前信号量的计数值大小
(三)互斥量
使用互斥信号量:首先将宏configUSE_MUTEXES置一
使用流程:创建互斥信号量 ->(task)获取信号量 ->(give)释放信号量
1、xSemaphoreCreateMutex()创建互斥量
c
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
#define xSemaphoreCreateMutex() xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )
#endif
返回值:互斥量的句柄
官方例子:
c
Example usage:
SemaphoreHandle_t xSemaphore;
void vATask( void * pvParameters )
{
// Semaphore cannot be used before a call to xSemaphoreCreateMutex().
// This is a macro so pass the variable in directly.
xSemaphore = xSemaphoreCreateMutex();
if( xSemaphore != NULL )
{
// The semaphore was created successfully.
// The semaphore can now be used.
}
}
2、互斥信号量释放与获取同二值信号量
获取互斥量使用函数xSemaphoreTake(),释放信号量使用
函数xSemaphoreGive(),这两个函数的用法与获取和释放二值信号量一样。
注意1: 创建互斥信号量时,会主动释放一次信号量
注意2: 互斥信号量不支持中断中调用。
xSemaphoreGiveFromISR()和xSemaphoreTakeFromISR()不能应用于互斥量。