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 |