-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #11 from xieduoyuan/master
add chapter 9-18
- Loading branch information
Showing
11 changed files
with
3,763 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,308 @@ | ||
# 第12章 互斥量(mutex) | ||
|
||
怎么独享厕所?自己开门上锁,完事了自己开锁。 | ||
|
||
你当然可以进去后,让别人帮你把门:但是,命运就掌握在别人手上了。 | ||
|
||
使用队列、信号量,都可以实现互斥访问,以信号量为例: | ||
|
||
- 信号量初始值为1 | ||
- 任务A想上厕所,"take"信号量成功,它进入厕所 | ||
- 任务B也想上厕所,"take"信号量不成功,等待 | ||
- 任务A用完厕所,"give"信号量;轮到任务B使用 | ||
|
||
这需要有2个前提: | ||
|
||
- 任务B很老实,不撬门(一开始不"give"信号量) | ||
- 没有坏人:别的任务不会"give"信号量 | ||
|
||
可以看到,使用信号量确实也可以实现互斥访问,但是不完美。 | ||
|
||
使用互斥量可以解决这个问题,互斥量的名字取得很好: | ||
|
||
- 量:值为0、1 | ||
- 互斥:用来实现互斥访问 | ||
|
||
它的核心在于:谁上锁,就只能由谁开锁。 | ||
|
||
很奇怪的是,FreeRTOS的互斥锁,并没有在代码上实现这点: | ||
|
||
- 即使任务A获得了互斥锁,任务B竟然也可以释放互斥锁。 | ||
- 谁上锁、谁释放:只是约定。 | ||
|
||
本章涉及如下内容: | ||
|
||
- 为什么要实现互斥操作 | ||
- 怎么使用互斥量 | ||
- 互斥量导致的优先级反转、优先级继承 | ||
|
||
## 12.1 互斥量的使用场合 | ||
|
||
在多任务系统中,任务A正在使用某个资源,还没用完的情况下任务B也来使用的话,就可能导致问题。 | ||
|
||
比如对于串口,任务A正使用它来打印,在打印过程中任务B也来打印,客户看到的结果就是A、B的信息混杂在一起。 | ||
|
||
这种现象很常见: | ||
|
||
- 访问外设:刚举的串口例子 | ||
- 读、修改、写操作导致的问题 | ||
|
||
对于同一个变量,比如int a,如果有两个任务同时写它就有可能导致问题。 | ||
|
||
对于变量的修改,C代码只有一条语句,比如:a=a+8;,它的内部实现分为3步:读出原值、修改、写入。 | ||
|
||
<img src="https://photos.100ask.net/renesas-docs/DShanMCU_RA6M5/FreeRTOS/chapter-12/image1.png" style="zoom:67%;" /> | ||
|
||
我们想让任务A、B都执行add_a函数,a的最终结果是1+8+8=17。 | ||
|
||
假设任务A运行完代码①,在执行代码②之前被任务B抢占了:现在任务A的R0等于1。 | ||
|
||
任务B执行完add_a函数,a等于9。 | ||
|
||
任务A继续运行,在代码②处R0仍然是被抢占前的数值1,执行完②③的代码,a等于9,这跟预期的17不符合。 | ||
|
||
- 对变量的非原子化访问 | ||
|
||
修改变量、设置结构体、在16位的机器上写32位的变量,这些操作都是非原子的。也就是它们的操作过程都可能被打断,如果被打断的过程有其他任务来操作这些变量,就可能导致冲突。 | ||
|
||
- 函数重入 | ||
|
||
"可重入的函数"是指:多个任务同时调用它、任务和中断同时调用它,函数的运行也是安全的。可重入的函数也被称为"线程安全"(thread safe)。 | ||
|
||
每个任务都维持自己的栈、自己的CPU寄存器,如果一个函数只使用局部变量,那么它就是线程安全的。 | ||
|
||
函数中一旦使用了全局变量、静态变量、其他外设,它就不是"可重入的",如果该函数正在被调用,就必须阻止其他任务、中断再次调用它。 | ||
|
||
上述问题的解决方法是:任务A访问这些全局变量、函数代码时,独占它,就是上个锁。这些全局变量、函数代码必须被独占地使用,它们被称为临界资源。 | ||
|
||
互斥量也被称为互斥锁,使用过程如下: | ||
|
||
- 互斥量初始值为1 | ||
- 任务A想访问临界资源,先获得并占有互斥量,然后开始访问 | ||
- 任务B也想访问临界资源,也要先获得互斥量:被别人占有了,于是阻塞 | ||
- 任务A使用完毕,释放互斥量;任务B被唤醒、得到并占有互斥量,然后开始访问临界资源 | ||
- 任务B使用完毕,释放互斥量 | ||
|
||
正常来说:在任务A占有互斥量的过程中,任务B、任务C等等,都无法释放互斥量。 | ||
但是FreeRTOS未实现这点:任务A占有互斥量的情况下,任务B也可释放互斥量。 | ||
|
||
## 12.2 互斥量函数 | ||
|
||
### 12.2.1 创建 | ||
|
||
互斥量是一种特殊的二进制信号量。 | ||
|
||
使用互斥量时,先创建、然后去获得、释放它。使用句柄来表示一个互斥量。 | ||
创建互斥量的函数有2种:动态分配内存,静态分配内存,函数原型如下: | ||
|
||
```c | ||
/* 创建一个互斥量,返回它的句柄。 | ||
* 此函数内部会分配互斥量结构体 | ||
* 返回值: 返回句柄,非NULL表示成功 | ||
*/ | ||
SemaphoreHandle_t xSemaphoreCreateMutex( void ); | ||
|
||
/* 创建一个互斥量,返回它的句柄。 | ||
* 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针 | ||
* 返回值: 返回句柄,非NULL表示成功 | ||
*/ | ||
SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t *pxMutexBuffer ); | ||
``` | ||
要想使用互斥量,需要在配置文件FreeRTOSConfig.h中定义: | ||
```c | ||
##define configUSE_MUTEXES 1 | ||
``` | ||
|
||
### 12.2.2 其他函数 | ||
|
||
要注意的是,互斥量不能在ISR中使用。 | ||
各类操作函数,比如删除、give/take,跟一般是信号量是一样的。 | ||
|
||
```c | ||
/* | ||
* xSemaphore: 信号量句柄,你要删除哪个信号量, 互斥量也是一种信号量 | ||
*/ | ||
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore ); | ||
|
||
/* 释放 */ | ||
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore ); | ||
|
||
/* 释放(ISR版本) */ | ||
BaseType_t xSemaphoreGiveFromISR( | ||
SemaphoreHandle_t xSemaphore, | ||
BaseType_t *pxHigherPriorityTaskWoken | ||
); | ||
|
||
/* 获得 */ | ||
BaseType_t xSemaphoreTake( | ||
SemaphoreHandle_t xSemaphore, | ||
TickType_t xTicksToWait | ||
); | ||
/* 获得(ISR版本) */ | ||
xSemaphoreGiveFromISR( | ||
SemaphoreHandle_t xSemaphore, | ||
BaseType_t *pxHigherPriorityTaskWoken | ||
); | ||
``` | ||
## 12.3 示例16: 互斥地使用LCD | ||
本节代码为:1201_mutex_lcd,主要看applications\nwatch\draw.c。 | ||
有多个任务会使用到LCD,比如car1、car2、car3,操作LCD时会用到SPI控制器。代码如下: | ||
```c | ||
01 void draw_flushArea(unsigned short *pwBuf, int x, int y, int w, int h) | ||
02 { | ||
03 static volatile int bCanUse = 1; | ||
04 while (!bCanUse); | ||
05 bCanUse = 0; | ||
06 g_ptDispDev->SetDisplayWindow(g_ptDispDev, x, y, x+w-1, y+h-1); | ||
07 g_ptDispDev->FlushBuf(g_ptDispDev, pwBuf, w*h*2); | ||
08 bCanUse = 1; | ||
09 } | ||
``` | ||
|
||
上述代码存在缺陷,比如car1任务刚执行完第5行使得bCanUse为0,表示它占用了LCD。切换到car2,car2的优先级更高,它就在第4行死循环。这时,car1无法运行,导致car2一直在第4行死循环。 | ||
|
||
我们使用互斥量解决这个问题,代码在applications\nwatch\draw.c中,如下: | ||
```c | ||
01 | ||
02 static SemaphoreHandle_t g_xLCDMutex; | ||
03 | ||
04 static void GetLCD(void) | ||
05 { | ||
06 /* 等待互斥量 */ | ||
07 xSemaphoreTake(g_xLCDMutex, portMAX_DELAY); | ||
08 } | ||
09 | ||
10 static void PutLCD(void) | ||
11 { | ||
12 /* 释放互斥量 */ | ||
13 xSemaphoreGive(g_xLCDMutex); | ||
14 } | ||
15 | ||
16 void draw_flushArea(unsigned short *pwBuf, int x, int y, int w, int h) | ||
17 { | ||
18 GetLCD(); | ||
19 g_ptDispDev->SetDisplayWindow(g_ptDispDev, x, y, x+w-1, y+h-1); | ||
20 g_ptDispDev->FlushBuf(g_ptDispDev, pwBuf, w*h*2); | ||
21 PutLCD(); | ||
22 } | ||
23 | ||
24 void draw_init(void) | ||
25 { | ||
26 g_ptDispDev = LCDGetDevice(); | ||
27 lcdBuffer = LCD_GetFrameBuffer(&g_xres, &g_yres, &g_bpp); | ||
28 g_xLCDMutex = xSemaphoreCreateMutex(); | ||
29 } | ||
``` | ||
第28行:创建互斥量。 | ||
第4、10行,实现了2个函数:GetLCD、PutLCD,分别获取、释放互斥量。 | ||
第16行的LCD操作函数里,要操作LCD需要先调用GetLCD函数,使用完毕再调用PutLCD函数。 | ||
假设car1任务刚执行完第18行成功获得了互斥量。切换到car2,car2的优先级更高,它就在第18行阻塞。这时,car1仍然可以运行,它使用完LCD释放互斥量后,car2就被唤醒继续运行了。 | ||
## 12.4 示例17: 优先级继承 | ||
本节代码为:1202_mutex_priority_inversion,主要看applications\nwatch\game2.c。 | ||
示例15的问题在于,car1低优先级任务获得了锁,但是它优先级太低而无法运行。 | ||
如果能提升car1任务的优先级,让它能尽快运行、释放锁,"优先级反转"的问题不就解决了吗? | ||
把car1任务的优先级提升到什么水平?car3也想获得同一个互斥锁,不成功而阻塞时,它会把car1的优先级提升得跟car3一样。 | ||
这就是优先级继承: | ||
● 假设持有互斥锁的是任务A,如果更高优先级的任务B也尝试获得这个锁 | ||
● 任务B说:你既然持有宝剑,又不给我,那就继承我的愿望吧 | ||
● 于是任务A就继承了任务B的优先级 | ||
● 这就叫:优先级继承 | ||
● 等任务A释放互斥锁时,它就恢复为原来的优先级 | ||
● 互斥锁内部就实现了优先级的提升、恢复 | ||
跟1201_mutex_lcd相比,在1202_mutex_priority_inversion里,创建的是互斥量,代码如下: | ||
```c | ||
01 void car_game(void) | ||
02 { | ||
03 int x; | ||
04 int i, j; | ||
05 unsigned short tmpLcdBufGame[8]; | ||
06 g_framebuffer = LCD_GetFrameBuffer(&g_xres, &g_yres, &g_bpp); | ||
07 draw_init(); | ||
08 draw_end(); | ||
09 | ||
10 //g_xSemTicks = xSemaphoreCreateCounting(1, 1); | ||
11 g_xSemTicks = xSemaphoreCreateMutex(); | ||
``` | ||
|
||
把第10行打开、第11行去掉,就是1201_mutex_lcd,它仍然有优先级反转的问题。 | ||
|
||
把第10行去掉、第11行打开,就解决了优先级反转的问题。 | ||
|
||
1202_mutex_priority_inversion的实验现象为:car1先运行一会;然后car2运行一会;接着car3任务启动,但是它无法获得互斥量而阻塞,并且提升了car1的优先级;于是:car1、car2交替运行(虽然car1的优先级高于car2,但是car1会使用vTaskDelay阻塞,car2就有机会运行了);当car1运行到终点,是否了互斥量,car3就可以运行了。 | ||
|
||
## 12.5 递归锁 | ||
|
||
### 12.5.1 死锁的概念 | ||
|
||
日常生活的死锁:我们只招有工作经验的人!我没有工作经验怎么办?那你就去找工作啊! | ||
假设有2个互斥量M1、M2,2个任务A、B: | ||
|
||
- A获得了互斥量M1 | ||
- B获得了互斥量M2 | ||
- A还要获得互斥量M2才能运行,结果A阻塞 | ||
- B还要获得互斥量M1才能运行,结果B阻塞 | ||
- A、B都阻塞,再无法释放它们持有的互斥量 | ||
- 死锁发生! | ||
|
||
### 12.5.2 自我死锁 | ||
|
||
假设这样的场景: | ||
|
||
- 任务A获得了互斥锁M | ||
- 它调用一个库函数 | ||
- 库函数要去获取同一个互斥锁M,于是它阻塞:任务A休眠,等待任务A来释放互斥锁! | ||
- 死锁发生! | ||
|
||
### 12.5.3 函数 | ||
|
||
怎么解决这类问题?可以使用递归锁(Recursive Mutexes),它的特性如下: | ||
|
||
- 任务A获得递归锁M后,它还可以多次去获得这个锁 | ||
- "take"了N次,要"give"N次,这个锁才会被释放 | ||
|
||
递归锁的函数根一般互斥量的函数名不一样,参数类型一样,列表如下: | ||
|
||
| | **递归锁** | **一般互斥量** | | ||
| ---- | ------------------------------ | --------------------- | | ||
| 创建 | xSemaphoreCreateRecursiveMutex | xSemaphoreCreateMutex | | ||
| 获得 | xSemaphoreTakeRecursive | xSemaphoreTake | | ||
| 释放 | xSemaphoreGiveRecursive | xSemaphoreGive | | ||
|
||
函数原型如下: | ||
|
||
```c | ||
/* 创建一个递归锁,返回它的句柄。 | ||
* 此函数内部会分配互斥量结构体 | ||
* 返回值: 返回句柄,非NULL表示成功 | ||
*/ | ||
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex( void ); | ||
|
||
|
||
/* 释放 */ | ||
BaseType_t xSemaphoreGiveRecursive( SemaphoreHandle_t xSemaphore ); | ||
|
||
/* 获得 */ | ||
BaseType_t xSemaphoreTakeRecursive( | ||
SemaphoreHandle_t xSemaphore, | ||
TickType_t xTicksToWait | ||
); | ||
``` |
Oops, something went wrong.