본문 바로가기
임베디드 FW/펌웨어 구조

RTOS 자원 관리 (Critical Section, Mutex, Semaphore)

by MachineJW 2025. 12. 11.

1. 임계구간 (Critical Section)

임계구간, 임계구역 등으로 표현된다.
TASK (Process) 에서 데이터 경쟁이 발생되는 코드 블럭을 의미한다.

  • RTOS를 임베디드 시스템에 적용하고 설계할 때 반드시 여러 개의 TASK가 하나의 변수나 구조체 등의 데이터를 참조하여 Read, Write 해야하는 상황이 있다.
  • 하나의 자원에 접근하려는 시도가 있는 코드 블럭 자체를 임계 구간 (Critical Section) 이라고 한다.
  • 임계 구간 충돌을 방지하기 위한 다양한 방법이 있는데 바로 뮤텍스(Mutex)세마포어(Semaphore)이다.
  • 아래는 보호 하지 않은 RTOS 코드의 예시를 보여준다.
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

typedef struct {
  float sensor1;
  float sensor2;
} SensorData_t;

/* 공유 자원 (보호 안 됨!) */
SensorData_t SharedData;

TaskHandle_t Task1Handle;
TaskHandle_t Task2Handle;
TaskHandle_t Task3Handle;

/* ───── TASK1 : 센서1 읽기 (보호 없음) ───── */
void Task1(void *pvParameters) {
  while (1) {
    float value = random(200, 300) / 10.0f;  // 센서1 값 예시

    // 임계구역이지만 보호 없음!
    SharedData.sensor1 = value;

    Serial.printf("[TASK1] sensor1 write = %.2f\n", value);
    vTaskDelay(50 / portTICK_PERIOD_MS);   // 짧게 돌려서 충돌 잘 나게
  }
}

/* ───── TASK2 : 센서2 읽기 (보호 없음) ───── */
void Task2(void *pvParameters) {
  while (1) {
    float value = random(500, 700) / 10.0f;  // 센서2 값 예시

    // 임계구역이지만 보호 없음!
    SharedData.sensor2 = value;

    Serial.printf("[TASK2] sensor2 write = %.2f\n", value);
    vTaskDelay(70 / portTICK_PERIOD_MS);
  }
}

/* ───── TASK3 : 네트워크로 전송 (보호 없음) ───── */
void Task3(void *pvParameters) {
  while (1) {
    float s1 = SharedData.sensor1;   // 읽는 중에 다른 Task가 수정할 수 있음
    float s2 = SharedData.sensor2;

    Serial.printf("[TASK3] send → s1=%.2f, s2=%.2f\n", s1, s2);

    // 서버에 데이터를 전송하는 함수 예시
    // sendToServer(s1, s2);

    vTaskDelay(100 / portTICK_PERIOD_MS);
  }
}

void setup() {
  Serial.begin(115200);

  // 초기값
  SharedData.sensor1 = 0;
  SharedData.sensor2 = 0;

  xTaskCreate(Task1, "Sensor1", 4096, NULL, 2, &Task1Handle);
  xTaskCreate(Task2, "Sensor2", 4096, NULL, 2, &Task2Handle);
  xTaskCreate(Task3, "Network", 4096, NULL, 1, &Task3Handle);
}

void loop() {
}
  • 제시된 코드는 Arduino Core 에서 동작되는 Free RTOS 프로그램이다.
  • 현재 코드를 보면 아무렇지 않게 TASK에 임계구역이 할당되어 보호없이 동일 구조체 데이터에 접근하고 있다.
  • TASK1이 sensor1에 값을 쓰는 중 동시에 TASK3이 Sensor1을 읽어 동시에 네트워크로 데이터 전송
  • 임계구역 보호는 안전성, 정확성, 일관성 보장을 통해 전체 시스템을 정상 유지하기 위한 필수 요소
  • ESP-IDF 공식 문서에서도 "멀티코어 환경에서는 동일한 데이터에 대한 접근이 보호되지 않으면
    데이터 일관성이 보장되지 않으며, 이는 오류의 주요 원인이다.” 라고 명시하고 있다.

2. 뮤텍스 (Mutex)

" 이 자원은 내가 쓰고 있으니, 다른  태스크는 기다려. "

Mutex는 Mutual Exclusion (상호배제) 의 약자이다.
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"   // 뮤텍스, 세마포어 사용

typedef struct {
  float sensor1;
  float sensor2;
} SensorData_t;

