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释放信号量。
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);
}
}
}
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);
4.7 知识体系总览
下面这张图,是我梳理的信号量知识体系。你把它存下来,以后写代码时对照着看,基本不会出错。
好了,信号量的内容就这些。说白了就是三个类型、四个API、一个原则。你在光刻机项目里写代码时,多想想今天讲的这些,尤其是那个优先级反转的问题,别等出了bug再回头查。