1.1同步互斥的概念:
什么同步,只有一个厕所你在上厕所,我在等待你上完厕所,你上厕所和我等待就是同步
什么是互斥,你在上厕所我也要上厕所,但是你不让我用厕所,这就是互斥。
同步和互斥经常在一起说,可以用同步来实现互斥,比如你上厕所我等。
同一时间只能有一个人使用的资源,被称为临界资源。比如任务A、B都要使用串口来打印,串口就是临界资源。如果A、B同时使用串口,那么打印出来的信息就是A、B混杂,无法分辨。所以使用串口时,应该是这样:A用完,B再用;B用完,A再用。
要想实现真正的同步互斥并不简单,我们需要用队列,互斥量,信号量,任务通知这几个技能。
1.2用变量来实现互斥
在裸机程序里,可以使用一个全局变量或静态变量实现互斥操作,比如要互斥地使用LCD,可以使用如下代码:
int LCD_PrintString(int x, int y, char *str)
{
static int bCanUse = 1;
if (bCanUse)
{
bCanUse = 0;
/* 使用LCD */
bCanUse = 1;
return 0;
}
return -1;
}
但是在RTOS里,使用上述代码实现互斥操作时,大概率是没问题的,但是无法确保万无一失。
假设如下场景:有两个任务A、B都想调用LCD_PrintString,任务A执行到第4行代码时发现bCanUse为1,可以进入if语句块,它还没执行第6句指令就被切换出去了;然后任务B也调用LCD_PrintString,任务B执行到第4行代码时也发现bCanUse为1,也可以进入if语句块使用LCD。在这种情况下,使用静态变量并不能实现互斥操作。
上述例子中,是因为第4、第6两条指令被打断了,那么如下改进:在函数入口处先然让bCanUse减一。这能否实现万无一失的互斥操作呢?
int LCD_PrintString(int x, int y, char *str)
{
static int bCanUse = 1;
bCanUse--;
if (bCanUse == 0)
{
/* 使用LCD */
bCanUse++;
return 0;
}
else
{
bCanUse++;
return -1;
}
}
把第4行的代码使用汇编指令表示如下:
04.1 LDR R0, [bCanUse] // 读取bCanUse的值,存入寄存器R0
04.2 DEC R0, #1 // 把R0的值减一
04.3 STR R0, [bCanUse] // 把R0写入变量bCanUse
假设如下场景:有两个任务A、B都想调用LCD_PrintString,任务A执行到第04.1行代码时读到的bCanUse为1,存入寄存器R0就被切换出去了;然后任务B也调用LCD_PrintString,任务B执行到第4行时发现bCanUse为1并把它减为0,执行到第5行代码时发现条件成立可以进入if语句块使用LCD,然后任务B也被切换出去了;现在任务A继续运行第04.2行代码时R0为1,运行到第04.3行代码时把bCanUse设置为0,后续也能成功进入if的语句块。在这种情况下,任务A、B都能使用LCD。
上述方法不能保证万无一失的原因在于:在判断过程中,被打断了。如果能保证这个过程不被打断,就可以了:通过关闭中断来实现。
示例1的代码改进如下:在第5~7行前关闭中断。
int LCD_PrintString(int x, int y, char *str)
{
static int bCanUse = 1;//就算bCanUse
disable_irq();//先关闭中断,防止切换任务
if (bCanUse)//如果没有任务在使用LCD_PrintString
{
bCanUse = 0;//进来后其他任务想要执行
enable_irq();
/* 使用LCD */
bCanUse = 1;
return 0;
}
enable_irq();
return -1;
}
示例2的代码改进如下:在第5行前关闭中断。
int LCD_PrintString(int x, int y, char *str)
{
static int bCanUse = 1;
disable_irq();//先关闭中断再变量--,防止在变量减的过程中被切换
bCanUse--;//在互斥变量改变前关中断,之后开中断
enable_irq();
if (bCanUse == 0)//若果没有任务使用lcd
{
/* 使用LCD */
bCanUse++;
return 0;
}
else
{
disable_irq();
bCanUse++;//在互斥变量改变前关中断,之后开中断
enable_irq();
return -1;
}
}
但是关闭中断的实现当任务切换出去之后有可能另一个任务在等待,还是占用了CPU资源。存在缺陷
注:我们在使用全局变量实现同步互斥时,不要让编译器去优化这个全局变量,如果优化这个全局变量在另一个任务中改变,在另一个任务中读取,如果优化的话系统每次都在读取这个变量寄存器的旧值而不会在内存中更新数值。
使用变量达到共享资源互斥避免方法:
在任务a使用时关闭中断,使用完成后再开启中断
也可以使用rtos提供的函数(这个之后一步步讲)
1.3专业实现同步互斥的方法!
能实现同步、互斥的内核方法有:任务通知(task notification)、队列(queue)、事件组(event group)、信号量(semaphoe)、互斥量(mutex)。
它们都有类似的操作方法:获取/释放、阻塞/唤醒、超时。比如:
- 任务A获取资源,用完后任务A释放资源
- 任务A获取不到资源则阻塞,任务B释放资源并把任务A唤醒
- 任务A获取不到资源则阻塞,并定个闹钟;A要么超时返回,要么在这段时间内因为任务B释放资源而被唤醒。
这些内核对象五花八门,记不住怎么办?我也记不住,通过对比的方法来区分它们。
- 能否传信息?还是只能传递状态?
- 为众生(所有任务都可以使用)?只为你(只能指定任务使用)?
- 我生产,你们消费?
- 我上锁,只能由我开锁
内核对象 | 生产者 | 消费者 | 数据/状态 | 说明 |
---|---|---|---|---|
队列 | ALL | ALL | 数据:若干个数据 谁都可以往队列里扔数据, 谁都可以从队列里读数据 | 用来传递数据, 发送者、接收者无限制, 一个数据只能唤醒一个接收者 |
事件组 | ALL | ALL | 多个位:或、与 谁都可以设置(生产)多个位, 谁都可以等待某个位、若干个位 | 用来传递事件, 可以是N个事件, 发送者、接受者无限制, 可以唤醒多个接收者:像广播 |
信号量 | ALL | ALL | 数量:0~n 谁都可以增加一个数量, 谁都可消耗一个数量 | 用来维持资源的个数, 生产者、消费者无限制, 1个资源只能唤醒1个接收者 |
任务通知 | ALL | 只有我 | 数据、状态都可以传输, 使用任务通知时, 必须指定接受者 | N对1的关系: 发送者无限制, 接收者只能是这个任务 |
互斥量 | 只能A开锁 | A上锁 | 位:0、1 我上锁:1变为0, 只能由我开锁:0变为1 | 就像一个空厕所, 谁使用谁上锁, 也只能由他开锁 |
使用图形对比如下:
-
队列:
- 里面可以放任意数据,可以放多个数据
- 任务、ISR都可以放入数据;任务、ISR都可以从中读出数据
- 把队列想象成传送带,工人在上面放产品(写数据),消费者取产品(读数据)
-
事件组:
- 一个事件用一bit表示,1表示事件发生了,0表示事件没发生
- 可以用来表示事件、事件的组合发生了,不能传递数据
- 有广播效果:事件或事件的组合发生了,等待它的多个任务都会被唤醒
- 每一位代表一个事件,任务A把该做的事情做完,它就把代表事件的状态那个特定位置1,其他任务就可以根据这一位来判断事件状态来做特定的事(等待还是执行),这一位是可以让多个任务来判断的。
-
信号量:
- 核心是"计数值"
- 任务、ISR释放信号量时让计数值加1
- 任务、ISR获得信号量时,让计数值减1
- 有个饺子馆,每生产一个饺子计数值加1,消费者每吃一个饺子计数值减一,如果这个计数值只有0和1那这个信号量就变回互斥量。
-
任务通知:
- 核心是任务的TCB里的数值
- 会被覆盖
- 发通知给谁?必须指定接收任务
- 只能由接收任务本身获取该通知
-
互斥量:
- 数值只有0或1
- 谁获得互斥量,就必须由谁释放同一个互斥量
- 用来保护临界资源(共享资源比如屏幕,串口等),使用时互斥量会引入其他问题,比如优先级翻转,提出优先级继承来解决。
他们具体是干什么的我们之后慢慢讲解!