RTOS #14

홍태준·2026년 2월 25일

RTOS

목록 보기
14/20
post-thumbnail

Week 3 Day 4: Mutex 고급

학습 목표

  • Mutex와 Binary Semaphore의 구조적 차이와 올바른 사용 기준을 이해한다
  • Priority Inheritance의 동작 원리와 한계를 파악한다
  • Recursive Mutex의 필요성과 xSemaphoreCreateRecursiveMutex() 사용법을 학습한다
  • 복합적인 자원 보호 시나리오를 구현하는 실습을 수행한다

1. Mutex vs Binary Semaphore

1.1 구조적 유사성과 본질적 차이

Mutex와 Binary Semaphore는 모두 0 또는 1의 값을 가지는 세마포어 구조체를 기반으로 합니다. 그러나 설계 목적과 소유권 메커니즘에서 근본적인 차이가 있습니다.

특성MutexBinary Semaphore
주요 목적공유 자원 보호 (상호배제)Task 간 동기화 (이벤트 알림)
소유권있음 (Take한 Task만 Give 가능)없음 (어느 Task든 Give 가능)
Priority Inheritance지원미지원
ISR에서 Give불가능가능 (xSemaphoreGiveFromISR)
초기 상태사용 가능 (1)사용 불가 (0)
Recursive 지원별도 API 사용 시 가능불가능

Mutex의 초기 상태는 반납(available)이므로, 생성 직후 첫 번째 Task가 즉시 획득할 수 있습니다. Binary Semaphore의 초기 상태는 반대로 획득 불가(unavailable)이므로, 다른 Task 또는 ISR이 Give를 호출하기 전까지 Take는 차단됩니다. 이 차이가 두 기법의 사용 목적을 명확히 구분합니다.

1.2 소유권(Ownership) 개념

Mutex는 xSemaphoreTake()를 호출한 Task가 해당 Mutex의 소유자가 됩니다. FreeRTOS는 소유자가 아닌 Task가 xSemaphoreGive()를 호출하는 것을 허용하지 않으며, 이를 위반하면 pdFALSE를 반환합니다.

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

SemaphoreHandle_t xMutex;

/* 잘못된 예: Task B가 Task A 소유의 Mutex를 반납 시도 */
void vTaskA(void *pvParameters) {
    xSemaphoreTake(xMutex, portMAX_DELAY);
    /* Mutex를 반납하지 않은 채로 종료 */
    vTaskDelay(pdMS_TO_TICKS(1000));
}

