怎么独享厕所?自己开门上锁,完事了自己开锁。
你当然可以进去后,让别人帮你把门:但是,命运就掌握在别人手上了。
使用队列、信号量,都可以实现互斥访问,以信号量为例:
⚫ 信号量初始值为 1
⚫ 任务 A 想上厕所, "take"信号量成功,它进入厕所
⚫ 任务 B 也想上厕所, "take"信号量不成功,等待
⚫ 任务 A 用完厕所, "give"信号量;轮到任务 B 使用
这需要有 2 个前提:
⚫ 任务 B 很老实,不撬门(一开始不"give"信号量)
⚫ 没有坏人:别的任务不会"give"信号量
可以看到,使用信号量确实也可以实现互斥访问,但是不完美。
使用互斥量可以解决这个问题,互斥量的名字取得很好:
⚫ 量:值为 0、 1
⚫ 互斥:用来实现互斥访问
它的核心在于:谁上锁,就只能由谁开锁。
很奇怪的是, FreeRTOS的互斥锁,并没有在代码上实现这点:
⚫ 即使任务 A 获得了互斥锁,任务 B 竟然也可以释放互斥锁。
⚫ 谁上锁、谁释放:只是约定。
本文涉及如下内容:
⚫ 为什么要实现互斥操作
⚫ 怎么使用互斥量
⚫ 互斥量导致的优先级反转、优先级继承
互斥量的使用场合
在多任务系统中,任务 A 正在使用某个资源,还没用完的情况下任务 B 也来使用的话,
就可能导致问题。
比如对于串口,任务A正使用它来打印,在打印过程中任务B也来打印,客户看到的结
果就是A、 B的信息混杂在一起。
这种现象很常见:
⚫ 访问外设:刚举的串口例子
⚫ 读、修改、写操作导致的问题
对于同一个变量,比如int a,如果有两个任务同时写它就有可能导致问题。
对于变量的修改, C代码只有一条语句,比如: a=a+8;,它的内部实现分为3步:读出原值、
修改、写入。
我们想让任务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也可释放互斥量。
互斥量函数
创建
互斥量是一种特殊的二进制信号量。
使用互斥量时,先创建、然后去获得、释放它。使用句柄来表示一个互斥量。
创建互斥量的函数有2种:动态分配内存,静态分配内存,函数原型如下:
/* 创建一个互斥量,返回它的句柄。
* 此函数内部会分配互斥量结构体
* 返回值: 返回句柄,非 NULL 表示成功
*/
SemaphoreHandle_t xSemaphoreCreateMutex( void );
/* 创建一个互斥量,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个 StaticSemaphore_t 结构体,并传入它的
指针
* 返回值: 返回句柄,非 NULL 表示成功
*/
SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t *pxMutexBuffer );
要想使用互斥量,需要在配置文件FreeRTOSConfig.h中定义:
##define configUSE_MUTEXES 1
其他函数
要注意的是,互斥量不能在 ISR 中使用。
各类操作函数,比如删除、 give/take,跟一般是信号量是一样的。
/* * 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 );
示例: 互斥量基本使用
使用互斥量时有如下特点:
⚫ 刚创建的互斥量可以被成功"take"
⚫ "take"互斥量成功的任务,被称为"holder",只能由它"give"互斥量;别的任务
"give"不成功
⚫ 在 ISR 中不能使用互斥量
⚫ 本程序创建 2 个发送任务:故意发送大量的字符。 可以做 2 个实验:
⚫ 使用互斥量:可以看到任务 1、任务 2 打印的字符串没有混杂在一起
⚫ 不使用互斥量:任务 1、任务 2 打印的字符串混杂在一起
main函数代码如下:
/* 互斥量句柄 */
SemaphoreHandle_t xMutex;
int main( void )
{
prvSetupHardware();
/* 创建互斥量 */
xMutex = xSemaphoreCreateMutex( );
if( xMutex != NULL )
{
/* 创建 2 个任务: 都是打印
* 优先级相同
*/
xTaskCreate( vSenderTask, "Sender1", 1000, (void *)1, 1, NULL );
xTaskCreate( vSenderTask, "Sender2", 1000, (void *)2, 1, NULL );
/* 启动调度器 */
vTaskStartScheduler();
}
else
{
/* 无法创建互斥量 */
}
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
发送任务的函数如下:
static void vSenderTask( void *pvParameters )
{
const TickType_t xTicksToWait = pdMS_TO_TICKS( 10UL );
int cnt = 0;
int task = (int)pvParameters;
int i;
char c;
/* 无限循环 */
for( ;; )
{
/* 获得互斥量: 上锁 */
xSemaphoreTake(xMutex, portMAX_DELAY);
printf("Task %d use UART count: %d, ", task, cnt++);
c = (task == 1 ) ? 'a' : 'A';
for (i = 0; i < 26; i++)
printf("%c", c + i);
printf("\r\n");
/* 释放互斥量: 开锁 */
xSemaphoreGive(xMutex);
vTaskDelay(xTicksToWait);
}
}
可以做两个实验: vSenderTask 函数的 for 循环中 xSemaphoreTake 和 xSemaphoreGive
这 2 句代码保留、不保留
⚫ 保留:实验现象如下图左边,任务 1、任务 2 的打印信息没有混在一起
⚫ 不保留:实验现象如下图右边,打印信息混杂在一起
程序运行结果如下图所示:
示例 : 谁上锁就由谁解锁?
互斥量、互斥锁,本来的概念确实是:谁上锁就得由谁解锁。
但是FreeRTOS并没有实现这点,只是要求程序员按照这样的惯例写代码。
main函数创建了2个任务:
⚫ 任务 1:高优先级,一开始就获得互斥锁,永远不释放。
⚫ 任务 2:任务 1 阻塞时它开始执行,它先尝试获得互斥量,失败的话就监守
自盗(释放互斥量、开锁),然后再上锁
代码如下:
int main( void )
{
prvSetupHardware();
/* 创建互斥量 */
xMutex = xSemaphoreCreateMutex( );
if( xMutex != NULL )
{
/* 创建 2 个任务: 一个上锁, 另一个自己监守自盗(开别人的锁自己用)
*/
xTaskCreate( vTakeTask, "Task1", 1000, NULL, 2, NULL );
xTaskCreate( vGiveAndTakeTask, "Task2", 1000, NULL, 1, NULL );
/* 启动调度器 */
vTaskStartScheduler();
}
else
{
/* 无法创建互斥量 */
}
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
两个任务的代码和执行流程如下图所示:
⚫ A:任务 1 的优先级高,先运行,立刻上锁
⚫ B:任务 1 阻塞
⚫ C:任务 2 开始执行,尝试获得互斥量(上锁),超时时间设为 0。根据返回值
打印出:上锁失败
⚫ D:任务 2 监守自盗,开锁,成功!
⚫ E:任务 2 成功获得互斥量
⚫ F:任务 2 阻塞
可见,任务 1 上的锁,被任务 2 解开了。所以, FreeRTOS 并没有实现"谁上锁就得由谁
开锁"的功能。
示例 : 优先级反转
假设任务 A、 B 都想使用串口, A 优先级比较低:
⚫ 任务 A 获得了串口的互斥量
⚫ 任务 B 也想使用串口,它将会阻塞、等待 A 释放互斥量
⚫ 高优先级的任务,被低优先级的任务延迟,这被称为"优先级反转"(priority
inversion)
如果涉及 3 个任务,可以让"优先级反转"的后果更加恶劣。
互斥量可以通过"优先级继承",可以很大程度解决"优先级反转"的问题,这也是
FreeRTOS中互斥量和二级制信号量的差别。
本文程序使用二级制信号量来演示"优先级反转"的恶劣后果。
main函数创建了3个任务: LPTask/MPTask/HPTask(低/中/高优先级任务),代码如下:
/* 互斥量/二进制信号量句柄 */
SemaphoreHandle_t xLock;
int main( void )
{
prvSetupHardware();
/* 创建互斥量/二进制信号量 */
xLock = xSemaphoreCreateBinary( );
if( xLock != NULL )
{
/* 创建 3 个任务: LP,MP,HP(低/中/高优先级任务)
*/
xTaskCreate( vLPTask, "LPTask", 1000, NULL, 1, NULL );
xTaskCreate( vMPTask, "MPTask", 1000, NULL, 2, NULL );
xTaskCreate( vHPTask, "HPTask", 1000, NULL, 3, NULL );
/* 启动调度器 */
vTaskStartScheduler();
}
else
{
/* 无法创建互斥量/二进制信号量 */
}
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
LPTask/MPTask/HPTask三个任务的代码和运行过程如下图所示:
⚫ A: HPTask 优先级最高,它最先运行。在这里故意打印,这样才可以观察到
flagHPTaskRun 的脉冲。
⚫ HP Delay: HPTask 阻塞
⚫ B: MPTask 开始运行。在这里故意打印,这样才可以观察到 flagMPTaskRun
的脉冲。
⚫ MP Delay: MPTask 阻塞
⚫ C: LPTask 开始运行,获得二进制信号量,然后故意打印很多字符
⚫ D: HP Delay 时间到, HPTask 恢复运行,它无法获得二进制信号量,一直阻
塞等待
⚫ E: MP Delay 时间到, MPTask 恢复运行,它比 LPTask 优先级高,一直运
行。导致 LPTask 无法运行,自然无法释放二进制信号量,于是 HPTask 用于无法
运行。
⚫ 总结:
⚫ LPTask 先持有二进制信号量,
⚫ 但是 MPTask 抢占 LPTask,使得 LPTask 一直无法运行也就无法释放信号
量,
⚫ 导致 HPTask 任务无法运行
⚫ 优先级最高的 HPTask 竟然一直无法运行!
示例 : 优先级继承
上个示例的问题在于, LPTask低优先级任务获得了锁,但是它优先级太低而无法运行。
如果能提升LPTask任务的优先级,让它能尽快运行、释放锁, "优先级反转"的问题不就
解决了吗?
把LPTask任务的优先级提升到什么水平?
优先级继承:
⚫ 假设持有互斥锁的是任务 A,如果更高优先级的任务 B 也尝试获得这个锁
⚫ 任务 B 说:你既然持有宝剑,又不给我,那就继承我的愿望吧
⚫ 于是任务 A 就继承了任务 B 的优先级
⚫ 这就叫:优先级继承
⚫ 等任务 A 释放互斥锁时,它就恢复为原来的优先级
⚫ 互斥锁内部就实现了优先级的提升、恢复
在上个示例代码上做了一些简单修改:
int main( void )
{
prvSetupHardware();
/* 创建互斥量/二进制信号量 */
//xLock = xSemaphoreCreateBinary( );
xLock = xSemaphoreCreateMutex( )
运行时序图如下图所示:
⚫ A: HPTask 执行 xSemaphoreTake(xLock, portMAX_DELAY);,它的优先
级被 LPTask 继承
⚫ B: LPTask 抢占 MPTask,运行
⚫ C: LPTask 执行 xSemaphoreGive(xLock);,它的优先级恢复为原来值
⚫ D: HPTask 得到互斥锁,开始运行
⚫ 互斥锁的"优先级继承",可以减小"优先级反转"的影响
递归锁
死锁的概念
日常生活的死锁:我们只招有工作经验的人!我没有工作经验怎么办?那你就去找工作
啊!
假设有2个互斥量M1、 M2, 2个任务A、 B:
⚫ A 获得了互斥量 M1
⚫ B 获得了互斥量 M2
⚫ A 还要获得互斥量 M2 才能运行,结果 A 阻塞
⚫ B 还要获得互斥量 M1 才能运行,结果 B 阻塞
⚫ A、 B 都阻塞,再无法释放它们持有的互斥量
⚫ 死锁发生!
自我死锁
假设这样的场景:
⚫ 任务 A 获得了互斥锁 M
⚫ 它调用一个库函数
⚫ 库函数要去获取同一个互斥锁 M,于是它阻塞:任务 A 休眠,等待任务 A
来释放互斥锁!
⚫ 死锁发生!
函数
怎么解决这类问题?可以使用递归锁(Recursive Mutexes),它的特性如下:
⚫ 任务 A 获得递归锁 M 后,它还可以多次去获得这个锁
⚫ "take"了 N 次,要"give"N 次,这个锁才会被释放
递归锁的函数根一般互斥量的函数名不一样,参数类型一样,列表如下:
函数原型如下:
/* 创建一个递归锁,返回它的句柄。
* 此函数内部会分配互斥量结构体
* 返回值: 返回句柄,非 NULL 表示成功
*/
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex( void );
/* 释放 */
BaseType_t xSemaphoreGiveRecursive( SemaphoreHandle_t xSemaphore );
/* 获得 */
BaseType_t xSemaphoreTakeRecursive(
SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait
);
示例: 递归锁
递归锁实现了:谁上锁就由谁解锁。
⚫ 任务 1:高优先级,一开始就获得递归锁,然后故意等待很长时间,让任务 2运行
⚫ 任务 2:低优先级,看看能否操作别人持有的锁
main函数代码如下:
/* 递归锁句柄 */
SemaphoreHandle_t xMutex;
int main( void )
{
prvSetupHardware();
/* 创建递归锁 */
xMutex = xSemaphoreCreateRecursiveMutex( );
if( xMutex != NULL )
{
/* 创建 2 个任务: 一个上锁, 另一个自己监守自盗(看看能否开别人的锁自己用)
*/
xTaskCreate( vTakeTask, "Task1", 1000, NULL, 2, NULL );
xTaskCreate( vGiveAndTakeTask, "Task2", 1000, NULL, 1, NULL );
/* 启动调度器 */
vTaskStartScheduler();
}
else
{
/* 无法创建递归锁 */
}
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
两个任务经过精细设计,代码和运行流程如下图所示:
⚫ A:任务 1 优先级最高,先运行,获得递归锁
⚫ B:任务 1 阻塞,让任务 2 得以运行
⚫ C:任务 2 运行,看看能否获得别人持有的递归锁:不能
⚫ D:任务 2 故意执行"give"操作,看看能否释放别人持有的递归锁:不能
⚫ E:任务 2 等待递归锁
⚫ F:任务 1 阻塞时间到后继续运行,使用循环多次获得、释放递归锁
⚫ 递归锁在代码上实现了:谁持有递归锁,必须由谁释放。
程序运行结果如下图所示:
常见问题
使用互斥量的两个任务是相同优先级时的注意事项。