Semaphore 는 상호 배제 (Mutual exclusion) 을 위해 고안된 기법 입니다.

임계 구역

입계 구역(Critical Section) 은 복수의 쓰레드, 프로세스 등이 동시에 접근해서는 안되는 공유 자원(자료 구조 또는 장치)을 접근하는 코드의 일부를 말합니다.

ESP 에서 대표적으로 임계 구역으로 부를만한, 복수의 태스크가 접근하면 안되는 대표적인 예시로 시리얼 통신, 소켓, 파일시스템 등이 있습니다.

시리얼 통신을 예를 들면, 여러개의 태스크가 시리얼 채널에 데이터를 작성하면, 데이터가 겹쳐지면서 데이터가 깨지는 문제가 발생할 수 있습니다.

이처럼 임계 구역에 접근하는 태스크는 세마포어 등의 동기화 매커니즘을 사용하여 자원에 접근해야 합니다.

세마포어(Semaphore)

세마포어는 atomic(원자적인) 정수 변수로 세마포어의 값을 접근 가능한 테스크의 수로 설정 합니다.

특정 테스크가 임계 영역에 접근하기 전, 세마포어의 값이 0이면 영역에 접근하지 못하도록 차단하고, 0 이상의 정수라면 테스크는 임계영역에 접근하고, 세마포어의 값을 1 감소시킵니다. (P 연산)

반대로 임계영역에서 나가는 테스크는 세마포어의 값을 1 증가시켜 다른 테스크가 영역에 접근할 수 있도록 합니다. (V 연산)

세마포어의 최대값이 1 이라면 BinarySemaphore 라고 부릅니다. (세마포어의 값이 0 혹은 1)

세마포어 사용하기

세마포어를 사용하기 위해 다음 헤더파일을 인클루드 하면 됩니다.

// 모든 FreeRTOS 헤더파일을 include 하기 전에 먼저 include 해야함
#include <freertos/FreeRTOS.h>

// 테스크
#include <freertos/task.h>

// 세마포어
#include <freertos/semphr.h>

바이너리 세마포어(Binary Semaphore)

바이너리 세마포어 만들기

SemaphoreHandle_t sem = NULL;

sem = xSemaphoreCreateBinary();

if (sem == NULL) {
    ESP_LOGE(TAG, "create sem error");
    esp_restart();
} else {
    ESP_LOGE(TAG, "sem created");
}

xSemaphoreCreateBinary() 매크로로 바이너리 세마포어를 만들 수 있습니다.

힙 메모리 부족 등으로 세마포어 생성에 실패하면 NULL 을 리턴합니다.

이전에는 vSemaphoreCreateBinary() 매크로를 사용하였지만, deprecated 되었으니 사용하면 안됩니다.

https://www.freertos.org/a00121.html

생성된 세마포어는 0 의 값을 가집니다.

P 연산 (Take)

임계 영역 진입 전에 사용하며 세마포어를 취득합니다.

세마포어의 값이 0이 아니라면 세마포어의 값을 1 감소시키고 세마포어의 값이 0이라면 세마포어의 값이 증가할 때 까지 wait 합니다.

FreeRTOS 에서는 xSemaphoreTake() 매크로를 사용합니다.

이 매크로는 ISR 에서 호출하면 안되며, ISR 에서는 xSemaphoreTakeFromISR() 매크로를 사용해야 합니다.

이 매크로에는 인자가 두개 필요 합니다.

xSemaphoreTake( xSemaphore, xBlockTime )

xSemaphorexSemaphoreCreateBinary() 로 초기화된 세마포어 핸들러 입니다.

xBlockTime 은 세마포어가 사용 가능해질 때까지 기다리는 틱 입니다. portTICK_PERIOD_MS 매크로로 나누어서 밀리 세컨드 단위로 변환 할 수 있습니다.

xSemaphoreTake 매크로에 진입하면, xBlockTime 틱 만큼 대기를하며, 대기 시간동안 세마포어를 얻으면 true를 리턴하고 대기를 종료합니다.

xBlockTime 틱 까지 기다려도 세마포어를 얻지 못한다면 false 를 리턴하고 대기를 종료합니다.

// 10틱 까지 대기
if(xSemaphoreTake(sem, 10)) {
	// 대기 시간동안 세마포어를 얻었음
    
    // 임계 영역 접근
    // 임계 영역 종료
    
    xSemaphoreGive(sem);
} else {
	// 대기 시간동안 세마포어를 얻지 못함
    ESP_LOGE(TAG, "세마포어를 얻지 못함");
}

V 연산 (Give)

임계 영역에서 나갈떄 세마포어를 반환합니다.

xSemaphoreGive() 매크로를 사용하며 하나의 인자가 필요합니다.

