网站建设 竞赛 方案,wordpress用的什么框架,直播源码下载,宁夏吴忠市红寺堡建设局网站#x1f431;作者#xff1a;一只大喵咪1201 #x1f431;专栏#xff1a;《RTOS学习》 #x1f525;格言#xff1a;你只管努力#xff0c;剩下的交给时间#xff01; 信号量 | 互斥量 | 递归锁 #x1f37a;信号量#x1f964;原理#x1f964;使用信号量的函数作者一只大喵咪1201 专栏《RTOS学习》 格言你只管努力剩下的交给时间 信号量 | 互斥量 | 递归锁 信号量原理使用信号量的函数基本使用 互斥量原理使用互斥量的函数互斥量的基本使用优先级反转优先级继承 递归锁大概原理使用递归锁的函数使用 总结 信号量
信号量也是FreeRTOS实现同步与互斥的方式。 如上图便是信号量的模型任务A和任务B是生产者任务负责生产数据任务C和任务D是消费者任务负责消费数据。
生产者每生产一个数据信号量就加1当增加到用户设定的限定值时直接失败返回。
消费者每消费一个数据信号量就减一当信号量减到0的时候消费者任务就处于阻塞状态直到新数据到来才被唤醒。 被唤醒时谁的优先级高就唤醒谁优先级相同就唤醒阻塞时间最长的任务。 从信号量这个名字来看
信号起通知作用量还可以用来表示资源的数量
当量没有限制时它就是“计数型信号量”当量只有0、1两个取值时它就是“二进制信号量”。 二进制信号量和计数型信号量的唯一差别就是计数值的最大值被限定为1。 支持give给出资源计数值加一还支持take获得资源计数值减一。 信号量本身就是一个共享资源。 计数型信号量的典型场景是
计数数据产生时give信号量让计数值加1处理数据时要先take信号量就是获得信号量让计数值减1。资源管理要想访问资源需要先take信号量让计数值减1用完资源后give信号量让计数值加1。 信号量只表示数据的存在情况相当于买票时票的余量它并不管理数据本身。 原理 如上图在使用信号量的时候需要先调用xSemaphoreCreateCounting来创建信号量可以看到它底层调用的是xQueueCreateCountingSemaphore函数从名字就可以看出其实是创建了一个队列。
在函数内部会调用xQueueGenericCreate创建一个队列该队列的大小就是指定信号量的计数值。 信号量的上限值就是队列的长度。 如上图使用xSemaphoreGive增加信号量时其本质就是向队列中写数据每增加1就写一个数据。
使用xSemaphoreTake减少信号量时其本质就是从队列中读取数据每减1就读取一个数据。
由于信号量的底层其实是队列所以它实现同步与互斥的原理是和队列是类似的。
信号量和队列的对比
队列信号量可以容纳多个数据有两部分内存队列结构体、存储数据的空间只有计数值无法容纳其他数据生产者没有空间存入数据时可以阻塞生产者不用阻塞计数值达到最大时失败返回消费者没有数据时可以阻塞消费者没有资源时可以阻塞 除了使用内存上的区别外最大的区别就是信号量的生产者任务一般不阻塞失败了就直接返回。 使用信号量的函数
使用信号量时先创建、然后去添加资源、获得资源。使用句柄来表示一个信号量。
创建二进制信号量
/* 动态创建 */
SemaphoreHandle_t xSemaphoreCreateBinary( void );/* 静态创建 */
SemaphoreHandle_t xSemaphoreCreateBinaryStatic(StaticSemaphore_t *pxSemaphoreBuffer );动态创建时不用传参只需要接收返回的信号量句柄。静态创建时需要用户指定存放信号量的内存空间pxSemaphoreBuffer 。 创建计数型信号量
/* 动态创建 */
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount);/* 静态创建*/
SemaphoreHandle_t xSemaphoreCreateCountingStatic( UBaseType_t uxMaxCount, UBaseType_t uxInitialCount, StaticSemaphore_t *pxSemaphoreBuffer );uxMaxCount最大计数值uxInitialCount计数初始值pxSemaphoreBuffer静态创建时需要指定存放信号量的内存。返回值创建成功返回信号量句柄 增加信号量
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );xSemaphore 要增加的信号量句柄。返回值成功返回pdTRUE失败返回pdFALSE。 获取信号量
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore,TickType_t xTicksToWait);xSemaphore要减少的信号量句柄xTicksToWait无法获得信号量时的阻塞时间0不阻塞马上返回portMAX_DELAY一直阻塞直到被唤醒。返回值成功返回pdTRUE失败返回pdFALSE。 删除信号量
对于动态创建的信号量不再需要它们时可以删除它们以回收内存。
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );xSemaphore 要删除的信号量句柄无返回值必然会成功。 基本使用
二进制信号量
通过二进制信号量来解决两个任务同时使用一个串口的时的缺陷 如上图创建一个二进制信号量此时信号量的初始值是0所以需要先使用xSemaphoreGive给它增加1。
当两个任务在使用串口时先Take信号量如果Take成功才能使用串口否则就阻塞等待使用完毕后需要Give信号量以便另一个任务使用串口。 如上图两个任务在交替使用串口。
这个过程中两个任务只能有一个任务使用串口有任务在使用串口这个二进制信号量就是0另一个任务就无法Take只能阻塞就实现了互斥的目的。 使用信号量不存在Take到一半时被切换走因为获取过程中会关闭所有中断不会调度其他任务。这种要么不做要做就做完的性质称为原子性。 信号量的Take和Give操作是具有原子性的。
计数型信号量 如上图在使用计数型信号量之前必须先定义宏开关configUSE_COUNTING_SEMAPHORES。 如上图代码先创建计数型信号量计数上限是3初始值是0然后创建两个任务发送任务vSender用来增加信号量优先级是2接收任务vReceiver用来减少信号量优先级是1。 如上图代码所示任务vSender的优先级高所以先执行它连续四次增加信号量增加成功打印成功对应的信息成功计数值加一失败的话打印失败信息失败计数值加一然后进入延时阻塞状态。
任务vReceiver的此时才有机会运行它不断减少信号量减少成功就打印成功信息成功计数值加一失败的话就打印失败信息失败计数值加一等vSender延时结束后抢占CPU继续增加信号量如此反复。 如上图所示运行结果vSender任务连续四次Giver信号量中只有前三次是成功的因为信号量的上限是3。
vReceiver在Take信号量时也值能成功三次因为信号量的计数值只到3由于它是阻塞式Take所以没有信号量后就处于阻塞状态了故而没有答应错误信息。
互斥量
拿最开始(上篇文章)中上厕所的例子来说怎么独享厕所你当然可以进去后让别人帮你把门但是命运就掌握在别人手上了。
使用队列、信号量都可以实现互斥访问以信号量为例
信号量初始值为1任务A想上厕所take信号量成功它进入厕所任务B也想上厕所take信号量不成功等待任务A用完厕所give信号量轮到任务B使用
这需要两个前提才能保证安全 任务B很老实不撬门(不give信号量)。 没有坏人别的任务不会give信号量。
前面信号量的介绍中也可以看到负责give和take信号量的任务并不是同一个。
所以说使用信号量确实可以实现互斥访问但是并不完美最完美的方式是自己开门上锁完事了自己开锁。
使用互斥量就可以解决这个问题 如上图互斥量的值只能为1或者0它也经常被称为锁也就是只有两个状态上锁和解锁。
任务A上锁后互斥量就变为0此时其他任何一个任务都无法再申请到这个锁只有任务A解锁互斥量变为1后其他任务才能申请到锁进行上锁。 它的核心在于谁上锁就只能由谁开锁。 很奇怪的是FreeRTOS的互斥锁并没有在代码上实现这点甚至是Linux也没有实现即使任务A获得了互斥锁任务B竟然也可以释放互斥锁后面在使用本喵会用代码演示。 谁上锁谁就解锁这只是一个约定需要程序员自己去维护。 在多任务系统中任务A正在使用某个资源还没用完的情况下任务B也来使用的话就可能导致问题。比如对于串口任务A正使用它来打印在打印过程中任务B也来打印客户看到的结果就是A、B的信息混杂在一起。
再比如对于同一个变量比如 int a 如果有两个任务同时写它就有可能导致问题 对于变量a的修改C代码只有一句a a 8但是实际上它分3步实现读出原数值修改数值写回到内存。
我们想让任务A、B都执行add_a函数函数执行两次最终的结果是1 8 8 17。
现在假设任务A运行完代码①在执行代码②之前被任务B抢占了现在任务A的R0等于1。任务B执行完add_a函数a等于9。
任务A继续运行在代码②处R0仍然是被抢占前的数值1执行完②③的代码a等于9这跟预期的17不符合。 修改变量、设置结构体、在16位的机器上写32位的变量这些操作都是非原子的。也就是它们的操作过程都可能被打断如果被打断的过程有其他任务来操作这些变量就可能导致冲突。 上述问题的解决方法是任务A访问全局变量、函数代码时独占它就是上个锁。这些全局变量、函数代码必须被独占地使用它们被称为临界资源。
原理 如上图使用互斥量之前需要先调用xSemaphoreCreateMutex创建互斥量其实是在调用xQueueCreateMutex它的底层会创建一个队列长度为1。
从创建互斥量函数的名字中也可以看出其实就是一个类似二进制型的信号量底层同样也是用的队列。
所以当锁被任务A申请走以后任务B就会被阻塞同样是放在队列的接收阻塞链表中当任务A归还锁后任务B才会被唤醒去申请锁。 为了独立理解互斥量就认为它是一把锁只有上锁和开锁两种状态申请锁后就上锁了归还后就开锁了。 既然是互斥量的本质就是一个二进制型的信号量那么申请锁和归还锁用的也是信号量的Take和Give方式。 互斥量也被称为互斥锁使用过程如下
互斥量初始值为1任务A想访问临界资源先获得并占有互斥量然后开始访问任务B也想访问临界资源也要先获得互斥量被别人占有了于是阻塞任务A使用完毕释放互斥量任务B被唤醒、得到并占有互斥量然后开始访问临界资源任务B使用完毕释放互斥量
正常来说在任务A占有互斥量的过程中任务B、任务C等等都无法释放互斥量。但是FreeRTOS未实现这点任务A占有互斥量的情况下任务B也可释放互斥量后面本喵会演示。
使用互斥量的函数
创建互斥量
/* 动态创建 */
SemaphoreHandle_t xSemaphoreCreateMutex( void );
/* 静态创建 */
SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t *pxMutexBuffer );pxMutexBuffer 静态创建时需要用户指定存放互斥量的内存空间。返回值用于控制互斥量的句柄。 申请锁/释放锁
/* 申请锁 */
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore,TickType_t xTicksToWait );
/*释放锁*/
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );xSemaphore互斥量的句柄。xTicksToWait 等待时间返回值成功返回pdTRUE失败返回pdFALSE。 互斥量本质上就是信号量所以互斥量的句柄类型也是SemaphoreHandle_t。
删除互斥量
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );互斥量的基本使用 如上图使用互斥锁之前需要先定义宏configUSE_MUTEXS。
一个任务上锁另一个任务解锁 如上图创建一个互斥量再创建两个任务申请锁任务优先级是2释放锁任务优先级是1优先级为2的任务先执行。 如上图Take任务先执行先申请互斥锁成功则打印成功信息后阻塞失败则直接阻塞让GiveAndTake任务有机会执行。
GiveAndTake任务执行时先尝试申请一下互斥锁成功则打印成功信息不成功则打印失败信息并且将互斥锁私自释放释放成功则打印相关信息然后再申请互斥锁成功则打印成功信息后阻塞失败后直接阻塞。 GiveAndTake尝试申请互斥锁如果不成功则监守自盗。 如上图Take申请互斥锁成功后GiveAndTake任务是无法再次申请到这个互斥锁的进而私自释放互斥锁然后再申请互斥锁从而监守自盗成功。 切记切记我们写代码时不要这么做。 两个任务使用同一个串口
仍然使用两个任务使用串口的例子虽然用二进制信号量解决了但是最恰当的还是信号量 如上图创建互斥量后不用像二进制信号量那样要Give一下进行初始化互斥量创建函数中内部已经进行了初始化。 如上图在使用串口前需要先上锁使用完毕后再解锁当任务1上锁了以后任务2是无法使用串口的。 上锁和解锁之间的代码就是临界区上锁能够保证临界区的安全让所有任务串行执行临界区代码。 如上图此时任务1和任务2在交替使用串口比如不会出现打印信息的混乱。
优先级反转
互斥锁虽然没有实现谁上锁谁解锁但是它实现了优先级继承用来解决优先级反转的问题。 如上图此时有三个任务任务A的优先级是1任务B的优先级是2任务C的优先级是3它们共同使用一把互斥锁。
任务B和任务C先阻塞一会让任务A先运行任务A在运行的过程中申请了互斥锁当任务B和任务C延时结束以后最高优先级的任务C开始运行。
任务C在运行的过程中也要申请这把锁但是锁已经被任务A申请走了任务A此时已经被切换走了进入阻塞状态它抱着锁走了。所以任务C无法申请到锁也进入阻塞状态。
此时能运行的就只有任务B了因为任务B的优先级高于任务A所以任务A始终得不到运行也就始终无法释放锁。 这就导致优先级最高的任务C因为无法申请到锁而阻塞无法运行优先级发生了反转。 如上图代码创建三个任务分别执行低优先级任务vLPTask中优先级任务vMPTask高优先级任务vHPTask也就是任务A任务B和任务C。 这里必须创建二进制信号量不能创建互斥锁因为二进制信号量没有解决优先级反转的功能才能看到优先级反转的实验现象。 如上图低优先级任务中将对应的标志位置一然后申请互斥锁之后进行长时间的运算运算完毕后归还锁。 二上图中优先级任务先延时防止抢占低优先级任务让低优先级任务先运行。 如上图代码高优先级任务一开始也进入延时防止抢占低优先级任务延时结束后申请互斥锁然后再释放互斥锁。 如上图所示运行结果低优先级任务先执行成功申请互斥锁但是还没有来得及归还高优先级任务抢占执行低优先级任务抱着锁被切走了。
高优先级任务运行后也申请锁但是申请失败所以阻塞了此时中优先级任务得到了机会始终运行。 这种现象就是优先级反转。 优先级继承
而互斥锁的成功解决了优先级反转这一问题它是通过优先级继承解决的。 如上图前面部分和优先级反转一样任务C因为无法申请到锁而进入阻塞状态。
但是使用互斥锁时短暂地将任务C的高优先级3赋给任务A此时任务A的优先级就比任务B的优先级高所以此时执行的是任务A而不是B。
直到任务A将锁归还以后立刻将任务A的高优先级回收任务A的优先级又变成了1此时任务C的优先级最高所以任务C执行。
由于任务A已经将锁归还了所以任务C可以顺利的申请到锁而继续执行下去此时就符合最高优先及的任务抢占执行的特性了。 优先级继承就是短暂提高低优先级任务到高优先级让它有机会执行归还互斥锁然后再恢复到低优先级。 如上图代码只需要将原本创建的二进制型信号量变成创建互斥量就可以解决优先级反转的问题其他代码都不用作任何改变。 如上图运行结果和我们分析的一致当高优先级任务因为低优先级任务抱着锁被切走而无法申请锁时短暂的赋予低优先级任务高优先级的权利。
原本的低优先级任务得以执行释放互斥锁后恢复到了原本的低优先级此时高优先级任务抢占执行并且申请锁成功不再阻塞从而始终执行。
在优先级继承这个过程中中优先级任务就没有机会被执行此时就完美解决了优先级反转的问题。
递归锁
假设这样的场景 任务 A 获得了互斥锁 M它又调用了一个函数这个函数函数也要去获取同一个互斥锁 M于是它阻塞此时任务 A 休眠等待任务 A来释放互斥锁 任务A自己阻塞不动了还在等着自己释放互斥锁来救自己。此时就产生了死锁 就像我们找工作的时候公司只招有工作经验的人但是我们没有工作经验又只能去找工作。 如上图代码所示创建一个互斥锁再创建一个任务在该任务中申请锁申请成功后打印一句话表示自己在运行然后调用另一个函数在这个函数中也申请这个锁申请成功也打印一句话表示该函数在执行没有申请成功就阻塞。 如上图只有新创建的任务在申请锁成功够打印了一句话它调用的另一个函数并没有打印说明这个函数申请锁失败了。
但是程序并没有运行下去此时整个任务处于阻塞状态因为在两处申请了同一把锁而且第一次申请完后还没有来得及释放这就是发生了死锁。 普通的互斥锁在FreeRTOS中发生死锁后可以由其他任务来释放锁但是不建议这么做因为它设计的初衷就是谁申请谁释放。 这样来说上面这种情况就不能有吗如果就是需要这种场景呢此时就可以使用递归锁(Recursive Mutexes)
任务A获得递归锁M后还可以多次去获得这个锁。Take了N次要GiveN次这锁才会释放。 递归锁实现了由谁上锁就必须由谁解锁其他人不能解锁。 大概原理 如上图递归锁的结构大概如上图所示它虽然也是一个互斥量但是和互斥量不同的是它还有一个专门用来记录任务TCB节点的成员以及一个记录申请锁次数的计数值。
当Task1申请到递归锁以后TCB*成员中存放的就是Task1此时Task2再来申请这个递归锁时由于和记录的TCB节点不符就会拒绝Task2申请递归锁。
对于Task1它可以多次申请递归锁每Take申请一次递归锁内部的计数值就会加一每Give释放一次该计数值就会减一。
所以Task1申请了多少次就必须释放多少次否则这个锁就一直属于Task1其他任务无法申请。
使用递归锁的函数
递归锁的函数和普通互斥锁的函数名不一样但是参数类型一样所以本喵就不介绍它的参数类型了
功能递归锁普通互斥锁创建xSemaphoreCreateRecursiveMutexxSemaphoreCreateMutex申请锁xSemaphoreTakeRecursivexSemaphoreTake释放锁xSemaphoreGiveRecursivexSemaphoreGive
可以看到递归锁的常用操作也是只有这三个在函数名上比普通互斥锁多了Recursive表示这是递归锁的函数。
使用 如上图在使用递归锁之前先要定义互斥锁的宏开关configUSE_RECURSIVE_MUTEXES。
一个任务申请递归锁另一个释放 如上图创建两个任务任务1的优先级是2任务2的优先级1开始调度后让任务1先执行。 如上图任务1先开始运行连续多次申请递归锁每申请成功一次就打印一次成功信息失败则打印失败信息当多次申请完毕后让自己进入阻塞状态让出CPU让任务2执行。
任务2开始执行后先释放任务1申请的递归锁如果释放成功则打印成功信息失败则打印失败信息之后再申请任务1的递归锁如果申请成功则打印成功信息失败打印失败信息。 如上图任务1五次申请递归锁都成功此时意味着递归锁中的计数值就成了5。任务2 释放 任务1申请的递归锁失败了之后 申请 任务1的递归锁也失败了。 谁申请的递归锁必须由谁释放。递归锁只能由一个任务申请。 解决死锁问题 如上图仍然是前面产生死锁的例子只是这里将普通互斥锁换成了递归锁将申请锁和释放锁的方式换成对应递归锁的方式其他没有变。 如上图此时就不再有死锁的情况了两个函数都可以执行OtherFunction在TaskFunction申请递归锁还没有释放的情况下又再次申请成功。
总结
信号量是基于队列构建的只负责管理数据的余量不管理数据本身互斥量是基于二进制型信号量构建的它的计数值只有0和1并且增加了一个优先级继承功能来解决优先级反转的问题。又衍生出了递归锁解决了死锁问题和监守自盗的问题。
这三种结构最底层还是队列所以它们实现同步与互斥的原理和队列是相同的。