Week 3 Day 3: Mutex (상호배제) - 기초

학습 목표

  • Mutex의 개념과 Binary Semaphore와의 차이를 이해한다
  • xSemaphoreCreateMutex()를 통한 Mutex 생성 방법을 학습한다
  • xSemaphoreTake() / xSemaphoreGive()의 동작 원리를 파악한다
  • Mutex를 활용하여 UART 공유 자원 보호를 실습을 통해 구현한다

1. Mutex란 무엇인가?

1.1 개요

Mutex(Mutual Exclusion)는 공유 자원에 대한 접근을 하나의 Task만 허용하도록 보장하는 동기화 기법입니다. Critical Section이 인터럽트를 비활성화하는 방식인 것과 달리, Mutex는 Task 간 소유권(Ownership) 개념을 통해 공유 자원을 보호합니다.

Critical Section은 인터럽트 자체를 차단하는 반면, Mutex는 자원에 대한 접근 권한을 토큰처럼 관리합니다. 자원을 사용하려는 Task는 반드시 토큰을 획득해야 하며, 사용이 끝나면 반납합니다. 토큰이 없는 Task는 반납될 때까지 대기 상태로 전환됩니다.

Mutex 동작 흐름:

단계동작
xSemaphoreTake() 호출Mutex 획득 시도, 이미 사용 중이면 대기(Blocked) 상태 진입
공유 자원 접근Mutex를 보유한 Task만 접근 가능
xSemaphoreGive() 호출Mutex 반납, 대기 중인 Task가 있으면 Unblocked 상태로 전환

기본 사용 구조:

#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"

SemaphoreHandle_t xMutex;

