Skip to content

Commit

Permalink
Merge pull request #11 from xieduoyuan/master
Browse files Browse the repository at this point in the history
add chapter 9-18
  • Loading branch information
YobeZhou authored Oct 20, 2023
2 parents 0f364c5 + c916391 commit 244042c
Show file tree
Hide file tree
Showing 11 changed files with 3,763 additions and 0 deletions.
10 changes: 10 additions & 0 deletions docs/.vuepress/configs/sidebar/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,16 @@ export const sidebarZh: SidebarConfig = {
'/zh/DShanMCU_RA6M5/FreeRTOS/chapter6.md',
'/zh/DShanMCU_RA6M5/FreeRTOS/chapter7.md',
'/zh/DShanMCU_RA6M5/FreeRTOS/chapter8.md',
'/zh/DShanMCU_RA6M5/FreeRTOS/chapter9.md',
'/zh/DShanMCU_RA6M5/FreeRTOS/chapter10.md',
'/zh/DShanMCU_RA6M5/FreeRTOS/chapter11.md',
'/zh/DShanMCU_RA6M5/FreeRTOS/chapter12.md',
'/zh/DShanMCU_RA6M5/FreeRTOS/chapter13.md',
'/zh/DShanMCU_RA6M5/FreeRTOS/chapter14.md',
'/zh/DShanMCU_RA6M5/FreeRTOS/chapter15.md',
'/zh/DShanMCU_RA6M5/FreeRTOS/chapter16.md',
'/zh/DShanMCU_RA6M5/FreeRTOS/chapter17.md',
'/zh/DShanMCU_RA6M5/FreeRTOS/chapter18.md',
],
},
],
Expand Down
780 changes: 780 additions & 0 deletions docs/zh/DShanMCU_RA6M5/FreeRTOS/chapter10.md

Large diffs are not rendered by default.

410 changes: 410 additions & 0 deletions docs/zh/DShanMCU_RA6M5/FreeRTOS/chapter11.md

Large diffs are not rendered by default.

308 changes: 308 additions & 0 deletions docs/zh/DShanMCU_RA6M5/FreeRTOS/chapter12.md
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
);
```
Loading

0 comments on commit 244042c

Please sign in to comment.