4. 任务间同步:信号量(二值、计数、互斥)的原理与API使用

各位搞光刻机嵌入式开发的同行,咱们今天聊聊信号量。

说实话,在尼康光刻机的实时系统里,任务间同步是个绕不开的坎。你想想看,一个晶圆台在移动,激光在曝光,传感器在采集数据,这些任务如果不同步,那画面太美我不敢看。我当年刚接手光刻机项目时,就因为在同步问题上栽了个跟头,导致晶圆台和曝光时序错位,直接报废了一整批晶圆。从那以后,我对信号量就格外上心。

4.1 信号量是什么?

信号量,说白了就是一个内核维护的计数器。任务想要访问共享资源,就得先拿到这个"通行证"。拿不到?那就乖乖等着。

我习惯把信号量想象成停车场的车位计数器:

  • 有车位(信号量 > 0):直接进,计数器减1
  • 没车位(信号量 = 0):排队等,有人走了再进

在RTOS里,信号量主要分三种:二值信号量、计数信号量、互斥信号量。咱们一个一个说。

4.2 二值信号量

二值信号量,值只有0和1。它就像一把锁,要么锁着(0),要么开着(1)。

我一般在两种场景用它:

  • 任务同步:比如A任务做完某件事,通知B任务开始干活
  • 中断与任务同步:ISR里释放信号量,任务里获取,实现中断下半部处理

举个例子,光刻机里的激光发射和晶圆台定位:

// 定义二值信号量
SemaphoreHandle_t xLaserSync;

// 任务1:晶圆台定位
void vWaferStageTask(void *pvParameters) {
    while(1) {
        // 移动晶圆台到目标位置
        move_to_target_position();
        // 定位完成,释放信号量,通知激光可以发射了
        xSemaphoreGive(xLaserSync);
        // 继续下一个位置
    }
}

// 任务2:激光发射控制
void vLaserTask(void *pvParameters) {
    while(1) {
        // 等待晶圆台定位完成
        if(xSemaphoreTake(xLaserSync, portMAX_DELAY) == pdTRUE) {
            // 发射激光
            fire_laser();
        }
    }
}

嗯,这里要注意:二值信号量在创建时,初始值通常设为0。这样任务2一开始就会阻塞,直到任务1释放信号量。

我的小技巧:在ISR里释放信号量时,一定要用xSemaphoreGiveFromISR(),别用xSemaphoreGive()。我曾经见过同事在中断里用了普通版本,结果系统直接崩了。中断上下文和任务上下文,API不能混用。

4.3 计数信号量

计数信号量,值可以大于1。它适合管理多个同类资源。

你想想看,光刻机里有多个曝光头,每个曝光头同一时间只能被一个任务使用。这时候计数信号量就派上用场了。

// 创建计数信号量,初始有5个资源
SemaphoreHandle_t xExposureHeadSem;
xExposureHeadSem = xSemaphoreCreateCounting(5, 5);
// 参数1:最大计数值,参数2:初始计数值

// 任务:使用曝光头
void vExposureTask(void *pvParameters) {
    while(1) {
        // 申请一个曝光头
        if(xSemaphoreTake(xExposureHeadSem, pdMS_TO_TICKS(100)) == pdTRUE) {
            // 拿到曝光头,开始曝光
            do_exposure();
            // 用完释放
            xSemaphoreGive(xExposureHeadSem);
        } else {
            // 超时没拿到,做其他处理
            handle_timeout();
        }
    }
}

我个人习惯把计数信号量的初始值设成最大可用资源数。这样一开始所有资源都是空闲的,任务可以随时申请。

关键点:计数信号量不记录"谁拿了资源"。它只关心"还剩几个资源"。如果你需要知道哪个任务拿了哪个资源,那就得用其他机制了。

4.4 互斥信号量

互斥信号量,是二值信号量的"升级版"。它解决了两个经典问题:优先级反转和递归加锁。

优先级反转是什么?我举个例子你就明白了:

  • 低优先级任务拿了互斥锁
  • 高优先级任务来了,想拿锁,拿不到,被阻塞
  • 中优先级任务来了,它不需要锁,但它抢占了低优先级任务的CPU
  • 结果:高优先级任务反而被中优先级任务"踩在脚下"

这合理吗?当然不合理。互斥信号量通过优先级继承来解决:当高优先级任务被阻塞时,低优先级任务会临时"继承"高优先级任务的优先级,直到释放锁。

// 创建互斥信号量
SemaphoreHandle_t xMutex;
xMutex = xSemaphoreCreateMutex();

// 任务:访问共享的校准数据
void vCalibrationTask(void *pvParameters) {
    while(1) {
        // 拿互斥锁
        if(xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
            // 读写校准数据
            update_calibration_data();
            // 释放互斥锁
            xSemaphoreGive(xMutex);
        }
    }
}
注意:互斥信号量不能在ISR中使用!因为优先级继承在中断上下文里没有意义。在ISR里,请用二值信号量代替。

4.5 三种信号量对比

特性 二值信号量 计数信号量 互斥信号量
取值范围 0 或 1 0 ~ N 0 或 1
优先级继承 不支持 不支持 支持
ISR中使用 可以(用FromISR版本) 可以(用FromISR版本) 不可以
递归加锁 不支持 不支持 支持(同一任务可多次Take)
典型用途 任务同步、中断通知 管理多个同类资源 保护共享资源

4.6 核心API速查

FreeRTOS里信号量的API其实不多,记住这几个就够了:

// 创建
SemaphoreHandle_t xSemaphoreCreateBinary(void);
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount);
SemaphoreHandle_t xSemaphoreCreateMutex(void);

// 获取(Take)
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);
BaseType_t xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken);

// 释放(Give)
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
BaseType_t xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken);

// 删除
void vSemaphoreDelete(SemaphoreHandle_t xSemaphore);
避坑指南:我曾经在项目里犯过一个低级错误——在Take之后忘了Give。结果那个任务只运行了一次,后面所有等这个信号量的任务全卡死了。所以我现在写代码有个习惯:Take和Give成对出现,就像括号一样,写完Take立刻写Give,再往中间填业务逻辑。

4.7 知识体系总览

下面这张图,是我梳理的信号量知识体系。你把它存下来,以后写代码时对照着看,基本不会出错。

信号量知识体系 信号量 (Semaphore) 二值信号量 值:0 或 1 用途:任务同步、中断通知 ISR:支持 (FromISR版本) 计数信号量 值:0 ~ N 用途:管理多个同类资源 ISR:支持 (FromISR版本) 互斥信号量 值:0 或 1 用途:保护共享资源 ISR:不支持 核心原则:Take 和 Give 必须成对出现

好了,信号量的内容就这些。说白了就是三个类型、四个API、一个原则。你在光刻机项目里写代码时,多想想今天讲的这些,尤其是那个优先级反转的问题,别等出了bug再回头查。

专注资料整理