欢迎友友指正🤗🤗🤗,有输出一起进步~
复习一下rtos理论(自己的感悟结合韦东山老师部分讲解)
先说重难点,方便已有基础的同学快速联想(前置知识可以划到下面“前言”部分开始)
互斥量
本质是特殊的二值信号量
由于使用信号量确实也可以实现互斥访问,但是不完美。这是二值信号量做互斥,和 Mutex 互斥量不一样。
(二值信号量实现互斥的缺陷:
1·、没有【所有权】,任何人都可以释放信号量(致命)
——Mutex(互斥量):谁拿锁,只能谁解锁。
——二值信号量:任意任务 / 中断都可以调用 Give 直接把信号量释放。
2、会出现优先级反转,没有优先级继承机制(最经典缺点)
——后文细说;
3、不支持递归锁(可重入),如果同一个任务,已经拿到信号量,再次 Take:直接阻塞卡死自己。
——Mutex 可以支持递归互斥锁(Recursive Mutex),同一个任务多次 take,计数叠加,需要同等次数 give 释放。二值信号量做不到。)
优先级反转
前置知识:
阻塞不是“一个全局状态”。每个内核对象(信号量、队列、任务通知)都有自己独立的等待链表,不属于全局就绪队列,阻塞就是任务在等待一个特定事件,离开全局就绪链表,住在上面这些私有链表其中里。调度器不可见。
全局就绪队列:所有“已经准备好了,只要有CPU就能跑,不等任何特定事件”的任务。调度器每次选任务,只在这里面挑选最高优先级的。
各个对象的私有等待链表:任务“自愿在这里休眠,专门等这个对象的事件”,已经离开就绪队列,调度器看不见它们;
——————————————————————————————————————————
简单说:两个任务,高优先级的因为低优先级的任务持有信号量而被阻塞;
但多数情况是多个任务,拿三任务举例:
(存在一个中优先级任务不依赖那个被占用的信号量/锁)
1、系统刚跑,只有TaskL就绪,TaskH、TaskM(中优先级)都还没触发;
TaskL开始运行,TaskL 成功 xSemaphoreTake(xSem,portMAx_DELAY),拿到信号量,开始操作手保护的共享资源;
此时:xSem=0(资源被占用);
TaskL继续在CPU上跑。TaskH突然因为外部中断/定时器中断/其他任务通知/队列收数据...而就绪,调度器看到就绪列表里的高优先级任务比这个L任务高,立刻抢占了,把低优先级任务踢到就绪队列里,CPU交给TaskH——问题是高优先级拿不到这个信号量take不到,TaskH被踢出了,而不是被抢占踢回就绪队列,是直接踢到阻塞列表,等待信号量;
2、关键来了——此时就绪队列里最高的是TaskM(不需要信号量/锁),在TaskL前进入占据CPU不放,TaskL没办法进去继续运行到释放信号量的时候了,没有机会了。——TaskM,TaskH反而被卡住。
解决办法:
互斥锁(互斥量Mutex)——附带优先级继承效果
所有权:谁 take,只能谁 give(谁上锁,就只能由谁开锁。二值信号量没有这个)
上面的优先级反转情形,如果能提升TaskL任务的优先级,让它能尽快运行、释放锁,"优先级反转"的问题不就解决了吗?等TaskL执行完后释放互斥锁,就恢复优先级——级优先级继承。
创建
/* 创建一个互斥量,返回它的句柄。 * 此函数内部会分配互斥量结构体 * 返回值: 返回句柄,非NULL表示成功 */ SemaphoreHandle_t xSemaphoreCreateMutex( void ); /* 创建一个互斥量,返回它的句柄。 * 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针 * 返回值: 返回句柄,非NULL表示成功 */ SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t *pxMutexBuffer );要想使用互斥量,需要在配置文件FreeRTOSConfig.h中定义:
#define configUSE_MUTEXES 1
注意:二级制信号量初始值是0,创建后需要Give一次;互斥量初始值是1,创建后不需要Give一次。
要注意的是,互斥量不能在ISR中使用。
递归锁
1.死锁的概念
什么情况下会造成死锁,例如,线程 A 持有锁 L1 并请求锁 L2,而线程 B 持有锁 L2 并请求锁 L1,这种情况可能导致死锁。所以我们编写程序时要注意,不要写成死锁的情况,等以后写一些大型的代码,需要同时操控几个互斥量时,可能会犯这种错误。
假设有2个互斥量M1、M2,2个任务A、B:
A获得了互斥量M1
B获得了互斥量M2
A还要获得互斥量M2才能运行,结果A阻塞
B还要获得互斥量M1才能运行,结果B阻塞
A、B都阻塞,再无法释放它们持有的互斥量
死锁发生!
2.自我死锁
假设这样的场景:
任务A获得了互斥锁M
它调用一个库函数
库函数要去获取同一个互斥锁M,于是它阻塞:任务A休眠,等待任务A来释放互斥锁!
死锁发生!
一个任务申请锁后调用其他函数时申请同一个锁,导致要释放锁的任务阻塞,从而无法释放锁。
——
怎么解决这类问题?可以使用递归锁(Recursive Mutexes),它的特性如下:
递归锁实现了:谁上锁就由谁解锁。
任务A获得递归锁M后,它还可以多次去获得这个锁
"take"了N次,要"give"N次,这个锁才会被释放
递归锁的函数根一般互斥量的函数名不一样,参数类型一样,列表如下:
| 递归锁 | 一般互斥量 | |
| 创建 | xSemaphoreCreateRecursiveMutex | xSemaphoreCreateMutex |
| 获得 | xSemaphoreTakeRecursive | xSemaphoreTake |
| 释放 | xSemaphoreGiveRecursive | xSemaphoreGive |
注意:要使用递归锁,需要定义配置项configUSE_RECURSIVE_MUTEXES!!!
——————————————————————————————————————
什么时候用二值信号量做互斥,什么时候用 Mutex:
✅ 保护共享资源、临界区 → 优先用Mutex互斥量
✅ 任务间事件通知(比如中断通知任务收到数据)→ 用二值信号量
=======================================================================
前言
队列(queue)可以用于传输数据:在任务之间、任务和中断之间。
有时候我们只需要传递状态,并不需要传递具体的信息,比如:
我的事做完了,通知一下你
卖包子了、卖包子了,做好了1个包子!做好了2个包子!做好了3个包子!
这个停车位我占了,你们只能等着
这种情况下我们只需要维护一个数值而且不需要复制数据,使用信号量效率更高、它更节省内存。是可以用来保证两个或多个关键代 码段不被并发调用。——信号量也是队列。
当我们的量只有0和1的时候,它就可以被称作二值信号量,只有两个状 态,当我们的那个量没有限制的时候,它就可以被称作为计数型信号量。
信号量函数
创建
使用信号量之前,要先创建,得到一个句柄;使用信号量时,要使用句柄来表明使用哪个信号量。 对于二进制信号量、计数型信号量,它们的创建函数不一样:
创建二进制信号量的函数原型如下:
/* 创建一个二进制信号量,返回它的句柄。 * 此函数内部会分配信号量结构体 * 返回值: 返回句柄,非NULL表示成功 */ SemaphoreHandle_t xSemaphoreCreateBinary( void ); /* 创建一个二进制信号量,返回它的句柄。 * 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针 * 返回值: 返回句柄,非NULL表示成功 */ SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t *pxSemaphoreBuffer );创建计数型信号量的函数原型如下:
/* 创建一个计数型信号量,返回它的句柄。 * 此函数内部会分配信号量结构体 * uxMaxCount: 最大计数值 * uxInitialCount: 初始计数值 * 返回值: 返回句柄,非NULL表示成功 */ SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount); /* 创建一个计数型信号量,返回它的句柄。 * 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针 * uxMaxCount: 最大计数值 * uxInitialCount: 初始计数值 * pxSemaphoreBuffer: StaticSemaphore_t结构体指针 * 返回值: 返回句柄,非NULL表示成功 */ SemaphoreHandle_t xSemaphoreCreateCountingStatic( UBaseType_t uxMaxCount, UBaseType_t uxInitialCount, StaticSemaphore_t *pxSemaphoreBuffer );删除
对于动态创建的信号量,不再需要它们时,可以删除它们以回收内存。
通用:
/* * xSemaphore: 信号量句柄,你要删除哪个信号量 */ void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );获得与释放信号量
二进制信号量、计数型信号量的give、take操作函数是一样的。这些函数也分为2个版本:给任务使 用,给ISR使用。列表如下:
参数