FreeRTOS学习笔记(3、信号量、互斥量的使用)

前言

这是第三弹,由于CSDN长度的限制,所以把FreeRTOS学习分为几部分来发,这是第三部分


主要包括信号量、互斥量使用

往期学习笔记链接

第一弹FreeRTOS学习笔记(1、FreeRTOS初识、任务的创建以及任务状态理论、调度算法等)
第二弹: FreeRTOS学习笔记(2、同步与互斥通信、队列、队列集的使用)
第三弹: FreeRTOS学习笔记(3、信号量、互斥量的使用)
第四弹: FreeRTOS学习笔记(4、事件组、任务通知)
第五弹: FreeRTOS学习笔记(5、定时器、中断管理、调试与优化)

学习工程

所有学习工程
oufen / FreeRTOS学习
都在我的Gitee工程当中,大家可以参考学习

信号量 semaphore

队列可以传送数据,队列可以传送不同的数据

有的时候只需要传递状态,并不需要传递具体的信息

这就是信号量,不去传送数据,而是传送状态,这样至起到了通知的作用,更加节省内存

信号量,不能传输数据,只有一个计数值,来表示资源的数量


信号起通知作用

量,表示资源的数量

左边是生产者,生产好一个商品后让计数值+1
右边是消费者,取出一个商品后计数值-1

如何创建信号量

  • 创建信号量

  • 生产者生产好后让计数值+1 give

  • 消费者取出后让计数值-1 take

计数:事件产生give信号量,计数值加1,处理事件,take信号量,计数值减1
资源管理:想要访问资源时,首先需要take信号量,计数值减1,用完资源后,give信号量,计数值加1

两种信号量的对比

  • 计数型信号量

  • 二进制信号量

计数型信号量,的取值范围为0-任意数

二进制信号量,的取值返回为0或者1 但是二进制信号量的初始值为0

除了取值不一样外,其他的操作都是完全一样的

信号量也相当于是一个队列

队列有一个结构体Queue,结构体中有一个指针,指向存放数据的一个buff

但是对于信号量,并不需要这个buffer,只需要这个结构体

对于信号量,核心是信号量的计数值
这个计数值保存在初始化信号量时传入的初始的计数值

创建完信号量后,就可以加减信号量的Value了
让信号量的计数值+1,并且取出东西

一开始Value为0,调用take函数,使信号量减1,没有数据,那么信号量就没办法-1,进入阻塞状态,还可以指定阻塞多长时间
在阻塞状态中,如果有另一个task往里面放数据,那么就会从阻塞状态中唤醒,进入Ready状态

  1. 对于give,信号量加1 解锁

对于计数型信号量,可以让这个值累加,但是不能超过创建时指定的最大值

对于二进制信号量,取值就只有0和1,如果值为1,再次调用give也不会成功
可以判断give函数的返回值,看累加是否成功

不管哪种信号量,只要没有超过创建时指定的最大值,都可以累加成功


  1. 对于take,信号量-1 上锁

如果信号量的值为0,就没办法take成功,不成功的话可以指定阻塞时间

  • 0 take不成功,返回err

  • portMax_Delay 一直阻塞,直到成功

  1. 有多个task执行take,当其他task执行give时,唤醒哪个任务?

  • 优先级最高的task 优先执行take

  • 优先级相同时,等待最久的task 优先执行take

pdTRUE,表示Take成功

小问题:
使用队列也可以实现同步,为什么还要使用信号量呢?

  • 使用队列可以传递数据,数据的保存需要空间

  • 使用信号量时不需要传递数据,更加节省空间

  • 使用信号量时不需要复制数据,效率更高

信号量的使用

使用信号量时,先创建,然后去添加资源,获得资源,使用句柄来表示一个信号量

需要定义这两个宏

#define configSUPPORT_DYNAMIC_ALLOCATION 1 /_信号量相关宏_/
#define configUSE_COUNTING_SEMAPHORES 1


1、创建信号量

初始值为0 信号量计数值 最大值为10



2、give




3、take




4、删除信号量

对于动态创建的信号量,如果不使用,不再需要时,可以删除他们以回收内存



使用计数型信号量实现同步功能


虽然实现了同步功能,但是对于数据的完整性,需要我们自己来做



使用二进制型信号量实现互斥功能


注意了二进制型信号量初始值为0
所以创建二进制信号量时需要,手动give一下,否则take时会一直卡在阻塞状态


task3和task4独占的使用串口


创建二进制信号量来实现互斥



task3和task4实现了串口的独占使用,即实现互斥功能



互斥量 mutex

互斥量是一个特殊的二进制信号量

任务A访问这些全局变量、函数代码时,独占它,就是上个锁。这些全局变
量、函数代码必须被独占地使用,它们被称为临界资源

