第一部分 信号量
(一)信号量的本质
首先我们先来看队列的结构体,我们不难发现队列结构体中说到有个联合体在用于队列时,使用Queue,在用于信号量时,使用XSemaphore。后面又说到了一些对列的类型,顺着这条线索我们来说说信号量。
细说上面的联合体
其实我们就不难发现,队列就是上述结构体+一个环形存储区,而信号量是一个特殊的队列,他只有队列的头部,并没有后面的环形存储区,因为信号量只负责信息传递,并不传递数据。
类似这样一个结构体。
(二)简介
信号量是一种任务间通信的机制,用来实现任务之间同步或临界资源的互斥访问。
是一个非负整数。当这个值加一:表明有一个资源被释放;当这个值减一:表明有一个资源被占有,当减到0时,表明没有任何资源,这时所有任务没有办法再获取到信号量,处于阻塞状态。
1、分类
(1)二值信号量
所谓二值信号量其实就是一个队列长度为1,没有数据存储器的队列,而二值则表示计数值uxMessagesWaiting只有0和1两种状态(就是队列空与队列满两种情况),uxMessagesWaiting在队列中表示队列中现有消息数量,而在信号量中则表示信号量的数量。
uxMessagesWaiting为0表示:信号量资源被获取了. uxMessagesWaiting为1表示:信号量资源被释放了 把这种只有 0 和 1 两种情况的信号量称之为二值信号量。
a.特点:信号量最大是1。
b.用于对临界资源的访问,也可以用来同步。比如之前我们在linux里面所说的按顺序打印ABC
这个就可以使用到二值信号量来同步三个资源的循环打印。
由于二值信号量就是特殊的队列,其实它的运转机制就是利用了队列的阻塞机,从而达到实现任务之间的同步与互斥(有优先级反转的缺陷,这个后面会介绍)。
(2)计数信号量
a.特点:用于对事件的计数和资源的管理
事件的计数:当信号量⽤于对事件计数时,信号量的值表示未被处理的事件数。其过程为:当中断或是任务中某个事件发⽣时,就会释放⼀个信号量(信号量计数值+1),当事件被处理时,信号量计数值-1。
资源管理:信号量的计数值表示系统中可⽤资源的数量。但是任务必须先获取到信号量才能访问资 源。
b.例子:停车场的空车位:最开始空车位为最大值,停一辆车则空车位资源数就减一,出去一辆空车位就加一,当全部停满时,在有车来停则停车失败可以选择等待(等待有空车位),当空车位为最大值时则不能再继续出车(因为停车场已经没有车了)。
2、常用API函数
(1)二值信号量
这里只介绍目前我了解的
接口函数 | 函数功能 |
---|---|
xSemaphoreCreateBinary() | (动态)创建一个二值信号量 |
xSemaphoreTake() | 获取信号量 |
xSemaphoreGive() | 释放信号量 |
xSemaphoreDelete() | 删除信号量 |
SemaphoreHandle_t | 信号量句柄 |
a.信号量句柄
从这里我们也能发现信号量其实就是一个特殊的队列。
b.创建一个二值信号量
实际是调用了这个函数
c.获取一个信号量
获取信号量其实与队列的出队基本差不多的 ①获取信号量资源数uxMessagesWaiting(在二值信号量中只能是0/1,代表信号量为空或满) ②判断信号量是否有资源uxSemaphoreCount >0 (有资源才能被获取) ③如果信号量有资源,uxSemaphoreCount 计数值减一
d.释放一个信号量
实际调用的是这个函数
1.判断信号量计数值是否达最大值, pxQueue->uxMessagesWaiting < pxQueue->uxLength ,(在创建二值信号量中,uxLength 赋值为1,则信号量计数值的最大值就为1,也就是说计数值要等于0才能释放信号) 2.若满足信号量释放条件,则计数值(uxMessagesWaiting )加1,并不会拷贝数据,此时计数值为1表示信号量有资源,如果有因为获取信号量而阻塞的任务,则需要将其唤醒(从xTasksWaitingToReceive列表中移除,将任务挂入就绪列表)
e.删除信号量
(2)计数信号量
这里只介绍目前我了解的
接口函数 | 函数功能 |
---|---|
xSemaphoreCreateCounting() | (动态)创建一个计数值信号量 |
xSemaphoreTake() | 获取信号量 |
xSemaphoreGive() | 释放信号量 |
xSemaphoreDelete() | 删除信号量 |
SemaphoreHandle_t | 信号量句柄 |
其实创建计数值信号量函数很简单,里面还是调用了队列创建函数xQueueGenericCreate(),创建一个长度为uxMaxCount,消息大小为0的特殊队列即计数值信号量,当然创建成功后,将计数值初值赋值成uxInitialCount,就是这么简单,然后其他获取、释放信号量等操作与二值信号量一模一样。
(三)互斥量
1、基本介绍
(1)概念:互斥量,⼜称互斥信号量,是⼀种特殊的⼆值信号量。
(2)状态:开锁 或 闭锁。 当线程持有它后,互斥量处于闭锁状态,由这个线程获得它的所有权。相反,当这个线程释放这个锁后,将对互斥量解锁,失去它的所有权。
(3)特点:互斥量所有权、递归访问、防⽌优先级翻转,解决了二值信号量存在优先级翻转的缺点,可以优先级继承。
2、优先级反转
2.1简介
低优先级的任务霸占CPU资源,导致高优先级任务无法运行的情况。低优先级的任务持有一个被高优先级任务所需要的共享资源(低优先级给共享资源上锁,高优先级需要等待低优先级解锁共享资源,而在这期间正好一个中等优先级的任务打断低优先级任务去执行,则低优先级任务迟迟不能运行则不能给共享资源解锁,导致高优先级要一直等待(阻塞),而中等优先级的任务正在运行(逍遥法外))。我们把这种高优先级任务无法运行而低优先级的任务可以运行的现象 称之为------优先级翻转。
优先级翻转带来的危害:续接上述案列,任务H等待资源⽽阻塞,任务L还在占⽤资源,此时任务M就绪,将抢占CPU,最终任务H等待执行的时间 = 任务M执行完 + 任务L释放资源。如果,有多个任务打断L,那么任务H的等待时间将会变的更长。
2.2代码演示
解决方式就是优先级继承,不是说低优先级任务无法执行,就在高优先级任务进入阻塞之前将低优先级任务的优先级提升至与高优先级一致,这样等高优先级任务进入阻塞之后,低优先级任务就能继承高优先级任务的优先级,这样低优先级任务就能尽快执行(从而解锁,让高优先级能够获取锁)。
3、优先级继承
优先级继承:暂时提高某个占有某种资源的低优先级任务的优先级,使之与在所有等待该资源的任务中优先级最高那个任务的优先级相等,而当这个低优先级任务执行完毕释放该资源时,优先级重新回到初始设定值。
当高优先级任务获取锁进入阻塞之前,会将低优先级任务的优先级提升至与高优先级任务一样,则等高优先级任务C进入阻塞之后,则低优先级任务A继承任务C的优先级,则任务A立马执行(任务B没机会执行),则任务A继续执行就能尽快开锁(释放信号量),这样就能极大的减少优先级反转带来的影响。
4、互斥量所有权
当一个线程持有互斥量时,其他线程将不能够对它进行开锁或持有它,持有该互斥量的线程也能够再次获得这个锁而不被挂起。
多线程环境下往往存在多个线程竞争同一临界资源的应用场景,互斥量可被用于对临界资源的保护从而实现独占式访问。
所以根据这个特征也可以解决上述优先级翻转的问题。
!!!注意:
互斥信号量不能用于中断服务函数中,原因如下: (1) 互斥信号量有任务优先级继承的机制,但是中断不是任务,没有任务优先级,所以互斥信号量只能用与任务中,不能用于中断服务函数。 (2) 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态
中断服务函数(Interrupt Service Routine,ISR)是一种特殊的函数,在处理硬件中断时会被调用。由于中断服务函数是在中断上下文中执行的,其执行期间必须尽可能地短小和高效,以确保不会影响系统的实时性和稳定性。因此,在中断服务函数中通常应该避免使用会导致阻塞的操作,比如等待互斥信号量而设置阻塞时间进入阻塞态。
主要原因包括:
实时性要求: 中断服务函数需要尽快完成对中断的处理,以确保系统能够及时响应硬件中断。如果在中断服务函数中设置阻塞时间并进入阻塞态,会导致无法满足实时性要求,可能造成系统性能下降或功能异常。
上下文切换: 在中断服务函数中执行阻塞操作可能引起上下文切换的开销。当中断服务函数被阻塞时,操作系统可能需要切换到其他任务来继续执行,这会增加系统的开销并可能引入不确定性。
死锁风险: 在中断服务函数中使用互斥信号量并设置阻塞时间会增加系统发生死锁的风险。如果其他任务持有需要的互斥信号量并且不释放,那么中断服务函数将永远无法获取该信号量,导致死锁。
5、互斥信号量解析
5.1 API函数
接口函数 | 函数功能 |
---|---|
xSemaphoreCreateMutex() | (动态)创建一个互斥信号量 |
xSemaphoreTake() | 获取信号量 |
xSemaphoreGive() | 释放信号量 |
xSemaphoreDelete() | 删除信号量 |
SemaphoreHandle_t | 互斥信号量句柄 |
在创建互斥信号量时候会释放一次信号量,表示一开始就有资源,而二值信号量则不会释放。
接下来就是看在什么时候进行优先级继承,什么时候解除优先级继承?
进行优先级继承的时候肯定是高优先级任务去获取信号量失败然后让低优先级任务继承它的优先级。 进行解除优先级继承的时候肯定是低优先级释放信号量,此时它的任务完成需要恢复成之前的优先级。
所以优先级继承发生在获取信号量,解除优先级继承发生在释放信号量。
以上两张图是我截别人的,在这块还是需要花大功夫研究一下里面继承的机制。
6、递归访问互斥信号量
6.1 API函数
接口函数 | 函数功能 |
---|---|
xSemaphoreCreateRecursiveMutex() | (动态)创建一个递归互斥信号量 |
xSemaphoreTakeRecursive() | 获取信号量 |
xSemaphoreGiveRecursive() | 释放信号量 |
xSemaphoreDelete() | 删除信号量 |
6.2为什么互斥信号量可以递归
互斥锁支持递归访问是为了解决同一任务需要多次获取同一个互斥锁的情况。当一个任务已经获取了互斥锁,并且在持有锁的状态下再次请求该锁时,如果互斥锁支持递归访问,那么这个任务可以再次成功获取该锁,而不会因为自己已经持有锁而被阻塞。
支持递归访问的互斥锁通过记录任务对锁的获取次数来实现递归访问。每当任务成功获取锁时,记录获取锁的次数;每当任务释放锁时,相应地减少获取锁的次数。只有当获取锁的次数归零时,其他任务才能成功获取该锁。
递归访问的互斥锁在某些情况下非常有用,比如在复杂的递归函数中可能需要多次获取同一个锁,或者在对象导向的程序设计中可能需要在对象的不同方法中多次获取同一个锁。
6.3 为什么普通信号量不能递归
信号量是一种用于任务同步和互斥的机制,与互斥锁不同,信号量并不支持递归访问。这是因为信号量的目的是控制对资源的访问权限,而不是为了提供递归锁定的功能。
在信号量中,当一个任务获取信号量时,信号量的计数会相应地减少;当任务释放信号量时,计数会增加。任务获取信号量后,其他任务将无法再次获取相同的信号量,直到原任务释放信号量为止。这种机制确保了资源的独占性和排他性。
如果信号量支持递归访问,就会破坏信号量所设计的用途和原理。递归访问会导致信号量的计数出现不可预测的变化,可能引发死锁或其他意想不到的问题。因此,为了保持信号量的正确性和一致性,通常不建议在信号量上实现递归访问的功能。如果需要递归锁定的功能,应该使用互斥锁来实现。