void vTaskA(void *pvParameters) {
    while(1) {
        /* Mutex 획득 시도: 최대 100ms 대기 */
        if(xSemaphoreTake(xMutex, pdMS_TO_TICKS(100)) == pdTRUE) {

            /* 공유 자원 접근 */
            shared_resource_access();

            /* Mutex 반납 */
            xSemaphoreGive(xMutex);
        }

        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

1.2 Mutex와 Binary Semaphore의 차이

Mutex와 Binary Semaphore는 구조는 유사하지만 사용 목적과 동작 방식에서 중요한 차이가 있습니다.

특성MutexBinary Semaphore
주요 목적공유 자원 보호 (상호배제)Task 간 동기화 (이벤트 알림)
소유권 개념있음 (Take한 Task만 Give 가능)없음 (어느 Task든 Give 가능)
Priority Inheritance지원미지원
ISR에서 Give불가능가능

Mutex의 가장 중요한 특징은 소유권입니다. Mutex를 xSemaphoreTake()로 획득한 Task만 xSemaphoreGive()로 반납할 수 있습니다. Binary Semaphore는 이 제약이 없으므로, 공유 자원 보호 목적에는 반드시 Mutex를 사용해야 합니다.

1.3 Priority Inheritance (우선순위 상속)

FreeRTOS의 Mutex는 Priority Inheritance 기법을 지원합니다. 이는 Priority Inversion(우선순위 역전) 문제를 완화하기 위한 메커니즘입니다.

Priority Inversion 문제 시나리오:

1. Low  Priority Task (L)가 Mutex 보유 중
2. High Priority Task (H)가 Mutex 획득 시도 -> 대기 상태
3. Mid  Priority Task (M)이 실행되며 L의 실행을 지연
4. 결과: H가 M보다 낮은 우선순위로 사실상 동작

Priority Inheritance 동작:

1. Low  Priority Task (L)가 Mutex 보유 중
2. High Priority Task (H)가 Mutex 획득 시도 -> 대기 상태
3. FreeRTOS가 L의 우선순위를 H 수준으로 일시 상승
4. L이 빠르게 실행을 완료하고 Mutex 반납
5. H가 Mutex 획득 후 실행, L은 원래 우선순위로 복원
/*
 * Priority Inheritance는 Mutex에 자동으로 적용됩니다.
 * 별도 설정 없이 xSemaphoreCreateMutex()로 생성하면 활성화됩니다.
 *
 * configUSE_MUTEXES가 1로 설정되어 있어야 합니다.
 */

/* FreeRTOSConfig.h */
#define configUSE_MUTEXES    1

Priority Inheritance는 Priority Inversion을 완전히 해결하지는 않으며, 완화하는 기법입니다. 완전한 해결을 위해서는 Priority Ceiling Protocol 등의 별도 기법이 필요합니다.

PCP(Priority Ceiling Protocol) 동작:

1. 각 자원(Mutex) 마다 우선순위 천장값 부여
2. 태스크 T가 자원을 획득하기 위해 현재 다른 태스크에 의해 잠긴 모든 자원들의 천장 값 중 가장 높은값 검색
3. 태스크 T의 우선순위가 이 최고 천장값보다 높은 경우 자원(Mutex) 획득 가능

2. xSemaphoreCreateMutex()

2.1 함수 원형 및 설명

SemaphoreHandle_t xSemaphoreCreateMutex(void);

Mutex를 동적으로 생성하고 핸들을 반환합니다. 내부적으로 FreeRTOS 힙에서 메모리를 할당하며, 생성 직후 Mutex는 반납(available) 상태입니다.

반환값:

반환값의미
NULL이 아닌 값Mutex 생성 성공, SemaphoreHandle_t 핸들 반환
NULL메모리 부족으로 생성 실패

생성 예시:

#include "FreeRTOS.h"
#include "semphr.h"

SemaphoreHandle_t xUartMutex;

void vApplicationInit(void) {
    /* Mutex 생성 */
    xUartMutex = xSemaphoreCreateMutex();

    if(xUartMutex == NULL) {
        /* 생성 실패 처리: 시스템 초기화 중단 등 */
        configASSERT(pdFALSE);
    }
}

NULL이 아닌 값을 반환했다고 해도 두 개 이상의 태스크가 서로의 자원을 기다리며 무한 대기(Deadlock)에 빠질 수 있으니 코드를 작성할 때 다음과 같이 주의해야 합니다.

//1. 무한 대기

// 위험: 자원을 얻을 때까지 영원히 기다립니다. (데드락의 원인)
xSemaphoreTake(xMutex, portMAX_DELAY); 

// 권장: 일정 시간(예: 100ms)만 기다리고 실패하면 다른 처리를 합니다.
if (xSemaphoreTake(xMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
    // 자원 사용
    xSemaphoreGive(xMutex);
} else {
    // 자원을 못 얻었을 때의 예외 처리 (데드락 회피)
}

//2. 중복 Take 금지(Self-Deadlock)

xSemaphoreTake(xMutex, portMAX_DELAY);
// ... 작업 중 ...
xSemaphoreTake(xMutex, portMAX_DELAY); // 여기서 영원히 멈춤

2.2 정적 할당 방식

동적 메모리 할당을 피해야 하는 환경에서는 xSemaphoreCreateMutexStatic()을 사용합니다.

#include "FreeRTOS.h"
#include "semphr.h"

static StaticSemaphore_t xMutexBuffer;
SemaphoreHandle_t        xMutex;

void vInit(void) {
    /* 정적 메모리를 사용하여 Mutex 생성 */
    xMutex = xSemaphoreCreateMutexStatic(&xMutexBuffer);

    /* 정적 생성은 메모리 부족으로 실패하지 않음 */
    configASSERT(xMutex != NULL);
}

임베디드 환경에서는 동적 메모리 할당의 단편화(Fragmentation) 문제를 방지하기 위해 정적 할당 방식이 권장되는 경우가 많습니다. 안전성이 중요한 시스템에서는 configSUPPORT_STATIC_ALLOCATION을 1로 설정하고 정적 방식을 사용하십시오.


3. xSemaphoreTake() / xSemaphoreGive()

3.1 xSemaphoreTake()

BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);

Mutex 획득을 시도합니다. Mutex가 이미 다른 Task에 의해 보유 중이면, xTicksToWait로 지정된 시간 동안 Blocked 상태로 대기합니다.

매개변수:

매개변수설명
xSemaphorexSemaphoreCreateMutex()로 생성한 핸들
xTicksToWait최대 대기 시간 (Tick 단위), portMAX_DELAY로 무한 대기 가능

반환값:

반환값의미
pdTRUEMutex 획득 성공
pdFALSE타임아웃 내에 획득 실패
void vTask(void *pvParameters) {
    while(1) {
        /* 방법 1: 타임아웃 지정 */
        if(xSemaphoreTake(xMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
            /* 자원 접근 */
            xSemaphoreGive(xMutex);
        } else {
            /* 타임아웃 처리 */
            printf("[경고] Mutex 획득 타임아웃\n");
        }

        /* 방법 2: 무한 대기 */
        xSemaphoreTake(xMutex, portMAX_DELAY);
        /* 자원 접근 (반드시 Give 호출 필요) */
        xSemaphoreGive(xMutex);
    }
}

3.2 xSemaphoreGive()

BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);

보유 중인 Mutex를 반납합니다. xSemaphoreTake()로 Mutex를 획득한 Task만 호출할 수 있습니다.

반환값:

반환값의미
pdTRUEMutex 반납 성공
pdFALSE반납 실패 (Mutex를 보유하지 않은 Task가 호출하는 경우 등)
void vTask(void *pvParameters) {
    while(1) {
        if(xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {

            /* 공유 자원 접근 */
            shared_resource_write(data);

            /* 반드시 Give 호출 */
            BaseType_t result = xSemaphoreGive(xMutex);

            if(result != pdTRUE) {
                /* 반납 실패: 소유권 없는 Task가 Give를 호출한 경우 */
                printf("[오류] Mutex Give 실패\n");
            }
        }

        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

3.3 Take와 Give의 대칭 원칙

xSemaphoreTake()xSemaphoreGive()는 반드시 대칭을 이루어야 합니다. 조건 분기가 있는 코드에서는 모든 실행 경로에서 Give가 호출되는지 검토해야 합니다.

/* 잘못된 예: 조건에 따라 Give가 누락됨 */
void bad_example(void) {
    xSemaphoreTake(xMutex, portMAX_DELAY);

    if(error_condition) {
        return;             /* Give 없이 반환: Mutex가 영구적으로 잠김 */
    }

    shared_data++;
    xSemaphoreGive(xMutex);
}

/* 올바른 예: 모든 경로에서 Give 보장 */
void good_example(void) {
    xSemaphoreTake(xMutex, portMAX_DELAY);

    if(!error_condition) {
        shared_data++;
    }

    xSemaphoreGive(xMutex);   /* 조건과 무관하게 항상 반납 */
}

3.4 ISR에서의 Mutex 사용 제한

Mutex는 ISR에서 사용할 수 없습니다. Priority Inheritance 메커니즘이 ISR 환경에서는 동작하지 않기 때문입니다. ISR에서 Task를 깨워야 하는 경우에는 Binary Semaphore 또는 Queue를 사용해야 합니다.

/* 오류: ISR에서 Mutex Take/Give 금지 */
void UART_IRQHandler(void) {
    xSemaphoreTake(xMutex, portMAX_DELAY);   /* ISR에서 사용 불가 */
    rxBuffer.count++;
    xSemaphoreGive(xMutex);
}

/* 올바른 방법: ISR에서는 Binary Semaphore Give만 사용 */
void UART_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    /* Binary Semaphore로 Task에 이벤트 알림 */
    xSemaphoreGiveFromISR(xBinarySemaphore, &xHigherPriorityTaskWoken);

    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

4. 실습: Mutex로 UART 보호

실습 1: 기본 UART Mutex 보호

여러 Task가 UART를 동시에 사용할 때 출력이 뒤섞이는 문제를 Mutex로 해결합니다.

#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include <stdio.h>

SemaphoreHandle_t xUartMutex;

/* Mutex로 보호되는 UART 출력 함수 */
void uart_print_safe(const char *message) {
    if(xSemaphoreTake(xUartMutex, pdMS_TO_TICKS(100)) == pdTRUE) {

        /* UART 출력 (Critical Section 없이도 단독 접근 보장) */
        printf("%s", message);

        xSemaphoreGive(xUartMutex);
    } else {
        /* 타임아웃: 필요에 따라 재시도 또는 오류 처리 */
    }
}

void vTask1(void *pvParameters) {
    char buffer[64];

    while(1) {
        snprintf(buffer, sizeof(buffer), "[Task1] 실행 중, Tick: %lu\n",
                 xTaskGetTickCount());
        uart_print_safe(buffer);

        vTaskDelay(pdMS_TO_TICKS(300));
    }
}

void vTask2(void *pvParameters) {
    char buffer[64];

    while(1) {
        snprintf(buffer, sizeof(buffer), "[Task2] 실행 중, Tick: %lu\n",
                 xTaskGetTickCount());
        uart_print_safe(buffer);

        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

void vTask3(void *pvParameters) {
    char buffer[64];

    while(1) {
        snprintf(buffer, sizeof(buffer), "[Task3] 실행 중, Tick: %lu\n",
                 xTaskGetTickCount());
        uart_print_safe(buffer);

        vTaskDelay(pdMS_TO_TICKS(700));
    }
}

int main(void) {
    xUartMutex = xSemaphoreCreateMutex();
    configASSERT(xUartMutex != NULL);

    xTaskCreate(vTask1, "Task1", 256, NULL, 2, NULL);
    xTaskCreate(vTask2, "Task2", 256, NULL, 2, NULL);
    xTaskCreate(vTask3, "Task3", 256, NULL, 2, NULL);

    vTaskStartScheduler();
    while(1);
}

실습 2: UART 드라이버 레이어 구현

실제 임베디드 환경에서 사용하는 구조처럼 UART 드라이버를 Mutex로 감싸는 레이어를 구현합니다.

#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include <stdio.h>
#include <string.h>
#include <stdarg.h>

#define UART_TX_TIMEOUT_MS  50

typedef struct {
    SemaphoreHandle_t xMutex;
    uint32_t          txCount;
    uint32_t          timeoutCount;
} UartDriver;

static UartDriver uartDrv = {0};

BaseType_t uart_driver_init(void) {
    uartDrv.xMutex = xSemaphoreCreateMutex();

    if(uartDrv.xMutex == NULL) {
        return pdFALSE;
    }

    uartDrv.txCount      = 0;
    uartDrv.timeoutCount = 0;

    return pdTRUE;
}

BaseType_t uart_send(const char *data, uint32_t length) {
    if(xSemaphoreTake(uartDrv.xMutex, pdMS_TO_TICKS(UART_TX_TIMEOUT_MS)) != pdTRUE) {
        uartDrv.timeoutCount++;
        return pdFALSE;
    }

    /* HAL_UART_Transmit() 또는 직접 레지스터 접근으로 대체 */
    for(uint32_t i = 0; i < length; i++) {
        putchar(data[i]);
    }

    uartDrv.txCount++;

    xSemaphoreGive(uartDrv.xMutex);
    return pdTRUE;
}

BaseType_t uart_printf(const char *format, ...) {
    char    buffer[128];
    int     length;
    va_list args;

    va_start(args, format);
    length = vsnprintf(buffer, sizeof(buffer), format, args);
    va_end(args);

    if(length <= 0) {
        return pdFALSE;
    }

    return uart_send(buffer, (uint32_t)length);
}

void uart_get_stats(uint32_t *txCount, uint32_t *timeoutCount) {
    if(xSemaphoreTake(uartDrv.xMutex, pdMS_TO_TICKS(UART_TX_TIMEOUT_MS)) == pdTRUE) {
        *txCount      = uartDrv.txCount;
        *timeoutCount = uartDrv.timeoutCount;
        xSemaphoreGive(uartDrv.xMutex);
    }
}

void vSensorTask(void *pvParameters) {
    int16_t temperature = 25;

    while(1) {
        temperature += (int16_t)((xTaskGetTickCount() % 3) - 1);
        uart_printf("[Sensor] 온도: %d C\n", temperature);

        vTaskDelay(pdMS_TO_TICKS(400));
    }
}

void vControlTask(void *pvParameters) {
    while(1) {
        uart_printf("[Control] 제어 루프 실행, Tick: %lu\n",
                    xTaskGetTickCount());

        vTaskDelay(pdMS_TO_TICKS(600));
    }
}

void vMonitorTask(void *pvParameters) {
    while(1) {
        vTaskDelay(pdMS_TO_TICKS(3000));

        uint32_t txCount, timeoutCount;
        uart_get_stats(&txCount, &timeoutCount);

        uart_printf("\n--- UART 통계 ---\n");
        uart_printf("송신 횟수:    %lu\n", txCount);
        uart_printf("타임아웃 횟수: %lu\n", timeoutCount);
        uart_printf("-----------------\n\n");
    }
}

int main(void) {
    if(uart_driver_init() != pdTRUE) {
        while(1);   /* 드라이버 초기화 실패 */
    }

    uart_printf("=== UART Mutex 보호 실습 ===\n\n");

    xTaskCreate(vSensorTask,  "Sensor",  256, NULL, 2, NULL);
    xTaskCreate(vControlTask, "Control", 256, NULL, 2, NULL);
    xTaskCreate(vMonitorTask, "Monitor", 256, NULL, 1, NULL);

    vTaskStartScheduler();
    while(1);
}

실습 3: Priority Inheritance 동작 확인

낮은 우선순위 Task가 Mutex를 보유한 상태에서 높은 우선순위 Task가 Mutex를 요청할 때 우선순위 상속이 발생하는 과정을 확인합니다.

#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include <stdio.h>

SemaphoreHandle_t xMutex;

/* 낮은 우선순위 Task: Mutex 보유 후 긴 작업 수행 */
void vLowPriorityTask(void *pvParameters) {
    while(1) {
        printf("[Low] Mutex 획득 시도\n");

        xSemaphoreTake(xMutex, portMAX_DELAY);
        printf("[Low] Mutex 획득 완료\n");
        printf("[Low] 현재 우선순위: %lu\n",
               (uint32_t)uxTaskPriorityGet(NULL));

        /* 긴 작업 시뮬레이션 */
        vTaskDelay(pdMS_TO_TICKS(500));

        printf("[Low] 작업 완료, Mutex 반납\n");
        printf("[Low] 반납 전 우선순위: %lu\n",
               (uint32_t)uxTaskPriorityGet(NULL));

        xSemaphoreGive(xMutex);

        printf("[Low] 반납 후 우선순위: %lu (복원됨)\n",
               (uint32_t)uxTaskPriorityGet(NULL));

        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

/* 중간 우선순위 Task: Mutex와 무관하게 실행 */
void vMidPriorityTask(void *pvParameters) {
    while(1) {
        printf("[Mid] 실행 중 (Mutex 불필요)\n");
        vTaskDelay(pdMS_TO_TICKS(200));
    }
}

/* 높은 우선순위 Task: Mutex 요청 -> Low Task 우선순위 상속 유발 */
void vHighPriorityTask(void *pvParameters) {
    /* Low Task가 Mutex를 획득할 시간을 줌 */
    vTaskDelay(pdMS_TO_TICKS(100));

    while(1) {
        printf("[High] Mutex 획득 시도 -> Low Task 우선순위 상승 예상\n");

        xSemaphoreTake(xMutex, portMAX_DELAY);
        printf("[High] Mutex 획득 성공\n");

        /* 공유 자원 접근 */
        vTaskDelay(pdMS_TO_TICKS(100));

        xSemaphoreGive(xMutex);
        printf("[High] Mutex 반납\n");

        vTaskDelay(pdMS_TO_TICKS(2000));
    }
}

int main(void) {
    printf("=== Priority Inheritance 동작 확인 ===\n\n");

    xMutex = xSemaphoreCreateMutex();
    configASSERT(xMutex != NULL);

    /* 우선순위: Low=1, Mid=2, High=3 */
    xTaskCreate(vLowPriorityTask,  "Low",  256, NULL, 1, NULL);
    xTaskCreate(vMidPriorityTask,  "Mid",  256, NULL, 2, NULL);
    xTaskCreate(vHighPriorityTask, "High", 256, NULL, 3, NULL);

    vTaskStartScheduler();
    while(1);
}

실습 4: 종합 문제 - 다중 공유 자원 보호

여러 공유 자원(UART, 센서 데이터, 상태 레지스터)을 각각의 Mutex로 독립적으로 보호하는 시스템을 구현합니다.

#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include <stdio.h>

/* 공유 자원별 Mutex */
SemaphoreHandle_t xUartMutex;
SemaphoreHandle_t xSensorMutex;
SemaphoreHandle_t xStateMutex;

typedef struct {
    int16_t  temperature;
    uint16_t humidity;
    uint32_t lastUpdate;
} SensorData;

typedef enum {
    STATE_IDLE = 0,
    STATE_RUNNING,
    STATE_ERROR
} SystemState;

SensorData    gSensorData = {0};
SystemState   gSystemState = STATE_IDLE;

void safe_uart_print(const char *msg) {
    if(xSemaphoreTake(xUartMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
        printf("%s", msg);
        xSemaphoreGive(xUartMutex);
    }
}

void update_sensor_data(int16_t temp, uint16_t hum) {
    if(xSemaphoreTake(xSensorMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
        gSensorData.temperature = temp;
        gSensorData.humidity    = hum;
        gSensorData.lastUpdate  = xTaskGetTickCount();
        xSemaphoreGive(xSensorMutex);
    }
}

SensorData read_sensor_data(void) {
    SensorData local = {0};

    if(xSemaphoreTake(xSensorMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
        local = gSensorData;
        xSemaphoreGive(xSensorMutex);
    }

    return local;
}

void set_system_state(SystemState newState) {
    if(xSemaphoreTake(xStateMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
        gSystemState = newState;
        xSemaphoreGive(xStateMutex);
    }
}

SystemState get_system_state(void) {
    SystemState state = STATE_IDLE;

    if(xSemaphoreTake(xStateMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
        state = gSystemState;
        xSemaphoreGive(xStateMutex);
    }

    return state;
}

void vSensorWriteTask(void *pvParameters) {
    int16_t  temp = 20;
    uint16_t hum  = 50;

    set_system_state(STATE_RUNNING);
    safe_uart_print("[SensorWrite] 시스템 상태: RUNNING\n");

    while(1) {
        temp += (int16_t)((xTaskGetTickCount() % 3) - 1);
        hum  += (uint16_t)((xTaskGetTickCount() % 5) - 2);

        update_sensor_data(temp, hum);

        char msg[64];
        snprintf(msg, sizeof(msg),
                 "[SensorWrite] 온도: %d, 습도: %u 업데이트\n", temp, hum);
        safe_uart_print(msg);

        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

void vSensorReadTask(void *pvParameters) {
    while(1) {
        vTaskDelay(pdMS_TO_TICKS(800));

        SensorData data   = read_sensor_data();
        SystemState state = get_system_state();

        char msg[128];
        snprintf(msg, sizeof(msg),
                 "[SensorRead] 온도: %d, 습도: %u, 갱신시각: %lu, 상태: %d\n",
                 data.temperature, data.humidity, data.lastUpdate, (int)state);
        safe_uart_print(msg);
    }
}

void vWatchdogTask(void *pvParameters) {
    while(1) {
        vTaskDelay(pdMS_TO_TICKS(2000));

        SensorData  data  = read_sensor_data();
        SystemState state = get_system_state();

        if(data.temperature > 40 || data.temperature < 0) {
            set_system_state(STATE_ERROR);
            safe_uart_print("[Watchdog] 온도 범위 초과: STATE_ERROR 설정\n");
        } else if(state == STATE_ERROR) {
            set_system_state(STATE_RUNNING);
            safe_uart_print("[Watchdog] 온도 정상 복귀: STATE_RUNNING 복원\n");
        }
    }
}

int main(void) {
    xUartMutex   = xSemaphoreCreateMutex();
    xSensorMutex = xSemaphoreCreateMutex();
    xStateMutex  = xSemaphoreCreateMutex();

    configASSERT(xUartMutex   != NULL);
    configASSERT(xSensorMutex != NULL);
    configASSERT(xStateMutex  != NULL);

    safe_uart_print("=== 다중 공유 자원 Mutex 보호 ===\n\n");

    xTaskCreate(vSensorWriteTask, "SensorWrite", 256, NULL, 3, NULL);
    xTaskCreate(vSensorReadTask,  "SensorRead",  256, NULL, 2, NULL);
    xTaskCreate(vWatchdogTask,    "Watchdog",    256, NULL, 1, NULL);

    vTaskStartScheduler();
    while(1);
}

5. 디버깅: Mutex 관련 문제

5.1 Deadlock (교착 상태) 감지

두 Task가 서로 상대방이 보유한 Mutex를 기다리는 상황을 Deadlock이라 합니다.

/*
 * Deadlock 시나리오:
 *
 * Task A: xMutex1 보유 -> xMutex2 대기
 * Task B: xMutex2 보유 -> xMutex1 대기
 * 결과: 두 Task 모두 영구 대기 상태
 */

/* 잘못된 구조 */
void vTaskA(void *pvParameters) {
    xSemaphoreTake(xMutex1, portMAX_DELAY);
    vTaskDelay(pdMS_TO_TICKS(10));          /* 이 사이에 Task B가 Mutex2 획득 */
    xSemaphoreTake(xMutex2, portMAX_DELAY); /* Deadlock 발생 */

    xSemaphoreGive(xMutex2);
    xSemaphoreGive(xMutex1);
}

void vTaskB(void *pvParameters) {
    xSemaphoreTake(xMutex2, portMAX_DELAY);
    xSemaphoreTake(xMutex1, portMAX_DELAY); /* Deadlock 발생 */

    xSemaphoreGive(xMutex1);
    xSemaphoreGive(xMutex2);
}

/* 올바른 구조: 획득 순서를 통일 */
void vTaskA_fixed(void *pvParameters) {
    xSemaphoreTake(xMutex1, portMAX_DELAY); /* 항상 Mutex1 먼저 */
    xSemaphoreTake(xMutex2, portMAX_DELAY);

    xSemaphoreGive(xMutex2);
    xSemaphoreGive(xMutex1);
}

void vTaskB_fixed(void *pvParameters) {
    xSemaphoreTake(xMutex1, portMAX_DELAY); /* 항상 Mutex1 먼저 */
    xSemaphoreTake(xMutex2, portMAX_DELAY);

    xSemaphoreGive(xMutex2);
    xSemaphoreGive(xMutex1);
}

5.2 타임아웃을 이용한 Deadlock 방어

/* portMAX_DELAY 대신 타임아웃을 설정하여 Deadlock 상황을 감지 */
void vSafeTask(void *pvParameters) {
    while(1) {
        if(xSemaphoreTake(xMutex1, pdMS_TO_TICKS(500)) != pdTRUE) {
            printf("[오류] Mutex1 획득 타임아웃: Deadlock 의심\n");
            continue;
        }

        if(xSemaphoreTake(xMutex2, pdMS_TO_TICKS(500)) != pdTRUE) {
            printf("[오류] Mutex2 획득 타임아웃: Deadlock 의심\n");
            xSemaphoreGive(xMutex1);    /* 보유 중인 Mutex 반납 후 재시도 */
            continue;
        }

        /* 두 Mutex 모두 획득 성공 */
        shared_operation();

        xSemaphoreGive(xMutex2);
        xSemaphoreGive(xMutex1);

        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

5.3 Mutex 미반납으로 인한 자원 고갈

/* 문제: 예외 경로에서 Mutex 미반납 */
void bad_resource_access(void) {
    xSemaphoreTake(xMutex, portMAX_DELAY);

    if(invalid_state()) {
        return;                     /* Mutex 반납 누락 */
    }

    process_data();
    xSemaphoreGive(xMutex);
}

/* 해결: 단일 퇴출 지점 패턴 사용 */
void good_resource_access(void) {
    xSemaphoreTake(xMutex, portMAX_DELAY);

    if(!invalid_state()) {
        process_data();
    }

    xSemaphoreGive(xMutex);         /* 항상 도달하는 단일 반납 지점 */
}

학습 정리

오늘 배운 핵심 내용

  1. Mutex의 개념

    • 소유권 기반의 상호배제 기법으로 공유 자원 보호에 적합
    • Critical Section(인터럽트 비활성화)과 달리 Task 스케줄링을 유지
    • Priority Inheritance를 통해 Priority Inversion 문제를 완화
  2. xSemaphoreCreateMutex()

    • Mutex를 동적으로 생성하며 NULL 반환 시 오류 처리 필수
    • 정적 할당이 필요한 환경에서는 xSemaphoreCreateMutexStatic() 사용
  3. xSemaphoreTake() / xSemaphoreGive()

    • 획득과 반납은 반드시 동일 Task에서 대칭 호출
    • 타임아웃을 활용하여 Deadlock 상황을 조기에 감지
    • ISR에서는 Mutex 사용 불가, Binary Semaphore 또는 Queue 사용
  4. 실전 설계 원칙

    • Mutex 보유 시간을 최소화하여 다른 Task의 대기 시간 감소
    • 여러 Mutex를 사용하는 경우 획득 순서를 통일하여 Deadlock 방지
    • 모든 실행 경로에서 Give가 호출되는지 반드시 검토

핵심 개념 요약

개념설명
xSemaphoreCreateMutex()Mutex 생성, Priority Inheritance 자동 적용
xSemaphoreTake()Mutex 획득, 실패 시 Blocked 상태로 대기
xSemaphoreGive()Mutex 반납, 대기 Task 중 Unblocked 처리
Priority InheritanceMutex 보유 Task의 우선순위를 요청자 수준으로 일시 상승
Priority Inversion낮은 우선순위 Task가 높은 우선순위 Task를 간접적으로 차단하는 현상
Deadlock두 Task가 서로의 Mutex를 무한 대기하는 교착 상태
소유권Mutex를 Take한 Task만 Give 가능하다는 제약

실습 과제

과제 1: 멀티 Task UART 로거 구현

4개 이상의 Task가 동시에 UART를 통해 로그를 출력하는 시스템을 Mutex로 보호하여 출력이 섞이지 않도록 구현하십시오.

요구사항:

  • 각 Task는 고유한 ID와 출력 주기를 가짐
  • UART 접근을 단일 Mutex로 보호
  • 타임아웃 발생 시 카운터 증가 및 주기적 출력
  • 1분 이상 실행 시 출력 누락 없음을 검증

과제 2: Mutex와 Critical Section 성능 비교

동일한 공유 자원 보호 작업에 대해 Mutex와 Critical Section의 동작 차이를 측정하고 분석하십시오.

요구사항:

  • 동일한 카운터 증가 작업을 각각 Mutex, Critical Section으로 구현
  • 각 방법의 수행 시간을 xTaskGetTickCount()로 측정
  • 인터럽트 응답 지연 비교 (Critical Section의 인터럽트 차단 시간 측정)
  • 결과를 표로 정리하고 언제 어느 방법을 선택해야 하는지 정리

과제 3: 다중 Mutex Deadlock 방지 설계

3개의 공유 자원(A, B, C)을 각각의 Mutex로 보호하고, 여러 Task가 2개 이상의 자원을 동시에 사용해야 하는 시나리오에서 Deadlock 없이 동작하는 시스템을 구현하십시오.

요구사항:

  • Task마다 접근하는 자원 조합이 다름 (AB, BC, AC 등)
  • 획득 순서 통일 원칙 적용
  • 500ms 타임아웃으로 Deadlock 감지 로직 포함
  • 30초 이상 실행 시 Deadlock 미발생 검증

디버깅 팁

문제 1: Mutex 생성 직후 Take 실패

/* 원인: Mutex 초기화 전에 Task에서 사용 시도 */
SemaphoreHandle_t xMutex;

void vTask(void *pvParameters) {
    /* Mutex가 아직 NULL일 수 있음 */
    xSemaphoreTake(xMutex, portMAX_DELAY);   /* Crash 위험 */
}

int main(void) {
    /* 해결: Task 생성 전에 반드시 Mutex 먼저 생성 */
    xMutex = xSemaphoreCreateMutex();
    configASSERT(xMutex != NULL);

    xTaskCreate(vTask, "Task", 256, NULL, 2, NULL);
    vTaskStartScheduler();
}

문제 2: 같은 Task에서 Mutex 이중 획득

/* 원인: 재귀 호출 또는 중첩 호출로 인한 이중 Take */
void function_a(void) {
    xSemaphoreTake(xMutex, portMAX_DELAY);
    function_b();                            /* function_b 내부에서도 Take 시도 */
    xSemaphoreGive(xMutex);
}

void function_b(void) {
    xSemaphoreTake(xMutex, portMAX_DELAY);   /* 자기 자신이 보유한 Mutex를 다시 Take -> Deadlock */
    xSemaphoreGive(xMutex);
}

/*
 * 해결 방법 1: Recursive Mutex 사용 (xSemaphoreCreateRecursiveMutex)
 * 해결 방법 2: 내부 함수가 Mutex를 직접 사용하지 않도록 설계 분리
 */

문제 3: ISR에서 Mutex 사용

/* 오류: ISR에서 Mutex Take/Give 사용 */
void TIM2_IRQHandler(void) {
    xSemaphoreTake(xMutex, 0);               /* ISR에서 사용 금지 */
    timerCount++;
    xSemaphoreGive(xMutex);
}

/* 해결: ISR에서는 taskENTER_CRITICAL_FROM_ISR() 사용 */
void TIM2_IRQHandler(void) {
    UBaseType_t uxSaved = taskENTER_CRITICAL_FROM_ISR();
    timerCount++;
    taskEXIT_CRITICAL_FROM_ISR(uxSaved);
}
profile
당신의 코딩 메이트

0개의 댓글