/* 공유 자원 */
SensorData_t SharedData;

/* 뮤텍스 핸들 */
SemaphoreHandle_t DataMutex;

TaskHandle_t Task1Handle;
TaskHandle_t Task2Handle;
TaskHandle_t Task3Handle;

void setup() {
  Serial.begin(115200);

  SharedData.sensor1 = 0;
  SharedData.sensor2 = 0;

  /* 뮤텍스 생성 */
  DataMutex = xSemaphoreCreateMutex();
  if (DataMutex == NULL) {
    Serial.println("Mutex create failed!");
    while (1);
  }

  xTaskCreate(Task1, "Sensor1", 4096, NULL, 2, &Task1Handle);
  xTaskCreate(Task2, "Sensor2", 4096, NULL, 2, &Task2Handle);
  xTaskCreate(Task3, "Network", 4096, NULL, 1, &Task3Handle);
}

void loop() {}
  • 우리는 이제 임계구역이라는 것을 알았으니, 공유자원을 보호해야 한다.
  • 가장 많이 쓰이는  방법은 뮤텍스(Mutex) 인데 TASK가 읽고 쓰는 중일때, 다른 TASK가 접근하지 못하도록 Lock을 걸어두는 것이다.
  • 코드로 적용하는 법은 FreeRTOS 기준으로 다음과 같다.

void Task1(void *pvParameters) {
  while (1) {
    float value = random(200, 300) / 10.0f;

    /* ── 임계구역 시작 ── */
    if (xSemaphoreTake(DataMutex, portMAX_DELAY)) {
      SharedData.sensor1 = value;
      xSemaphoreGive(DataMutex);
    }
    /* ── 임계구역 끝 ── */

    Serial.printf("[TASK1] sensor1 write = %.2f\n", value);
    vTaskDelay(50 / portTICK_PERIOD_MS);
  }
}
  • Task1이 공유 데이터 작업을 할 때, 다른 Task는 접근하지 못하고 기다린다.

void Task2(void *pvParameters) {
  while (1) {
    float value = random(500, 700) / 10.0f;

    /* ── 임계구역 시작 ── */
    if (xSemaphoreTake(DataMutex, portMAX_DELAY)) {
      SharedData.sensor2 = value;
      xSemaphoreGive(DataMutex);
    }
    /* ── 임계구역 끝 ── */

    Serial.printf("[TASK2] sensor2 write = %.2f\n", value);
    vTaskDelay(70 / portTICK_PERIOD_MS);
  }
}
  • Task2가 공유 데이터에 작업을 할 때, 다른 Task는 접근하지 못하고 기다린다.

void Task3(void *pvParameters) {
  float s1, s2;

  while (1) {
    /* ── 임계구역 시작 ── */
    if (xSemaphoreTake(DataMutex, portMAX_DELAY)) {
      s1 = SharedData.sensor1;
      s2 = SharedData.sensor2;
      xSemaphoreGive(DataMutex);
    }
    /* ── 임계구역 끝 ── */

    Serial.printf("[TASK3] send → s1=%.2f, s2=%.2f\n", s1, s2);

    // sendToServer(s1, s2);
    vTaskDelay(100 / portTICK_PERIOD_MS);
  }
}
  • Task3가 공유 데이터에 작업을 할 때, 다른 Task는 접근하지 못하고 기다린다.
xSemaphoreTake(DataMutex); // Key 획득 (Lock)
xSemaphoreGive(DataMutex); // Key 반납 (Lock 해제)

 

Mutex는 소유권 (Ownership) 이라는 개념이 있다.

  • Mutex 를 사용할 때 중요한 것은 반드시 잠근 TASK가 반드시 LOCK을 풀어줘야하는 것이다.
  • Lock 풀어주지 않으면 다른 Task는 전부 대기 상태에 빠지고, 이를 DeadLcok 이라고도 한다.

3. 세마포어 (Semaphore)

" 지금 이 자원을 사용해도 되는 지 알려줄게 "

흔히 볼 수 있는 철도의 신호기 구조

사실 세마포어(Semaphore)는 철도의 신호기를 가르키는 용어 이다.
해당 방법 또한 철도의 신호기와 방식이 비슷하다고 하여 용어가 세마포어 (Semaphore)가 되었다.
  • 뮤텍스가 TASK간 자원 접근 충돌을 막기위해 Lock을 사용했다면 세마포어는 동기화를 활용한다.
  • 즉, 이 자원을 지금 사용해도 되는가?를 알려주는 동기화 신호 매커니즘을 가지고 있다.
  • 철도의 신호기가 열차를 “지금 출발해도 된다 / 기다려라”를 알려주는 신호 체계인 것처럼, 세마포어 역시 Task의 실행 흐름을 신호 기반으로 제어한다.
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"