이 매크로는 ISR 에서 호출하면 안되며, ISR 에서는 xSemaphoreGiveFromISR() 매크로를 사용해야 합니다.

xSemaphoreGive( xSemaphore )

xSemaphorexSemaphoreCreateBinary() 로 초기화된 세마포어 핸들러 입니다.

세마포어 반환에 성공하면 true 를 실패하면 false 를 리턴합니다.

세마포어가 없는 경우

간단하게 두개의 테스크를 생성해서 두 테스크가 임계영역에 접근하도록 구성했습니다.

A 테스크

  • 1초 동안 임계 영역 접근 (vTaskDelay)
  • 임계 영역 에서 나오고 1초 대기

B 테스크

  • 2초 동안 임계 영역 접근 (vTaskDelay)
  • 임계 영역 에서 나오고 1초 대기
static const char *TAG = "main";

#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/semphr.h>

#include <esp_log.h>

void TaskA(void *) {
    while (1) {
        ESP_LOGI(TAG, "A - enter critical");

        // 임계 영역 시작
        vTaskDelay(1000 / portTICK_PERIOD_MS);
        // 임계 영역 끝

        ESP_LOGI(TAG, "A - exit critical");
        
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}

void TaskB(void *) {
    while (1) {
        ESP_LOGI(TAG, "B - enter critical");

        // 임계 영역 시작
        vTaskDelay(2000 / portTICK_PERIOD_MS);
        // 임계 영역 끝

        ESP_LOGI(TAG, "B - exit critical");

        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}

void app_main(void) {
    // 테스크 생성
    xTaskCreate(TaskA, "A", 4096, NULL, 6, NULL);
    xTaskCreate(TaskB, "B", 4096, NULL, 5, NULL);
}
I (323) main: A - enter critical	-- A 테스크 임계 영역 접근
I (323) main: B - enter critical    -- B 테스크 임계 영역 접근 -> 충돌
I (423) main: A - exit critical
I (523) main: A - enter critical
I (523) main: B - exit critical
I (623) main: A - exit critical

세마포어 사용

static const char *TAG = "main";

#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/semphr.h>

#include <esp_log.h>

SemaphoreHandle_t sem = NULL;

void TaskA(void *) {
    while (1) {
        xSemaphoreTake(sem, portMAX_DELAY);	// 세마포어를 얻을때 까지 무한 대기
        ESP_LOGI(TAG, "A - enter critical");

        // 임계 영역 시작
        vTaskDelay(1000 / portTICK_PERIOD_MS);
        // 임계 영역 끝

        ESP_LOGI(TAG, "A - exit critical");
        xSemaphoreGive(sem);


        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}

void TaskB(void *) {
    while (1) {
        xSemaphoreTake(sem, portMAX_DELAY);	// 세마포어를 얻을때 까지 무한 대기
        ESP_LOGI(TAG, "B - enter critical");

        // 임계 영역 시작
        vTaskDelay(2000 / portTICK_PERIOD_MS);
        // 임계 영역 끝

        ESP_LOGI(TAG, "B - exit critical");
        xSemaphoreGive(sem);

        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}

void app_main(void) {
    // 세마포어 생성
    sem = xSemaphoreCreateBinary();

    if (sem == NULL) {
        ESP_LOGI(TAG, "create sem error");
        esp_restart();
    }

	// xSemaphoreCreateBinary() 로 생성된 세마포어의 값은 '0' 입니다.
    // 현재 상황에서는 xSemaphoreGive() 로 세마포어의 값을 1 증가시켜야 합니다.
    xSemaphoreGive(sem);

    // 테스크 생성
    xTaskCreate(TaskA, "A", 4096, NULL, 6, NULL);
    xTaskCreate(TaskB, "B", 4096, NULL, 5, NULL);

    vTaskDelay(portMAX_DELAY);
}
I (323) main: A - enter critical
I (423) main: A - exit critical
I (423) main: B - enter critical
I (623) main: B - exit critical
I (623) main: A - enter critical
I (723) main: A - exit critical
I (723) main: B - enter critical
I (923) main: B - exit critical

테스크가 임계 영역에 하나 씩 접근하며 다른 테스크가 임계 영역에 나갈때 까지 테스크가 기다리는 모습을 볼 수 있습니다.

우선순위 역전

세마포어는 우선순위 상속 메커니즘이 포함되어 있지 않습니다.

이를 위해 뮤텍스를 사용하며 뮤텍스는 우선순위상속 매커니즘이 포함된 바이너리 세마포어 입니다.

이로 인해 바이너리 세마포어는 동기화(작업 간 또는 작업과 인터럽트 간)를 구현하는 데 더 나은 선택이 되고, 뮤텍스는 간단한 상호 배제를 구현하는 데 더 나은 선택이 됩니다.

우선순위 역전에 대해서는 뮤텍스 편에서 직접 다루겠습니다.