void vTaskB(void *pvParameters) {
    vTaskDelay(pdMS_TO_TICKS(500));

    /* Task A 소유의 Mutex를 Task B가 반납 시도 -> pdFALSE 반환 */
    BaseType_t result = xSemaphoreGive(xMutex);

    if(result == pdFALSE) {
        printf("[오류] 소유권 없는 Mutex Give 실패\n");
    }
}
/* 올바른 예: Take와 Give는 반드시 동일 Task에서 수행 */
void vTaskA(void *pvParameters) {
    while(1) {
        if(xSemaphoreTake(xMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
            shared_resource_access();
            xSemaphoreGive(xMutex);   /* 반드시 같은 Task에서 반납 */
        }
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

1.3 사용 목적에 따른 선택 기준

[자원 보호가 목적인가?]
    YES -> Mutex 사용
    NO  -> [ISR에서 신호를 보내야 하는가?]
               YES -> Binary Semaphore 사용
               NO  -> Binary Semaphore 또는 EventGroup 사용

Mutex를 사용해야 하는 경우:

/* UART, SPI, I2C 등 공유 주변기기 보호 */
void spi_transfer(uint8_t *data, uint16_t len) {
    if(xSemaphoreTake(xSpiMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
        HAL_SPI_Transmit(&hspi1, data, len, HAL_MAX_DELAY);
        xSemaphoreGive(xSpiMutex);
    }
}

Binary Semaphore를 사용해야 하는 경우:

/* ISR -> Task 간 이벤트 전달 */
void EXTI0_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    /* ISR에서 Binary Semaphore Give */
    xSemaphoreGiveFromISR(xBinarySemaphore, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

void vEventTask(void *pvParameters) {
    while(1) {
        /* ISR이 Give하기를 기다림 */
        xSemaphoreTake(xBinarySemaphore, portMAX_DELAY);
        process_event();
    }
}

2. Priority Inheritance (우선순위 상속)

2.1 Priority Inversion 문제 재검토

Priority Inversion은 낮은 우선순위 Task가 Mutex를 보유한 채로 중간 우선순위 Task에 의해 선점되어, 결과적으로 높은 우선순위 Task가 간접적으로 차단되는 현상입니다.

[Priority Inversion 발생 시나리오]

시각  Task L(우선순위 1)   Task M(우선순위 2)   Task H(우선순위 3)
 0    Mutex 획득
 1    작업 수행 중                               실행 대기 (Mutex 없음)
 2    선점됨              실행 시작
 3                        작업 수행 중           Mutex 대기 (Blocked)
 4                        작업 수행 중           차단 유지
 5    실행 재개            작업 완료
 6    Mutex 반납
 7                                               Mutex 획득, 실행 시작

결과: Task H (우선순위 3)가 Task M (우선순위 2)보다 늦게 실행됨

2.2 Priority Inheritance 동작 원리

FreeRTOS의 Mutex는 Priority Inheritance를 자동으로 적용합니다. 높은 우선순위 Task가 Mutex 획득을 대기하는 순간, FreeRTOS는 Mutex를 보유한 Task의 우선순위를 요청자 수준으로 일시 상승시킵니다.

[Priority Inheritance 적용 시 시나리오]

시각  Task L(우선순위 1)   Task M(우선순위 2)   Task H(우선순위 3)
 0    Mutex 획득 (우선순위 1)
 1    작업 수행 중                               Mutex 요청
 2    우선순위 3으로 상승  실행 대기             Blocked
 3    빠르게 실행 완료
 4    Mutex 반납, 우선순위 1로 복원
 5                                               Mutex 획득, 실행
 6                        실행 재개

결과: Task H가 Task M보다 먼저 실행됨 (우선순위 역전 완화)

Priority Inheritance 활성화 조건:

/* FreeRTOSConfig.h */
#define configUSE_MUTEXES    1   /* 반드시 1로 설정 */
/* Priority Inheritance는 xSemaphoreCreateMutex() 사용 시 자동 적용 */
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();

/* Binary Semaphore로 생성 시 Priority Inheritance 미적용 */
SemaphoreHandle_t xBinSem = xSemaphoreCreateBinary();  /* PI 없음 */

2.3 Priority Inheritance의 한계

Priority Inheritance는 Priority Inversion을 완전히 해결하지 않으며, 일부 시나리오에서는 여전히 문제가 발생할 수 있습니다.

한계 1: 체인형 Priority Inversion

Task L  -> Mutex A 보유
Task M  -> Mutex B 보유, Mutex A 대기
Task H  -> Mutex B 대기

결과: L의 우선순위가 M 수준으로만 상승, H 수준까지 전파 안 됨
     (FreeRTOS는 체인 전파를 완전히 지원하지 않음)

한계 2: 일시적 완화만 제공

/*
 * Priority Inheritance는 우선순위 역전을 줄이지만,
 * 완전한 해결은 Priority Ceiling Protocol (PCP) 등의
 * 별도 기법이 필요합니다.
 *
 * 안전 필수(Safety-Critical) 시스템에서는 AUTOSAR OS나
 * OSEK/VDX 등 PCP를 지원하는 RTOS를 검토해야 합니다.
 */

2.4 Priority Inheritance 동작 확인 코드

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

SemaphoreHandle_t xMutex;

void vLowPriorityTask(void *pvParameters) {
    while(1) {
        xSemaphoreTake(xMutex, portMAX_DELAY);

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

        /* 긴 작업: 이 구간에서 High Task가 Mutex를 요청하면 우선순위가 상승 */
        vTaskDelay(pdMS_TO_TICKS(300));

        printf("[Low] Mutex 반납 직전 우선순위: %lu (상속 중일 경우 상승)\n",
               (uint32_t)uxTaskPriorityGet(NULL));

        xSemaphoreGive(xMutex);

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

        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void vMidPriorityTask(void *pvParameters) {
    /* Mutex와 무관한 작업 수행, Low Task의 실행을 방해하는 역할 */
    while(1) {
        printf("[Mid] 실행 중\n");
        vTaskDelay(pdMS_TO_TICKS(150));
    }
}

void vHighPriorityTask(void *pvParameters) {
    vTaskDelay(pdMS_TO_TICKS(50));   /* Low Task가 Mutex를 먼저 획득하도록 대기 */

    while(1) {
        printf("[High] Mutex 요청 -> Low Task 우선순위 상속 유발\n");

        xSemaphoreTake(xMutex, portMAX_DELAY);

        printf("[High] Mutex 획득 성공\n");
        vTaskDelay(pdMS_TO_TICKS(50));

        xSemaphoreGive(xMutex);
        vTaskDelay(pdMS_TO_TICKS(2000));
    }
}

int main(void) {
    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);
}

3. Recursive Mutex

3.1 Recursive Mutex의 필요성

일반 Mutex는 동일 Task에서 이중으로 xSemaphoreTake()를 호출하면 Self-Deadlock이 발생합니다. 재귀 함수 호출이나 여러 레이어에서 동일 Mutex를 사용하는 경우 이 문제가 발생합니다.

SemaphoreHandle_t xMutex;

/* 문제: function_b가 function_a 내부에서 호출되며 동일 Mutex를 재획득 시도 */
void function_b(void) {
    xSemaphoreTake(xMutex, portMAX_DELAY);   /* Self-Deadlock 발생 */
    /* 작업 */
    xSemaphoreGive(xMutex);
}

void function_a(void) {
    xSemaphoreTake(xMutex, portMAX_DELAY);
    function_b();                            /* 내부에서 동일 Mutex 재획득 -> 영구 대기 */
    xSemaphoreGive(xMutex);
}

Recursive Mutex는 동일 Task가 동일 Mutex를 여러 번 획득할 수 있도록 내부 카운터를 관리합니다. 획득 횟수만큼 반납해야 완전히 해제됩니다.

[Recursive Mutex 동작]

Task A: Take -> 카운터 1
Task A: Take -> 카운터 2 (차단 없음)
Task A: Take -> 카운터 3 (차단 없음)
Task A: Give -> 카운터 2
Task A: Give -> 카운터 1
Task A: Give -> 카운터 0 (완전 해제, 다른 Task 획득 가능)

3.2 xSemaphoreCreateRecursiveMutex()

SemaphoreHandle_t xSemaphoreCreateRecursiveMutex(void);

Recursive Mutex를 동적으로 생성합니다. 활성화를 위해 FreeRTOSConfig.h에 다음 설정이 필요합니다.

/* FreeRTOSConfig.h */
#define configUSE_RECURSIVE_MUTEXES    1

Recursive Mutex 전용 API:

일반 Mutex API와 혼용하면 안 됩니다. Recursive Mutex는 전용 API를 사용해야 합니다.

일반 MutexRecursive Mutex
xSemaphoreTake()xSemaphoreTakeRecursive()
xSemaphoreGive()xSemaphoreGiveRecursive()
xSemaphoreCreateMutex()xSemaphoreCreateRecursiveMutex()
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"

SemaphoreHandle_t xRecursiveMutex;

void function_b(void) {
    /* 동일 Task에서 재획득 가능: 카운터 증가 */
    if(xSemaphoreTakeRecursive(xRecursiveMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
        printf("[function_b] Mutex 재획득 (카운터 증가)\n");

        /* 작업 수행 */

        xSemaphoreGiveRecursive(xRecursiveMutex);   /* 카운터 감소 */
        printf("[function_b] Mutex 반납 (카운터 감소)\n");
    }
}

void function_a(void) {
    if(xSemaphoreTakeRecursive(xRecursiveMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
        printf("[function_a] Mutex 첫 번째 획득 (카운터 1)\n");

        function_b();   /* 내부에서 동일 Mutex 재획득: 차단 없음 */

        xSemaphoreGiveRecursive(xRecursiveMutex);   /* 카운터 0으로 완전 해제 */
        printf("[function_a] Mutex 최종 반납 (카운터 0)\n");
    }
}

void vTask(void *pvParameters) {
    while(1) {
        function_a();
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

int main(void) {
    xRecursiveMutex = xSemaphoreCreateRecursiveMutex();
    configASSERT(xRecursiveMutex != NULL);

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

3.3 Recursive Mutex 사용 시 주의사항

주의 1: Take와 Give 횟수 반드시 일치

/* 잘못된 예: Give 횟수가 Take보다 적음 */
void bad_recursive(void) {
    xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY);   /* 카운터 1 */
    xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY);   /* 카운터 2 */
    xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY);   /* 카운터 3 */

    xSemaphoreGiveRecursive(xRecursiveMutex);                  /* 카운터 2 */
    xSemaphoreGiveRecursive(xRecursiveMutex);                  /* 카운터 1 */
    /* 카운터가 0이 되지 않아 다른 Task가 영원히 대기 */
}

/* 올바른 예 */
void good_recursive(void) {
    xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY);
    xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY);
    xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY);

    xSemaphoreGiveRecursive(xRecursiveMutex);
    xSemaphoreGiveRecursive(xRecursiveMutex);
    xSemaphoreGiveRecursive(xRecursiveMutex);   /* 카운터 0: 완전 해제 */
}

주의 2: API 혼용 금지

/* 오류: Recursive Mutex에 일반 API 혼용 */
SemaphoreHandle_t xRecMutex = xSemaphoreCreateRecursiveMutex();

xSemaphoreTakeRecursive(xRecMutex, portMAX_DELAY);   /* 올바름 */
xSemaphoreGive(xRecMutex);                           /* 오류: 일반 Give 사용 금지 */

/* 반드시 전용 API 사용 */
xSemaphoreGiveRecursive(xRecMutex);                  /* 올바름 */

주의 3: ISR에서 사용 불가

Recursive Mutex도 일반 Mutex와 동일하게 ISR에서 사용할 수 없습니다.

3.4 일반 Mutex vs Recursive Mutex 선택 기준

[Recursive Mutex가 필요한 경우]

1. 재귀 함수 내에서 Mutex 보호가 필요한 경우
   - 트리 탐색, 재귀 파싱 등

2. 레이어드 아키텍처에서 상위/하위 레이어가 동일 자원 접근 시
   - 드라이버 레이어 -> HAL 레이어 -> 동일 Mutex 사용

3. 콜백 함수가 Mutex를 보유한 상태에서 호출될 수 있는 경우

[일반 Mutex로 충분한 경우 (권장)]

- 재귀 호출이 없고 단일 진입점에서만 자원 접근
- Recursive Mutex는 카운터 관리 오버헤드가 추가되므로
  불필요한 경우 일반 Mutex 사용 권장

4. 실습: 복잡한 자원 보호

실습 1: 레이어드 드라이버에서의 Recursive Mutex

SPI 드라이버에서 상위 레이어(디바이스 드라이버)와 하위 레이어(HAL)가 동일 Mutex를 사용하는 구조를 구현합니다.

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

#define SPI_TIMEOUT_MS    50

typedef struct {
    SemaphoreHandle_t xMutex;
    uint32_t          transferCount;
} SpiDriver;

static SpiDriver spiDrv = {0};

/* HAL 레이어: 저수준 SPI 전송 (Mutex 내부에서 호출 가능) */
static BaseType_t hal_spi_transmit(const uint8_t *data, uint16_t len) {
    /*
     * Recursive Mutex를 사용하므로, 이미 Mutex를 보유한 상태에서
     * 호출되어도 차단 없이 카운터만 증가합니다.
     */
    if(xSemaphoreTakeRecursive(spiDrv.xMutex,
                               pdMS_TO_TICKS(SPI_TIMEOUT_MS)) != pdTRUE) {
        return pdFALSE;
    }

    /* HAL_SPI_Transmit() 호출을 시뮬레이션 */
    printf("[HAL] SPI 전송: %u 바이트\n", len);
    spiDrv.transferCount++;

    xSemaphoreGiveRecursive(spiDrv.xMutex);
    return pdTRUE;
}

/* 디바이스 드라이버 레이어: HAL 위에서 동작 */
BaseType_t device_write_register(uint8_t reg, uint8_t value) {
    uint8_t txBuf[2] = { reg, value };

    if(xSemaphoreTakeRecursive(spiDrv.xMutex,
                               pdMS_TO_TICKS(SPI_TIMEOUT_MS)) != pdTRUE) {
        return pdFALSE;
    }

    printf("[Device] 레지스터 0x%02X 에 0x%02X 쓰기\n", reg, value);

    /* Mutex를 보유한 채로 HAL 호출: Recursive Mutex이므로 차단 없음 */
    hal_spi_transmit(txBuf, sizeof(txBuf));

    xSemaphoreGiveRecursive(spiDrv.xMutex);
    return pdTRUE;
}

BaseType_t device_burst_write(uint8_t startReg, const uint8_t *data, uint8_t count) {
    if(xSemaphoreTakeRecursive(spiDrv.xMutex,
                               pdMS_TO_TICKS(SPI_TIMEOUT_MS)) != pdTRUE) {
        return pdFALSE;
    }

    printf("[Device] 연속 쓰기: 레지스터 0x%02X 부터 %u 바이트\n", startReg, count);

    /* 각 레지스터에 대해 반복 쓰기: 동일 Mutex를 반복 재획득 */
    for(uint8_t i = 0; i < count; i++) {
        device_write_register(startReg + i, data[i]);
    }

    xSemaphoreGiveRecursive(spiDrv.xMutex);
    return pdTRUE;
}

BaseType_t spi_driver_init(void) {
    spiDrv.xMutex = xSemaphoreCreateRecursiveMutex();

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

    spiDrv.transferCount = 0;
    return pdTRUE;
}

void vConfigTask(void *pvParameters) {
    uint8_t config[4] = { 0x01, 0x02, 0x03, 0x04 };

    while(1) {
        printf("\n[Config] 디바이스 초기화 시퀀스 시작\n");

        device_burst_write(0x10, config, 4);

        printf("[Config] 초기화 완료, 총 전송 횟수: %lu\n\n",
               spiDrv.transferCount);

        vTaskDelay(pdMS_TO_TICKS(2000));
    }
}

int main(void) {
    if(spi_driver_init() != pdTRUE) {
        while(1);
    }

    printf("=== Recursive Mutex SPI 드라이버 실습 ===\n\n");

    xTaskCreate(vConfigTask, "Config", 256, NULL, 2, NULL);
    vTaskStartScheduler();
    while(1);
}

실습 2: 다중 자원의 획득 순서 통일 및 Deadlock 방지

3개의 공유 자원을 여러 Task가 조합하여 접근하는 환경에서 획득 순서 통일 원칙을 적용합니다.

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

/*
 * 자원 획득 순서 규칙: xMutexA -> xMutexB -> xMutexC
 * 모든 Task는 이 순서를 반드시 준수해야 합니다.
 */

SemaphoreHandle_t xMutexA;   /* 자원 A: 센서 데이터 */
SemaphoreHandle_t xMutexB;   /* 자원 B: 처리 버퍼 */
SemaphoreHandle_t xMutexC;   /* 자원 C: 출력 장치 */

#define ACQUIRE_TIMEOUT_MS    200

/*
 * 안전한 다중 Mutex 획득 함수
 * 획득 실패 시 이미 보유한 Mutex를 모두 반납하고 재시도 가능한 상태로 복귀
 */
typedef enum {
    RESOURCE_NONE = 0,
    RESOURCE_A    = 1,
    RESOURCE_B    = 2,
    RESOURCE_C    = 4
} ResourceMask;

BaseType_t acquire_resources(ResourceMask mask, uint32_t timeoutMs) {
    /* 항상 A -> B -> C 순서로 획득 */
    if(mask & RESOURCE_A) {
        if(xSemaphoreTake(xMutexA,
                          pdMS_TO_TICKS(timeoutMs)) != pdTRUE) {
            printf("[오류] 자원 A 획득 타임아웃\n");
            return pdFALSE;
        }
    }

    if(mask & RESOURCE_B) {
        if(xSemaphoreTake(xMutexB,
                          pdMS_TO_TICKS(timeoutMs)) != pdTRUE) {
            printf("[오류] 자원 B 획득 타임아웃, A 반납\n");
            if(mask & RESOURCE_A) xSemaphoreGive(xMutexA);
            return pdFALSE;
        }
    }

    if(mask & RESOURCE_C) {
        if(xSemaphoreTake(xMutexC,
                          pdMS_TO_TICKS(timeoutMs)) != pdTRUE) {
            printf("[오류] 자원 C 획득 타임아웃, B/A 반납\n");
            if(mask & RESOURCE_B) xSemaphoreGive(xMutexB);
            if(mask & RESOURCE_A) xSemaphoreGive(xMutexA);
            return pdFALSE;
        }
    }

    return pdTRUE;
}

void release_resources(ResourceMask mask) {
    /* 획득 역순으로 반납: C -> B -> A */
    if(mask & RESOURCE_C) xSemaphoreGive(xMutexC);
    if(mask & RESOURCE_B) xSemaphoreGive(xMutexB);
    if(mask & RESOURCE_A) xSemaphoreGive(xMutexA);
}

/* Task 1: 자원 A, B 사용 (센서 -> 버퍼) */
void vTask_AB(void *pvParameters) {
    while(1) {
        if(acquire_resources(RESOURCE_A | RESOURCE_B,
                             ACQUIRE_TIMEOUT_MS) == pdTRUE) {
            printf("[Task_AB] 자원 A, B 작업 수행\n");
            vTaskDelay(pdMS_TO_TICKS(50));
            release_resources(RESOURCE_A | RESOURCE_B);
        }
        vTaskDelay(pdMS_TO_TICKS(300));
    }
}

/* Task 2: 자원 B, C 사용 (버퍼 -> 출력) */
void vTask_BC(void *pvParameters) {
    while(1) {
        if(acquire_resources(RESOURCE_B | RESOURCE_C,
                             ACQUIRE_TIMEOUT_MS) == pdTRUE) {
            printf("[Task_BC] 자원 B, C 작업 수행\n");
            vTaskDelay(pdMS_TO_TICKS(50));
            release_resources(RESOURCE_B | RESOURCE_C);
        }
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

/* Task 3: 자원 A, C 사용 (센서 -> 출력 직접) */
void vTask_AC(void *pvParameters) {
    while(1) {
        if(acquire_resources(RESOURCE_A | RESOURCE_C,
                             ACQUIRE_TIMEOUT_MS) == pdTRUE) {
            printf("[Task_AC] 자원 A, C 작업 수행\n");
            vTaskDelay(pdMS_TO_TICKS(50));
            release_resources(RESOURCE_A | RESOURCE_C);
        }
        vTaskDelay(pdMS_TO_TICKS(700));
    }
}

/* Task 4: 자원 A, B, C 모두 사용 */
void vTask_ABC(void *pvParameters) {
    while(1) {
        if(acquire_resources(RESOURCE_A | RESOURCE_B | RESOURCE_C,
                             ACQUIRE_TIMEOUT_MS) == pdTRUE) {
            printf("[Task_ABC] 자원 A, B, C 전체 작업 수행\n");
            vTaskDelay(pdMS_TO_TICKS(100));
            release_resources(RESOURCE_A | RESOURCE_B | RESOURCE_C);
        }
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

int main(void) {
    xMutexA = xSemaphoreCreateMutex();
    xMutexB = xSemaphoreCreateMutex();
    xMutexC = xSemaphoreCreateMutex();

    configASSERT(xMutexA != NULL);
    configASSERT(xMutexB != NULL);
    configASSERT(xMutexC != NULL);

    printf("=== 다중 자원 Deadlock 방지 실습 ===\n\n");

    xTaskCreate(vTask_AB,  "Task_AB",  256, NULL, 2, NULL);
    xTaskCreate(vTask_BC,  "Task_BC",  256, NULL, 2, NULL);
    xTaskCreate(vTask_AC,  "Task_AC",  256, NULL, 2, NULL);
    xTaskCreate(vTask_ABC, "Task_ABC", 256, NULL, 1, NULL);

    vTaskStartScheduler();
    while(1);
}

실습 3: Mutex와 Binary Semaphore 혼합 사용 (자원 보호 + 이벤트 동기화)

센서 데이터 수집을 Binary Semaphore로 동기화하고, 수집된 데이터는 Mutex로 보호하는 구조를 구현합니다.

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

SemaphoreHandle_t xDataReadySemaphore;   /* Binary Semaphore: ISR -> Task 동기화 */
SemaphoreHandle_t xDataMutex;            /* Mutex: 공유 데이터 보호 */

typedef struct {
    int16_t  adcRaw;
    float    voltage;
    uint32_t timestamp;
} SensorSample;

#define SAMPLE_BUFFER_SIZE    8

static SensorSample sampleBuffer[SAMPLE_BUFFER_SIZE];
static uint8_t      writeIndex = 0;
static uint8_t      sampleCount = 0;

/* ISR 시뮬레이션 Task: 실제 환경에서는 ADC ISR이 이 역할 수행 */
void vAdcIsrSimTask(void *pvParameters) {
    int16_t adcValue = 0;

    while(1) {
        vTaskDelay(pdMS_TO_TICKS(200));   /* ADC 변환 완료 주기 시뮬레이션 */

        adcValue = (int16_t)(1000 + (xTaskGetTickCount() % 1024));

        /*
         * 실제 ISR에서는 xSemaphoreGiveFromISR() 사용
         * 여기서는 Task에서 시뮬레이션하므로 xSemaphoreGive() 사용
         */
        xSemaphoreGive(xDataReadySemaphore);

        /*
         * ISR에서 공유 데이터를 직접 쓰지 않음
         * 원시 ADC 값 전달은 별도 Queue 사용 권장
         * 여기서는 단순화하여 전역 변수에 직접 기록
         */
        if(xSemaphoreTake(xDataMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
            sampleBuffer[writeIndex].adcRaw   = adcValue;
            sampleBuffer[writeIndex].voltage  = adcValue * 3.3f / 4096.0f;
            sampleBuffer[writeIndex].timestamp = xTaskGetTickCount();

            writeIndex = (writeIndex + 1) % SAMPLE_BUFFER_SIZE;
            if(sampleCount < SAMPLE_BUFFER_SIZE) sampleCount++;

            xSemaphoreGive(xDataMutex);
        }
    }
}

/* 데이터 처리 Task: Binary Semaphore로 이벤트 수신 후 Mutex로 데이터 접근 */
void vProcessTask(void *pvParameters) {
    while(1) {
        /* ADC 변환 완료 이벤트 대기 */
        if(xSemaphoreTake(xDataReadySemaphore,
                          portMAX_DELAY) == pdTRUE) {

            /* Mutex로 공유 데이터 접근 보호 */
            if(xSemaphoreTake(xDataMutex,
                              pdMS_TO_TICKS(50)) == pdTRUE) {

                uint8_t idx = (writeIndex == 0)
                              ? (SAMPLE_BUFFER_SIZE - 1)
                              : (writeIndex - 1);

                printf("[Process] ADC Raw: %d, Voltage: %.3fV, Tick: %lu\n",
                       sampleBuffer[idx].adcRaw,
                       sampleBuffer[idx].voltage,
                       sampleBuffer[idx].timestamp);

                xSemaphoreGive(xDataMutex);
            }
        }
    }
}

/* 통계 Task: 주기적으로 버퍼 전체를 분석 */
void vStatisticsTask(void *pvParameters) {
    while(1) {
        vTaskDelay(pdMS_TO_TICKS(2000));

        if(xSemaphoreTake(xDataMutex,
                          pdMS_TO_TICKS(100)) == pdTRUE) {

            if(sampleCount == 0) {
                xSemaphoreGive(xDataMutex);
                continue;
            }

            float   sum = 0.0f;
            float   minV = 3.3f, maxV = 0.0f;
            uint8_t count = sampleCount;

            for(uint8_t i = 0; i < count; i++) {
                float v = sampleBuffer[i].voltage;
                sum += v;
                if(v < minV) minV = v;
                if(v > maxV) maxV = v;
            }

            xSemaphoreGive(xDataMutex);

            printf("\n--- 전압 통계 (최근 %u 샘플) ---\n", count);
            printf("평균: %.3fV, 최솟값: %.3fV, 최댓값: %.3fV\n\n",
                   sum / count, minV, maxV);
        }
    }
}

int main(void) {
    xDataReadySemaphore = xSemaphoreCreateBinary();
    xDataMutex          = xSemaphoreCreateMutex();

    configASSERT(xDataReadySemaphore != NULL);
    configASSERT(xDataMutex          != NULL);

    printf("=== Mutex + Binary Semaphore 혼합 사용 실습 ===\n\n");

    xTaskCreate(vAdcIsrSimTask,  "AdcSim",   256, NULL, 3, NULL);
    xTaskCreate(vProcessTask,    "Process",  256, NULL, 2, NULL);
    xTaskCreate(vStatisticsTask, "Stats",    256, NULL, 1, NULL);

    vTaskStartScheduler();
    while(1);
}

실습 4: 종합 문제 - 계층적 드라이버 구조

UART, SPI, 상태 머신을 각각 일반 Mutex, Recursive Mutex, Binary Semaphore로 보호하는 통합 시스템을 구현합니다.

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

/* 동기화 객체 */
SemaphoreHandle_t xUartMutex;          /* 일반 Mutex: UART 보호 */
SemaphoreHandle_t xSpiRecursiveMutex;  /* Recursive Mutex: 레이어드 SPI 보호 */
SemaphoreHandle_t xAlarmSemaphore;     /* Binary Semaphore: 알람 이벤트 동기화 */

/* ============================================================
 * UART 드라이버 (일반 Mutex)
 * ============================================================ */
BaseType_t uart_log(const char *format, ...) {
    char    buf[128];
    int     len;
    va_list args;

    va_start(args, format);
    len = vsnprintf(buf, sizeof(buf), format, args);
    va_end(args);

    if(len <= 0) return pdFALSE;

    if(xSemaphoreTake(xUartMutex, pdMS_TO_TICKS(50)) != pdTRUE) {
        return pdFALSE;
    }

    printf("%s", buf);
    xSemaphoreGive(xUartMutex);
    return pdTRUE;
}

/* ============================================================
 * SPI 드라이버 (Recursive Mutex)
 * ============================================================ */
static BaseType_t spi_hal_write(uint8_t byte) {
    if(xSemaphoreTakeRecursive(xSpiRecursiveMutex,
                               pdMS_TO_TICKS(50)) != pdTRUE) {
        return pdFALSE;
    }

    /* HAL_SPI_Transmit 시뮬레이션 */
    (void)byte;

    xSemaphoreGiveRecursive(xSpiRecursiveMutex);
    return pdTRUE;
}

BaseType_t spi_device_write_reg(uint8_t reg, uint8_t val) {
    if(xSemaphoreTakeRecursive(xSpiRecursiveMutex,
                               pdMS_TO_TICKS(50)) != pdTRUE) {
        return pdFALSE;
    }

    spi_hal_write(reg);   /* Recursive: 내부 HAL 호출 가능 */
    spi_hal_write(val);

    xSemaphoreGiveRecursive(xSpiRecursiveMutex);
    return pdTRUE;
}

BaseType_t spi_device_init_sequence(void) {
    if(xSemaphoreTakeRecursive(xSpiRecursiveMutex,
                               pdMS_TO_TICKS(50)) != pdTRUE) {
        return pdFALSE;
    }

    /* 여러 레지스터 초기화: 각 호출에서 Recursive Mutex 재획득 */
    spi_device_write_reg(0x00, 0x01);   /* 활성화 */
    spi_device_write_reg(0x01, 0x10);   /* 설정 1 */
    spi_device_write_reg(0x02, 0x20);   /* 설정 2 */

    xSemaphoreGiveRecursive(xSpiRecursiveMutex);
    return pdTRUE;
}

/* ============================================================
 * Task 구현
 * ============================================================ */
void vSensorTask(void *pvParameters) {
    /* 디바이스 초기화 */
    if(spi_device_init_sequence() == pdTRUE) {
        uart_log("[Sensor] 디바이스 초기화 완료\n");
    }

    uint16_t sampleIndex = 0;

    while(1) {
        /* SPI로 센서 읽기 시뮬레이션 */
        spi_device_write_reg(0x10, (uint8_t)(sampleIndex & 0xFF));

        if(sampleIndex % 10 == 0) {
            uart_log("[Sensor] 샘플 %u 수집 완료\n", sampleIndex);
        }

        /* 임계 조건 발생 시 알람 발행 */
        if(sampleIndex == 50) {
            uart_log("[Sensor] 임계 조건 감지, 알람 발행\n");
            xSemaphoreGive(xAlarmSemaphore);
        }

        sampleIndex++;
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

void vAlarmTask(void *pvParameters) {
    while(1) {
        /* 알람 이벤트 대기 */
        xSemaphoreTake(xAlarmSemaphore, portMAX_DELAY);

        uart_log("[Alarm] 알람 수신, 대응 시퀀스 실행\n");

        /* 알람 대응: SPI 명령 전송 */
        spi_device_write_reg(0xFF, 0x01);   /* 안전 모드 진입 명령 */

        uart_log("[Alarm] 안전 모드 진입 완료\n");
    }
}

void vMonitorTask(void *pvParameters) {
    while(1) {
        vTaskDelay(pdMS_TO_TICKS(3000));
        uart_log("\n[Monitor] 시스템 정상 동작 중\n\n");
    }
}

int main(void) {
    xUartMutex         = xSemaphoreCreateMutex();
    xSpiRecursiveMutex = xSemaphoreCreateRecursiveMutex();
    xAlarmSemaphore    = xSemaphoreCreateBinary();

    configASSERT(xUartMutex         != NULL);
    configASSERT(xSpiRecursiveMutex != NULL);
    configASSERT(xAlarmSemaphore    != NULL);

    uart_log("=== 계층적 드라이버 통합 실습 ===\n\n");

    xTaskCreate(vSensorTask,  "Sensor",  256, NULL, 3, NULL);
    xTaskCreate(vAlarmTask,   "Alarm",   256, NULL, 2, NULL);
    xTaskCreate(vMonitorTask, "Monitor", 256, NULL, 1, NULL);

    vTaskStartScheduler();
    while(1);
}

5. 디버깅: Mutex 고급 문제

5.1 Recursive Mutex 카운터 불균형

/* 증상: 특정 Task가 Mutex 획득 후 영구 차단 */
void problematic_function(int depth) {
    xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY);   /* 매 호출마다 +1 */

    if(depth > 0) {
        problematic_function(depth - 1);
    }

    if(depth == 0) {
        return;   /* Give 없이 반환: 카운터가 depth+1만큼 누적 */
    }

    xSemaphoreGiveRecursive(xRecursiveMutex);   /* depth==0인 경우 실행 안 됨 */
}

/* 수정: 모든 경로에서 Give 보장 */
void fixed_function(int depth) {
    xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY);

    if(depth > 0) {
        fixed_function(depth - 1);
    }

    xSemaphoreGiveRecursive(xRecursiveMutex);   /* 반드시 모든 경로에서 실행 */
}

5.2 Priority Inheritance 미적용으로 인한 성능 저하

/*
 * 증상: 높은 우선순위 Task의 응답 시간이 예상보다 길고 불규칙
 * 원인: Binary Semaphore를 자원 보호에 잘못 사용
 */

/* 잘못된 예: 공유 자원 보호에 Binary Semaphore 사용 */
SemaphoreHandle_t xSharedDataSem = xSemaphoreCreateBinary();
/* xSemaphoreGive(xSharedDataSem) 로 초기화 필요 */

void vHighPriorityTask(void *pvParameters) {
    while(1) {
        /*
         * Binary Semaphore는 Priority Inheritance를 지원하지 않음
         * 낮은 우선순위 Task가 Semaphore를 보유하면 그대로 차단됨
         */
        xSemaphoreTake(xSharedDataSem, portMAX_DELAY);
        access_shared_data();
        xSemaphoreGive(xSharedDataSem);
    }
}

/* 수정: 공유 자원 보호에는 Mutex 사용 */
SemaphoreHandle_t xSharedDataMutex = NULL;   /* xSemaphoreCreateMutex()로 초기화 */

void vHighPriorityTask_fixed(void *pvParameters) {
    while(1) {
        /*
         * Mutex 사용 시 Priority Inheritance 자동 적용
         * 낮은 우선순위 Task의 우선순위가 일시 상승하여 빠르게 자원 해제
         */
        xSemaphoreTake(xSharedDataMutex, portMAX_DELAY);
        access_shared_data();
        xSemaphoreGive(xSharedDataMutex);
    }
}

5.3 Mutex 보유 시간 최소화

/*
 * 잘못된 예: Mutex 보유 중 시간이 걸리는 작업 수행
 * 다른 Task의 대기 시간이 증가하고 시스템 응답성이 저하됨
 */
void bad_mutex_usage(void) {
    xSemaphoreTake(xMutex, portMAX_DELAY);

    read_sensor();              /* 수십 ms 소요 */
    compute_result();           /* CPU 집약적 연산 */
    transmit_over_network();    /* 수백 ms 소요 가능 */
    update_shared_variable();   /* 실제 공유 자원 접근 */

    xSemaphoreGive(xMutex);
}

/*
 * 올바른 예: Mutex 보유 시간을 공유 자원 접근 구간으로 최소화
 */
void good_mutex_usage(void) {
    int16_t rawData;
    float   result;

    /* Mutex 없이 수행 가능한 작업을 먼저 처리 */
    rawData = read_sensor();
    result  = compute_result(rawData);

    /* 공유 자원 접근 구간만 Mutex 보호 */
    xSemaphoreTake(xMutex, portMAX_DELAY);
    update_shared_variable(result);
    xSemaphoreGive(xMutex);
}

학습 정리

오늘 배운 핵심 내용

  1. Mutex vs Binary Semaphore

    • Mutex는 소유권과 Priority Inheritance를 제공하므로 공유 자원 보호에 사용
    • Binary Semaphore는 소유권이 없으므로 ISR-Task 간 이벤트 동기화에 사용
    • 초기 상태 차이: Mutex(available), Binary Semaphore(unavailable)
  2. Priority Inheritance

    • xSemaphoreCreateMutex() 사용 시 자동 적용, configUSE_MUTEXES=1 필요
    • Mutex 보유 Task의 우선순위를 요청자 수준으로 일시 상승시켜 Priority Inversion 완화
    • 체인형 시나리오나 완전한 해결이 필요한 경우 PCP 등 별도 기법 검토 필요
  3. Recursive Mutex

    • 동일 Task에서 동일 Mutex 재획득이 필요한 경우 사용
    • configUSE_RECURSIVE_MUTEXES=1 설정 및 전용 API(TakeRecursive, GiveRecursive) 사용 필수
    • Take와 Give 횟수가 반드시 일치해야 완전 해제
  4. 복잡한 자원 보호 설계 원칙

    • 다중 Mutex 사용 시 획득 순서를 전 시스템에서 통일
    • Mutex 보유 시간을 공유 자원 접근 구간으로 최소화
    • 자원 보호(Mutex)와 이벤트 동기화(Binary Semaphore)를 역할에 따라 명확히 구분

핵심 개념 요약

개념설명
Mutex 소유권Take한 Task만 Give 가능, 위반 시 pdFALSE 반환
Binary Semaphore 초기 상태unavailable(0), 첫 Take는 Give 이후에 성공
Priority InheritanceMutex 보유 Task 우선순위를 요청자 수준으로 일시 상승
Priority Inversion낮은 우선순위 Task가 높은 우선순위 Task를 간접 차단하는 현상
Recursive Mutex동일 Task의 중복 Take를 허용, 내부 카운터로 관리
xSemaphoreTakeRecursive()Recursive Mutex 전용 획득 함수
xSemaphoreGiveRecursive()Recursive Mutex 전용 반납 함수
획득 순서 통일다중 Mutex Deadlock 방지를 위한 전 시스템 규칙
Mutex 보유 시간 최소화공유 자원 접근 구간 외 작업은 Mutex 밖에서 수행

실습 과제

과제 1: Recursive Mutex를 활용한 JSON 직렬화기 구현

중첩된 데이터 구조를 재귀적으로 직렬화하여 UART로 출력하는 함수를 Recursive Mutex로 보호하십시오.

요구사항:

  • 재귀 깊이 최소 3단계 (Object -> Array -> Value)
  • 각 단계에서 Recursive Mutex 재획득 확인 로그 출력
  • Take/Give 카운터를 직접 추적하여 균형 검증
  • 2개 이상의 Task가 동시에 직렬화 시도 시 상호배제 보장

과제 2: Priority Inheritance 효과 정량 측정

Priority Inheritance 적용 전후의 고우선순위 Task 응답 시간을 측정하고 차이를 분석하십시오.

요구사항:

  • Binary Semaphore와 Mutex를 각각 사용한 두 가지 구현 준비
  • xTaskGetTickCount()로 Mutex 요청부터 획득까지의 지연 시간 측정
  • 낮은 우선순위 Task의 Mutex 보유 시간을 의도적으로 길게 설정하여 차이 부각
  • 측정 결과를 표로 정리하고 Priority Inheritance 효과를 분석

과제 3: 자원 관리자 모듈 구현

여러 Task가 공유 자원(A, B, C)에 접근하는 요청을 중앙에서 관리하는 자원 관리자 Task를 구현하십시오.

요구사항:

  • 자원 요청은 Queue를 통해 자원 관리자 Task에 전달
  • 자원 관리자가 Mutex 획득 순서와 Deadlock 회피를 중앙 관리
  • 타임아웃으로 인한 실패 횟수를 통계로 기록
  • 30초 이상 실행 시 Deadlock 미발생 및 기아(Starvation) 미발생 검증

디버깅 팁

문제 1: Recursive Mutex와 일반 Mutex API 혼용

/*
 * 증상: 예상치 못한 동작 또는 Crash
 * 원인: Recursive Mutex에 일반 xSemaphoreTake() 사용
 */
SemaphoreHandle_t xRecMutex = xSemaphoreCreateRecursiveMutex();

/* 오류 */
xSemaphoreTake(xRecMutex, portMAX_DELAY);    /* 일반 API 사용 금지 */
xSemaphoreGive(xRecMutex);                   /* 일반 API 사용 금지 */

/* 수정 */
xSemaphoreTakeRecursive(xRecMutex, portMAX_DELAY);
xSemaphoreGiveRecursive(xRecMutex);

문제 2: Priority Inheritance 미설정

/*
 * 증상: Mutex를 사용하지만 우선순위 역전 문제가 그대로 발생
 * 원인: FreeRTOSConfig.h에서 configUSE_MUTEXES가 0
 */

/* FreeRTOSConfig.h 확인 */
#define configUSE_MUTEXES    1   /* 반드시 1로 설정 */

/*
 * 추가 확인: xSemaphoreCreateMutex()를 사용했는지 점검
 * xSemaphoreCreateBinary()는 Priority Inheritance 미지원
 */

문제 3: 다중 Mutex 획득 순서 불일치

/*
 * 증상: 간헐적 Deadlock (재현이 어렵고 타이밍에 의존)
 * 원인: 서로 다른 Task에서 Mutex 획득 순서가 다름
 * 진단: 각 Task의 Mutex 획득 순서를 코드 리뷰로 전수 검사
 */

/* 각 Task의 주석에 획득 순서를 명시하여 리뷰 시 확인 용이하게 작성 */
void vTaskA(void *pvParameters) {
    /* Mutex 획득 순서: xMutexSensor -> xMutexBuffer */
    xSemaphoreTake(xMutexSensor, portMAX_DELAY);
    xSemaphoreTake(xMutexBuffer, portMAX_DELAY);

    /* 작업 */

    xSemaphoreGive(xMutexBuffer);
    xSemaphoreGive(xMutexSensor);
}

void vTaskB(void *pvParameters) {
    /* Mutex 획득 순서: xMutexSensor -> xMutexBuffer (TaskA와 동일 순서 유지) */
    xSemaphoreTake(xMutexSensor, portMAX_DELAY);
    xSemaphoreTake(xMutexBuffer, portMAX_DELAY);

    /* 작업 */

    xSemaphoreGive(xMutexBuffer);
    xSemaphoreGive(xMutexSensor);
}
profile
당신의 코딩 메이트

0개의 댓글