Skip to content

信号量互斥锁

6.1 信号量的本质

信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务间同步或共享资源的互斥访问。一个信号量的数据结构中,通常有一个计数值,用于对有效资源数的计数,表示剩下的可被使用的共享资源数。

信号量用于多任务的同步,而后面会讲的互斥量用于实现多任务的互斥。

信号量本质上时特殊的队列,下面时对应的差异:

队列

  • 读队列:① 拷贝数据。 ② CNT-- 。 ③唤醒等待接收数据者。
  • 写队列:① 拷贝数据。 ② CNT++ 。 ③唤醒等待接收数据者。
  • 读队列时,队列为空 可以阻塞
  • 写队列时,队列已满 可以阻塞

信号量

  • 给出资源(Give):① CNT++。 ② 唤醒等待者。
  • 获得资源(Take):① CNT--。(没有唤醒操作)
  • Give:计数值已经达到最大时返回失败, 不阻塞
  • Take:没有资源获取时, 可以阻塞

6.2 信号量函数

6.2.1 创建信号量

两种信号量差异:

二进制信号量 计数型信号量
只有 0 或 1 两个值,被创建时初始值为 0 计数值自定义,且被创建时初始值可以设定
其他操作是一样的 其他操作是一样的

计数型信号量

C
/* Creates a new counting semaphore instance, and returns a handle by which the
 * new counting semaphore can be referenced. 
 */
SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount, UBaseType_t uxInitialCount )

二进制信号量

C
/* Creates a new binary semaphore instance, and returns a handle by which the
 * new semaphore can be referenced. 
 */
SemaphoreHandle_t xSemaphoreCreateBinary( void )

6.2.2 获取/释放信号量

获取信号量

C
/* to obtain a semaphore. */
xSemaphoreTake(
        SemaphoreHandle_t xSemaphore,
        TickType_t xBlockTime
               );
/* to obtain a semaphore from an ISR */   
xSemaphoreTakeFromISR(
        SemaphoreHandle_t xSemaphore,
        BaseType_t *pxHigherPriorityTaskWoken
                      );

释放信号量

C
/*  to release a semaphore. */
xSemaphoreGive( SemaphoreHandle_t xSemaphore );

/*  to release a semaphore from an ISR */
xSemaphoreGiveFromISR(
        SemaphoreHandle_t xSemaphore,
        BaseType_t *pxHigherPriorityTaskWoken
                      );

6.2.3 删除信号量

6.3 使用信号量

6.3.1 创建、获取与释放示例

在队列最后一个示例的基础上:

定义信号量句柄

C
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN Variables */
static SemaphoreHandle_t g_xSemaphoreHandle;
/* USER CODE END Variables */

创建信号量,最大计数为 5 ,可用量为 2 ;并将任务注释一个

C
void MX_FREERTOS_Init(void) {
    /* USER CODE BEGIN Init */
    g_xSemaphoreHandle = xSemaphoreCreateCounting(4, 2);
    /* USER CODE END Init */
    ...
    /* USER CODE BEGIN RTOS_THREADS */
    /* add threads, ... */
    xTaskCreate(OLED_PrintTask, "Task1", 128, &g_car[0], osPriorityNormal, NULL);
    xTaskCreate(OLED_PrintTask, "Task2", 128, &g_car[1], osPriorityNormal, NULL);
    xTaskCreate(OLED_PrintTask, "Task3", 128, &g_car[2], osPriorityNormal, NULL);
}

修改任务函数,取消队列,添加信号量获取与释放

C
void OLED_PrintTask(void *params) {
    Car *car = params;
    ShowCar(car);
    /* 获取信号量 */
    xSemaphoreTake(g_xSemaphoreHandle, portMAX_DELAY);
    while (1) {
        /* 读队 */
        HideCar(car);
        car->x += 1;
        if (car->x > 128 ) {car->x = 120;}
        ShowCar(car);
        vTaskDelay(100);
        if (car->x == 120) {
            /* 释放信号量 */
            xSemaphoreGive(g_xSemaphoreHandle);
            vTaskDelete(NULL);
        }
    }
}

实验现象,上两行字符先移动,到达终点释放信号量,第三行字符开始移动。

6.3.2 信号量分配优先级

  1. 优先级高的,后到可以排前面。
  2. 优先级一样高的,按创建任务的先后分配信号量,后创建的等待信号量释放。

6.3.3 优先级反转

什么时优先级反转,就是低优先级的任务卡住了高优先级任务,使其无法运行,如下面的例子:

