9 互斥量
互斥量(Mutex)笔记
1. 什么是互斥量
在 FreeRTOS 中,互斥量(Mutex) 是一种专门用于保护共享资源的同步机制。
“互斥”两个字的核心含义是:
同一时刻,只允许一个任务访问某个共享资源。
你可以把互斥量理解成一把“锁”:
- 谁先拿到这把锁,谁就可以访问共享资源
- 其他任务如果也想访问,就必须先等待
- 等当前任务用完资源并释放锁之后,其他任务才能继续竞争这把锁
2. 为什么需要互斥量
在多任务系统中,多个任务可能会同时访问同一个资源,例如:
- 串口
- I2C 总线
- SPI 总线
- LCD 显示器
- 全局变量
- 文件系统
- 某个硬件设备
如果不加保护,就可能出现以下问题:
- 数据被交叉覆盖
- 输出内容混乱
- 设备操作冲突
- 程序逻辑异常
- 系统行为不可预测
所以,互斥量的作用就是:
避免多个任务同时访问同一个共享资源,从而保证资源访问的安全性和完整性。
3. 互斥量的本质
互斥量本质上也是一种同步对象,但它和普通信号量的用途不同。
它主要不是用来“通知事件发生了”,而是用来:
给共享资源上锁和解锁。
因此,互斥量最典型的使用流程是:
- 任务先申请互斥量
- 拿到互斥量后进入临界区
- 操作共享资源
- 操作完成后释放互斥量
4. 什么是临界区
所谓临界区,就是那段访问共享资源的代码。
例如:
printf("task running...\n");如果多个任务都可能执行这条输出语句,而底层串口又是共享的,那么这段代码就可以看成临界区。
如果不加保护,可能输出变成:
taask runnning...也就是多个任务的输出互相打断、交叉混合。
因此,临界区需要互斥保护。
5. 互斥量的核心作用
5.1 保护共享资源
这是互斥量最主要的用途。
例如:
- 多个任务访问串口打印
- 多个任务访问同一个传感器
- 多个任务修改同一个全局变量
互斥量可以保证:
同一时刻只有一个任务进入资源访问代码。
5.2 防止竞争条件
多个任务同时读写共享变量时,可能会产生竞争条件(Race Condition)。
例如:
count = count + 1;这条语句看起来很简单,但在底层通常不是一步完成的,而是可能分成:
- 读取 count
- 加 1
- 写回 count
如果两个任务同时执行,就可能导致结果错误。
使用互斥量后,可以让这段操作变成“原子性地顺序执行”,避免竞争问题。
6. 互斥量和二值信号量的区别
很多人会问:
互斥量和二值信号量看起来都像“只有 0 和 1”,它们到底有什么区别?
虽然它们形式上很像,但设计目的不同。
6.1 二值信号量
二值信号量主要用于:
- 任务同步
- 中断通知任务
- 表示某个事件是否发生
它更像一个“通知开关”。
6.2 互斥量
互斥量主要用于:
- 保护共享资源
- 保证同一时刻只能有一个任务访问资源
它更像一把“资源锁”。
6.3 最重要的区别:优先级继承
互斥量支持 优先级继承(Priority Inheritance),而普通二值信号量通常不具备这个特性。
这一点非常重要,也是互斥量更适合做资源保护的根本原因。
7. 什么是优先级继承
优先级继承是为了缓解优先级反转问题。
7.1 什么是优先级反转
假设有三个任务:
- 低优先级任务 L
- 中优先级任务 M
- 高优先级任务 H
某一时刻:
- 低优先级任务 L 先拿到了互斥量
- 高优先级任务 H 也想获取这个互斥量,但拿不到,于是阻塞
- 这时候中优先级任务 M 抢占了低优先级任务 L
- L 迟迟得不到运行机会,就不能尽快释放互斥量
- 高优先级任务 H 也就一直被卡住
结果就是:
高优先级任务反而被低优先级任务间接拖住了。
这就是优先级反转。
7.2 优先级继承怎么解决
当低优先级任务持有互斥量,而高优先级任务在等待它时:
- FreeRTOS 会临时把低优先级任务的优先级提升到高优先级任务的级别
- 这样它就能尽快执行完临界区代码
- 然后尽快释放互斥量
- 释放后再恢复原来的优先级
这就是优先级继承。
8. 互斥量的使用特点
8.1 谁获取,谁释放
互斥量通常要求:
哪个任务获取了互斥量,就应该由哪个任务释放。
这是因为它代表的是“某个任务正在占用资源”。
所以互斥量不像普通信号量那样适合随便用于“你给我、我给你”的通知场景。
8.2 一般不用于中断中释放
互斥量主要用于任务之间的资源保护,通常不用于中断服务函数。
原因是:
- 互斥量涉及任务所有权
- 还涉及优先级继承机制
- 这类机制更适合在任务上下文中使用
因此:
- 中断同步 常用二值信号量
- 资源保护 常用互斥量
9. 互斥量的典型使用流程
互斥量的基本流程可以概括为:
第一步:创建互斥量
SemaphoreHandle_t xMutex;
xMutex = xSemaphoreCreateMutex();第二步:获取互斥量
xSemaphoreTake(xMutex, portMAX_DELAY);含义是:
- 任务申请这把锁
- 如果锁被别人占用,就一直等待
第三步:访问共享资源
printf("Task is using shared resource\n");第四步:释放互斥量
xSemaphoreGive(xMutex);表示:
- 资源已经用完
- 锁释放出去
- 其他等待的任务可以继续竞争
10. 一个简单示例
下面是一个典型的互斥量保护串口打印的例子:
SemaphoreHandle_t xMutex;
void Task1(void *pvParameters)
{
while (1)
{
xSemaphoreTake(xMutex, portMAX_DELAY);
printf("Task1 is running\n");
xSemaphoreGive(xMutex);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void Task2(void *pvParameters)
{
while (1)
{
xSemaphoreTake(xMutex, portMAX_DELAY);
printf("Task2 is running\n");
xSemaphoreGive(xMutex);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
int main(void)
{
xMutex = xSemaphoreCreateMutex();
xTaskCreate(Task1, "Task1", 1000, NULL, 1, NULL);
xTaskCreate(Task2, "Task2", 1000, NULL, 1, NULL);
vTaskStartScheduler();
while (1);
}说明
xMutex是共享的互斥量Task1和Task2都要先获取锁- 谁先拿到锁,谁先打印
- 打印完成后释放锁
- 这样可以避免两个任务同时操作串口,导致输出混乱
11. 使用互斥量时要注意的问题
11.1 不要忘记释放
如果任务获取了互斥量,却没有释放,那么其他任务可能会一直阻塞,导致系统卡住。
11.2 临界区不要太长
拿到互斥量后,应尽量快速完成资源访问,不要在临界区里做太耗时的事情。
因为:
- 临界区越长
- 其他任务等待越久
- 系统实时性越差
11.3 不要在持锁时长期阻塞
例如不要在拿着互斥量的情况下做很长时间的延时:
xSemaphoreTake(xMutex, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(5000));
xSemaphoreGive(xMutex);这会导致其他任务长期无法访问资源。
11.4 资源保护优先用互斥量,不要乱用二值信号量代替
虽然二值信号量有时也能做到“同一时刻一个任务访问”,但它不具备优先级继承,因此不如互斥量更适合保护共享资源。
12. 互斥量和临界区关中断的区别
有些场景也可以通过“关中断”或者“进入临界区”的方式保护代码,但它和互斥量不同。
互斥量
- 适合任务级资源保护
- 可以阻塞等待
- 支持优先级继承
- 不会简单粗暴地影响整个系统调度
临界区/关中断
- 适合非常短小的关键代码保护
- 通常不能持续太久
- 会影响系统响应中断和调度
所以一般来说:
- 很短的原子操作,可考虑临界区保护
- 涉及共享外设、共享模块、共享资源,优先考虑互斥量
13. 互斥量的本质总结
互斥量本质上是一种带有所有权和优先级继承机制的特殊信号量,专门用于保护共享资源。
它的核心思想是:
资源同一时刻只能被一个任务占用,其他任务必须等待。
14. 总结
FreeRTOS 中的互斥量可以总结为:
互斥量是一种用于保护共享资源的同步机制,它保证同一时刻只有一个任务可以访问某个资源,并且支持优先级继承,适合用于串口、总线、全局变量等共享资源的访问保护。
可以用一句话记忆:
互斥量就是“给共享资源上的锁”。
15. 面试式简短回答
如果面试官问“什么是互斥量(Mutex)”,可以这样回答:
互斥量是 FreeRTOS 中一种专门用于保护共享资源的同步机制,它可以保证同一时刻只有一个任务访问某个资源。和普通二值信号量不同,互斥量支持优先级继承,因此更适合用于串口、全局变量、总线接口等共享资源的互斥访问。
