4、互斥量API详解:xSemaphoreCreateMutex、xSemaphoreTake、xSemaphoreGive

好,咱们直接进入正题。互斥量这东西,说白了就是一把带「所有权」的锁。跟普通的二值信号量不一样,谁拿了谁才能放,别人想抢都抢不走。我刚开始用FreeRTOS那会儿,就因为这俩玩意儿没分清,踩过不少坑。

今天我把三个核心API给你掰开揉碎了讲。你只要记住这三个函数,互斥量的日常使用基本就稳了。

4.1 创建互斥量:xSemaphoreCreateMutex

先看创建。这个函数是互斥量的起点。

SemaphoreHandle_t xSemaphoreCreateMutex( void );

用法很简单,不需要传任何参数。返回一个句柄,你拿这个句柄去操作互斥量。

重点来了:这个函数内部会动态分配内存。如果你的项目里禁止使用malloc,或者你跑在裸机资源极度紧张的环境下,那就要小心了。我记得有一次做工业控制器,RAM总共才8KB,我随手创建了5个互斥量,结果系统直接启动失败。查了半天,发现是堆空间不够了。

返回值判断也很关键:

  • 成功:返回非NULL的句柄
  • 失败:返回NULL

嗯,这里要注意。很多新手拿到NULL就直接往下跑,结果系统崩得莫名其妙。我建议你每次创建完都加个断言或者错误处理。

我的小习惯:创建互斥量时,我会在初始化阶段统一创建,而不是在任务运行中动态创建。这样能提前发现内存不足的问题,不至于跑到一半突然崩掉。

4.2 获取互斥量:xSemaphoreTake

这个函数是互斥量的核心操作。你想访问共享资源?先拿锁。

BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait );

两个参数:

  • xSemaphore:你创建好的互斥量句柄
  • xTicksToWait:等待时间,单位是系统Tick。设为0表示不等待,立即返回。设为portMAX_DELAY表示死等,直到拿到锁为止。

返回值就两种:

  • pdTRUE:拿到锁了,放心用资源
  • pdFALSE:没拿到,超时了或者被别的任务占着

我曾经踩过的坑:有一次我在中断服务函数里调用了xSemaphoreTake,结果系统直接卡死。为什么?因为互斥量涉及任务调度,中断里不能调用!后来我改用xSemaphoreTakeFromISR才解决。记住,中断里别碰互斥量,这是铁律。

你想想看,如果两个任务同时调用xSemaphoreTake抢同一个互斥量,会发生什么?FreeRTOS内部有优先级继承机制,能防止优先级反转。这个机制我后面会专门讲,今天先记住:互斥量自带优先级继承,这是它比二值信号量强的地方。

4.3 释放互斥量:xSemaphoreGive

用完资源,记得还锁。这就是xSemaphoreGive的活。

BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );

参数就一个,互斥量句柄。返回值同样是pdTRUE或pdFALSE。

这里有个容易翻车的地方:谁拿的锁,必须由谁来释放。你不能在任务A里拿了锁,跑到任务B里去释放。FreeRTOS会检查当前任务是不是锁的持有者,如果不是,xSemaphoreGive会返回pdFALSE。

实战经验:我见过一个同事,在任务A里拿了锁,然后通过消息队列把锁句柄发给任务B,让任务B去释放。结果系统跑着跑着就死锁了。他查了两天才发现是这个问题。说白了,互斥量的所有权是绑定在任务上的,别想着跨任务操作。

释放失败的情况还有几种:

  • 互斥量已经被释放了,你又释放一次(重复释放)
  • 中断里调用xSemaphoreGive(应该用xSemaphoreGiveFromISR)
  • 互斥量句柄是NULL(创建失败没检查)

4.4 三个API的配合使用

光讲单个函数没意思,咱们看个完整的例子。这才是实战中常见的写法。

// 全局互斥量句柄
SemaphoreHandle_t xMutex;

// 创建互斥量(一般在启动调度器之前)
xMutex = xSemaphoreCreateMutex();
if (xMutex == NULL) {
    // 创建失败,处理错误
    while(1);
}

// 任务A:写数据
void TaskA(void *pvParameters) {
    while(1) {
        if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
            // 拿到锁了,操作共享资源
            shared_data++;
            // 用完记得释放
            xSemaphoreGive(xMutex);
        }
        // 做其他事情
        vTaskDelay(10);
    }
}

// 任务B:读数据
void TaskB(void *pvParameters) {
    while(1) {
        if (xSemaphoreTake(xMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
            // 拿到锁,读取数据
            int val = shared_data;
            xSemaphoreGive(xMutex);
        } else {
            // 超时没拿到锁,做其他处理
            // 不要在这里死等
        }
        vTaskDelay(20);
    }
}

你看,任务A用了portMAX_DELAY,意思是「我非要拿到这个锁不可」。任务B用了100ms超时,拿不到就干别的。这两种策略各有适用场景,我个人的习惯是:写操作尽量用死等,读操作用超时。为什么?写操作一般不能丢,读操作偶尔读不到也没关系。

避坑指南:释放互斥量之后,最好立即把互斥量句柄相关的操作结束掉。不要拿着锁去做延时、打印这种耗时操作。我曾经在一个项目里,任务拿了锁之后调了个printf,结果打印太慢,其他高优先级任务全被堵死了。记住:拿锁快,放锁更快

4.5 三个API的对比总结

最后给你一张表,方便你快速查阅。

API 作用 调用位置 注意事项
xSemaphoreCreateMutex 创建互斥量 任务启动前或任务中 检查返回值是否为NULL;动态分配内存
xSemaphoreTake 获取互斥量 任务中 不能在中断中调用;注意超时时间设置
xSemaphoreGive 释放互斥量 任务中 谁拿谁放;不能在中断中调用

嗯,这三个API你只要记住:创建、拿、放。顺序不能乱,谁拿谁放不能错。做到这几点,互斥量的基础用法你就掌握了。下一节我会讲互斥量的优先级继承机制,那才是真正体现互斥量价值的地方。