/* 세마포어 핸들 */
SemaphoreHandle_t DataReadySem;

/* 공유 데이터 (예시) */
float sensorValue = 0.0f;

void setup() {
  Serial.begin(115200);

  /* Binary Semaphore 생성 */
  DataReadySem = xSemaphoreCreateBinary();

  xTaskCreate(SensorTask, "SensorTask", 2048, NULL, 2, NULL);
  xTaskCreate(NetworkTask, "NetworkTask", 2048, NULL, 1, NULL);
}

void loop() {}
  • 코드로 적용하는 법은 FreeRTOS 기준으로 다음과 같다.
/* ───── 센서 Task (Producer) ───── */
void SensorTask(void *pvParameters) {
  while (1) {
    // 센서 값 측정 (예시)
    sensorValue = random(100, 300) / 10.0f;
    Serial.printf("[SensorTask] Sensor value = %.2f\n", sensorValue);

    // ✅ 데이터 준비 완료 신호
    xSemaphoreGive(DataReadySem);

    vTaskDelay(1000 / portTICK_PERIOD_MS);
  }
}

/* ───── 네트워크 Task (Consumer) ───── */
void NetworkTask(void *pvParameters) {
  while (1) {
    // 🔴 세마포어 신호를 기다림 (동기화 지점)
    if (xSemaphoreTake(DataReadySem, portMAX_DELAY) == pdTRUE) {

      // 세마포어를 통해 "데이터가 준비됨"이 보장됨
      Serial.printf("[NetworkTask] Send data = %.2f\n", sensorValue);

      // sendToServer(sensorValue);
    }
  }
}

  • 이 코드에서 세마포어는 자원을 직접적으로 보호하지 않는다.
  • 대신 태스크 1이 데이터를 준비한 이후에만 태스크 2가 실행되는 것을 보장한다.
  • xSemaphoreGive() 는 지금 접근해도 된다는 신호이고 xSemaphoreTake()는 신호가 올 때까지 대기하는 함수이다.
  • 이는 철도의 신호기가 "출발 가능" 신호를 줄 때 까지 열차가 대기하는 방식과 정확히 같다.
  • 사실 세마포어는 인터럽트 발생 후 TASK 처리에서 가장 많이 사용된다.
ISR는 자원을 보호하는 곳이 아니라, 이벤트를 알리는 곳이다.
그래서 ISR에서는 뮤텍스가 아니라 세마포어를 사용한다.
/* 세마포어는 ISR -> TASK 처리에서 가장 많이 사용된다. */

void IRAM_ATTR GPIO_ISR() {
  BaseType_t xHigherPriorityTaskWoken = pdFALSE;
  xSemaphoreGiveFromISR(EventSem, &xHigherPriorityTaskWoken);
  portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

void EventTask(void *pv) {
  while (1) {
    xSemaphoreTake(EventSem, portMAX_DELAY);
    // 인터럽트 이후 처리
  }
}
  • 해당 코드를 확인해보면 ISR은 동기화 신호를 전달하고 무거운 로직은 TASK 에서 처리된다.
  • RTOS 설계 원칙에서 완벽히 부합한다.

4. Free RTOS 세마포어, 뮤텍스 관련 함수 정리

함수명 용도 ISR 사용
xSemaphoreCreateBinary 이벤트 신호
xSemaphoreCreateCounting 개수 관리
xSemaphoreTake 획득
xSemaphoreGive 반납
xSemaphoreGiveFromISR ISR 반납
xSemaphoreCreateMutex 임계구역 보호
xSemaphoreCreateRecursiveMutex 재귀 Lock
vSemaphoreDelete 삭제

 

'임베디드 FW > 펌웨어 구조' 카테고리의 다른 글

통신 펌웨어 구현 가이드  (0) 2025.03.15
자료구조 (Data Structure)  (0) 2025.03.05
FreeRTOS 사용하기  (2) 2024.10.16
실시간 운영체제 (RTOS)  (1) 2024.09.04
[F/W] 펌웨어 구현 시 메모리 관리  (0) 2024.06.25