互斥量,就是用来保护临界资源,大家互斥的使用这些资源



二进制信号量也能实现互斥



当出现一种情况

TaskA获得信号量,计数值-1,此时二进制信号量为0
打印数据
此时TaskC运行另一个函数,give信号量,计数值+1,此时二进制信号量为1
此时TaskB从阻塞状态,进入Ready态
也能打印数据


此时串口被两个Task使用,就不是独占关系,不是互斥,对临界资源进行使用

本来应该是TaskA上锁(获得信号量,使信号量的计数值为0)
打印完数据后,应该由TaskA自己解锁
可是其他任务帮TaskA解锁,造成串口不是独占使用


要解决这样的问题,就应该是谁上锁,谁来解锁
二进制信号量并不能保证,谁上锁,谁解锁
虽然互斥量也不能保证


但是互斥量可以解决


  • 优先级反转
  • 解决递归上锁/解锁的问题

如何实现谁上锁,谁解锁

  • 上锁、解锁代码成对出现
  • 在临界代码中(想要某种资源被独占使用),不要解锁

问题:优先级反转

什么是优先级反转?
A/B/C的优先级分别是 1 2 3
A先运行,获得了锁,此时进入阻塞状态
B优先级比A高,抢占进入Running状态
由于A已经使用了锁,所以进入阻塞状态
C的优先级比B高,此时C运行,也想获得锁,因为A已经使用了锁,所以C进入阻塞状态
此时优先级高的Task优先执行,轮到B运行
在B运行过程中,一直没有放弃CPU资源,此时A不能执行

在这种情况下,C的优先级最高,A的优先级最低,结果优先级最高的C被B抢占了

优先级高的程序反而不能执行,这就是优先级反转



解决方法:优先级继承

解决优先级反转的方法就是使用优先级继承


什么是优先级继承?


在C获得锁Take,因为锁被A上锁了,所以进入阻塞状态,进入阻塞状态的同时会进行优先级继承
此时A的优先级变成了C的优先级,A继承了C的优先级
此时A的优先级变为了3,所以C阻塞后,A开始运行
A对锁进行解锁,unlock,释放互斥量,A的优先级又变成了原来的优先级1
然后轮到C来执行


这个过程中C的优先级并没有被B来反转,优先级继承解决了上述优先级反转的问题


优先级继承的好处在于提升优先级,如果C的优先级比A的还低,就没有继承的必要

问题:递归上锁造成死锁


这是自我死锁


TaskA运行,上锁后,信号量计数值为0,打印数据
进入xxxlib函数,再次上锁,因为信号量计数值为0,无法继续上锁,所以进入阻塞状态
进入阻塞状态,没有办法解锁,造成了死锁



解决方法:递归锁

递归锁是互斥量的另外一种形式

在上锁了之后,还可以二次上锁,但是二次上锁后要解锁,否则会进入阻塞状态
一次上锁对应一次解锁


此时B来上锁,就会进入阻塞状态



互斥量分为两种

  • 普通的互斥量
    • 具有优先级继承的功能
  • 递归锁
    • 除了具有优先级继承的功能外
    • 递归的功能

互斥量的基本使用

1、创建互斥量

二进制型信号量的初始值为0,所以创建时需要手动give释放一下,计数值+1,否则take时,计数值无法减1,将会发生阻塞

而互斥量的初始值为1,创建后不需要Give一次



创建互斥量时还需要配置宏




/_互斥量相关宏_/
#define configUSE_MUTEXES 1

2、获得互斥量 Take


3、释放互斥量 Give



使用优先级继承来实现优先级反转

二进制信号量优先级反转的过程分析

此时是二进制型信号量



下图就是优先级反转的例子




根据波形图对
优先级反转详细说明








互斥量使用优先级继承来解决优先级反转




互斥量和二进制信号量的区别和共同点


  1. 互斥量初始值为1

  2. 二进制信号量初始值为0

  3. Give/Take函数完全一样

  4. 互斥量具有优先级继承的功能


互斥量的递归锁

互斥量,本意是谁持有,谁释放

但是FreeRTOS没有实现这一点

A持有,B也可以释放


但是互斥量的递归锁实现了


谁持有,就有谁释放
递归上锁和解锁


一般的互斥量,并没有实现,谁持有,就由谁释放



递归锁实现


1、创建递归锁



递归锁实现,要首先配置相关宏



FreeRTOS为了减小程序的体积,使用某些功能时,首先需要配置


2、Give/Take

和信号量不同的是Give/Take的函数发生改变

同时递归锁能够实现谁上锁,谁解锁的功能




递归锁可以让task互斥使用串口

递归锁实现了谁持有,就由谁来释放

递归锁内部会记录持有者,对于持有递归锁的task,可以循环的使用上锁,开锁