① 低优先级任务:占用唯一一个信号量实验。
② 中优先级任务:没有阻塞态,一直处于可运行状态。(一旦此任务运行,则低优先级任务任务没有运行机会)
③ 高优先级任务:需要信号量,得不到信号量就阻塞。

复现上诉案例:

使用二进制信号量,并修改任务优先级

C
void MX_FREERTOS_Init(void) {
    /* USER CODE BEGIN Init */
    g_xSemaphoreHandle = xSemaphoreCreateBinary();
    // 释放一个信号量
    xSemaphoreGive(g_xSemaphoreHandle);
    /* USER CODE END Init */
    ...
    /* USER CODE BEGIN RTOS_THREADS */
    /* add threads, ... */
    xTaskCreate(OLED_PrintTask1, "Task1", 128, &g_car[0], osPriorityNormal, NULL);
    xTaskCreate(OLED_PrintTask2, "Task2", 128, &g_car[1], osPriorityNormal+1, NULL);
    xTaskCreate(OLED_PrintTask3, "Task3", 128, &g_car[2], osPriorityNormal+2, NULL);
    /* USER CODE END RTOS_THREADS */
    ...
}

修改任务函数

C
void OLED_PrintTask1(void *params) {
    Car *car = params;
    ShowCar(car);
    xSemaphoreTake(g_xSemaphoreHandle, portMAX_DELAY);
    while (1) {
        /* 读队 */
        HideCar(car);
        car->x += 1;
        if (car->x > 128 ) {car->x = 120;}
        ShowCar(car);
        vTaskDelay(100);
        if (car->x == 120) {
            xSemaphoreGive(g_xSemaphoreHandle);
            vTaskDelete(NULL);
        }
    }
}

void OLED_PrintTask2(void *params) {
    Car *car = params;
    ShowCar(car);
    vTaskDelay(1000);
//    xSemaphoreTake(g_xSemaphoreHandle, portMAX_DELAY);
    while (1) {
        /* 读队 */
        HideCar(car);
        car->x += 1;
        if (car->x > 128 ) {car->x = 120;}
        ShowCar(car);
//        vTaskDelay(100);
        HAL_Delay(100);
        if (car->x == 120) {
//            xSemaphoreGive(g_xSemaphoreHandle);
            vTaskDelete(NULL);
        }
    }
}
void OLED_PrintTask3(void *params) {
    Car *car = params;
    ShowCar(car);
    vTaskDelay(2000);
    xSemaphoreTake(g_xSemaphoreHandle, portMAX_DELAY);
    while (1) {
        /* 读队 */
        HideCar(car);
        car->x += 1;
        if (car->x > 128 ) {car->x = 120;}
        ShowCar(car);
        vTaskDelay(100);
        if (car->x == 120) {
            xSemaphoreGive(g_xSemaphoreHandle);
            vTaskDelete(NULL);
        }
    }
}

6.4 互斥锁

6.4.1 互斥锁本质

互斥锁又称 互斥型信号量 ,是一种特殊的二值性信号量,用于实现对共享资源的独占式处理。

多任务环境下往往存在多个任务竞争同一共享资源的应用场景,互斥锁可被用于对共享资源的保护从而实现独占式访问。另外互斥锁可以解决信号量存在的优先级反转问题

用互斥锁处理非共享资源的同步访问时,如果有任务访问该资源,则互斥锁为加锁状态。此时其他任务如果想访问这个公共资源则会被 阻塞 ,直到互斥锁被持有该锁的任务释放后,其他任务才能重新访问该公共资源,此时互斥锁再次上锁,如此确保同一时刻只有一个任务正在访问这个公共资源,保证了公共资源操作的完整性。

image-20250202212526648

案例分析:

任务1获取锁A,随后任务2获取锁A。由于此时任务1并未释放锁A,此时任务2会进入 阻塞态 。

6.4.2 使用场景

  1. 解决优先级反转问题,临时提拔低优先级任务的优先级和高优先级一样。
  2. 保护临界资源。

6.4.3 互斥锁函数

C
/* 创建 */
SemaphoreHandle_t xSemaphoreCreateMutex( void );
C
/* 释放 */
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );
C
/* 获得 */
BaseType_t xSemaphoreTake(
                   SemaphoreHandle_t xSemaphore,
                   TickType_t xTicksToWait
               );

6.4.4 解决优先级反转

将二进制信号量的创建改成互斥量即可:

C
void MX_FREERTOS_Init(void) {
    /* USER CODE BEGIN Init */
    g_xSemaphoreHandle = xSemaphoreCreateMutex();
    xSemaphoreGive(g_xSemaphoreHandle);
}

6.4.5 保护临界资源

待补充

推荐阅读:第13章 互斥量(mutex) | 百问网