RTOS #12

홍태준·2026년 2월 19일

RTOS

목록 보기
12/20
post-thumbnail

Week 3 Day 2: Critical Section 보호

학습 목표

  • taskENTER_CRITICAL() / taskEXIT_CRITICAL()의 동작 원리를 이해한다
  • ISR 환경에서의 Critical Section 처리 방법을 학습한다
  • Critical Section 사용 시 주의사항을 파악한다
  • 공유 변수 보호를 실습을 통해 구현하고 검증한다

1. taskENTER_CRITICAL() / taskEXIT_CRITICAL()

1.1 개요

FreeRTOS에서 Critical Section은 taskENTER_CRITICAL()taskEXIT_CRITICAL() 매크로를 통해 구현됩니다. 이 매크로 쌍은 내부적으로 인터럽트를 비활성화하여 공유 자원에 대한 접근을 원자적으로 보장합니다.

말 그대로 taskENTER/taskEXIT이 실행되는 구간에선 인터럽트를 차단하는 방식입니다. 인터럽트가 발생하지 않으니 컨텍스트 스위칭도 따라서 발생하지 않는 방식입니다.

동작 원리:

단계동작
taskENTER_CRITICAL() 호출configMAX_SYSCALL_INTERRUPT_PRIORITY 이하의 인터럽트 비활성화
Critical Section 실행공유 자원 접근 (컨텍스트 스위칭 불가)
taskEXIT_CRITICAL() 호출인터럽트 재활성화, 이전 인터럽트 상태 복원

기본 사용 구조:

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

volatile uint32_t sharedCounter = 0;

void safe_increment(void) {
    taskENTER_CRITICAL();

    /* 이 구간은 인터럽트와 컨텍스트 스위칭으로부터 보호됨 */
    uint32_t temp = sharedCounter;
    temp++;
    sharedCounter = temp;

    taskEXIT_CRITICAL();
}

1.2 중첩 호출 (Nesting)

FreeRTOS의 Critical Section은 중첩 호출을 지원합니다. 내부적으로 중첩 카운터를 유지하며, taskEXIT_CRITICAL()은 카운터가 0이 될 때만 인터럽트를 실제로 재활성화합니다.

void outer_function(void) {
    taskENTER_CRITICAL();       /* 중첩 카운터: 1, 인터럽트 비활성화 */

    inner_function();           /* 내부에서 추가 중첩 호출 */

    taskEXIT_CRITICAL();        /* 중첩 카운터: 0, 인터럽트 재활성화 */
}

void inner_function(void) {
    taskENTER_CRITICAL();       /* 중첩 카운터: 2, 이미 비활성화 상태 유지 */

    /* 공유 자원 접근 */
    sharedCounter++;

    taskEXIT_CRITICAL();        /* 중첩 카운터: 1, 아직 인터럽트 비활성화 유지 */
}

중첩 호출 시 taskENTER_CRITICAL()taskEXIT_CRITICAL()의 호출 횟수는 반드시 일치해야 합니다. 불일치가 발생하면 인터럽트가 영구적으로 비활성화되거나 조기에 재활성화되는 문제가 발생합니다.

1.3 인터럽트 우선순위와의 관계

FreeRTOS는 configMAX_SYSCALL_INTERRUPT_PRIORITY를 기준으로 인터럽트를 분류합니다.

높은 우선순위
    |
    |   [ 우선순위 레벨 1 ]  <-- taskENTER_CRITICAL()로 차단되지 않음
    |   [ 우선순위 레벨 2 ]  <-- FreeRTOS API 호출 불가 ISR
    |   ── configMAX_SYSCALL_INTERRUPT_PRIORITY ──
    |   [ 우선순위 레벨 3 ]  <-- taskENTER_CRITICAL()로 차단됨
    |   [ 우선순위 레벨 4 ]  <-- FreeRTOS API 호출 가능 ISR
낮은 우선순위

configMAX_SYSCALL_INTERRUPT_PRIORITY보다 높은 우선순위의 인터럽트는 Critical Section 내에서도 실행될 수 있습니다. 따라서 이 구간에서는 FreeRTOS API를 절대 호출해서는 안 됩니다.

/* FreeRTOSConfig.h 예시 */
#define configMAX_SYSCALL_INTERRUPT_PRIORITY    ( 5 << (8 - configPRIO_BITS) )

2. ISR에서의 Critical Section

2.1 ISR 전용 API

일반 Task에서 사용하는 taskENTER_CRITICAL() / taskEXIT_CRITICAL()은 ISR(Interrupt Service Routine)에서 사용할 수 없습니다. ISR 내부에서는 별도의 매크로를 사용해야 합니다.

환경진입 매크로퇴출 매크로
TasktaskENTER_CRITICAL()taskEXIT_CRITICAL()
ISRtaskENTER_CRITICAL_FROM_ISR()taskEXIT_CRITICAL_FROM_ISR()

ISR에서의 사용 구조:

void UART_IRQHandler(void) {
    /* 이전 인터럽트 마스크 상태를 저장 */
    UBaseType_t uxSavedInterruptStatus;
    uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();

    /* Critical Section */
    if(rxBuffer.count < BUFFER_SIZE) {
        rxBuffer.data[rxBuffer.head] = UART->DR;
        rxBuffer.head = (rxBuffer.head + 1) % BUFFER_SIZE;
        rxBuffer.count++;
    }

    /* 저장된 상태로 복원 */
    taskEXIT_CRITICAL_FROM_ISR(uxSavedInterruptStatus);
}

taskENTER_CRITICAL_FROM_ISR()은 반환값으로 이전 인터럽트 마스크 상태를 반환합니다. 이 값을 taskEXIT_CRITICAL_FROM_ISR()에 전달해야 올바르게 이전 상태로 복원됩니다.

2.2 ISR과 Task 간 공유 자원 접근

ISR과 Task가 동일한 자원을 공유할 때, 양쪽 모두 적절한 보호 매크로를 사용해야 합니다.

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

#define BUFFER_SIZE 32

typedef struct {
    volatile uint8_t  data[BUFFER_SIZE];
    volatile uint32_t head;
    volatile uint32_t tail;
    volatile uint32_t count;
} SharedBuffer;

SharedBuffer uartRxBuffer = {0};

/* ISR에서 데이터 쓰기 */
void UART_RX_IRQHandler(void) {
    UBaseType_t uxSavedInterruptStatus;
    uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();

    if(uartRxBuffer.count < BUFFER_SIZE) {
        uartRxBuffer.data[uartRxBuffer.head] = UART->DR;
        uartRxBuffer.head = (uartRxBuffer.head + 1) % BUFFER_SIZE;
        uartRxBuffer.count++;
    }

    taskEXIT_CRITICAL_FROM_ISR(uxSavedInterruptStatus);
}

/* Task에서 데이터 읽기 */
void vUartProcessTask(void *pvParameters) {
    uint8_t byte;

    while(1) {
        taskENTER_CRITICAL();

        if(uartRxBuffer.count > 0) {
            byte = uartRxBuffer.data[uartRxBuffer.tail];
            uartRxBuffer.tail = (uartRxBuffer.tail + 1) % BUFFER_SIZE;
            uartRxBuffer.count--;
        }

        taskEXIT_CRITICAL();

        /* 읽어온 데이터 처리 */
        process_byte(byte);

        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

2.3 ISR Critical Section의 동작 차이

taskENTER_CRITICAL_FROM_ISR()은 이미 ISR 내부에서 실행 중이므로, 호출 시 추가적인 인터럽트 마스킹 레벨 조정만 수행하고 스케줄러 중단은 포함하지 않습니다. 이는 Task용 Critical Section과의 핵심적인 차이입니다.

/* Task용: 스케줄러 + 인터럽트 차단 */
taskENTER_CRITICAL();
    /* ... */
taskEXIT_CRITICAL();

/* ISR용: 추가 인터럽트 레벨 차단만 수행 */
UBaseType_t uxSaved = taskENTER_CRITICAL_FROM_ISR();
    /* ... */
taskEXIT_CRITICAL_FROM_ISR(uxSaved);

3. 사용 시 주의사항

3.1 Critical Section 내부에서 금지되는 동작

Critical Section은 인터럽트를 비활성화하므로, 내부에서 시스템 응답성에 영향을 주는 동작을 수행해서는 안 됩니다.

void wrong_usage(void) {
    taskENTER_CRITICAL();

    /* 금지: vTaskDelay() 호출 - 스케줄러 사용 불가 */
    vTaskDelay(pdMS_TO_TICKS(100));

    /* 금지: xSemaphoreTake() 등 블로킹 API 호출 */
    xSemaphoreTake(xMutex, portMAX_DELAY);

    /* 금지: printf() 등 긴 수행 시간이 소요되는 함수 */
    printf("Critical Section 내부\n");

    taskEXIT_CRITICAL();
}
void correct_usage(void) {
    taskENTER_CRITICAL();

    /* 허용: 단순 변수 접근 및 산술 연산 */
    sharedCounter++;
    sharedFlag = pdTRUE;

    /* 허용: 구조체 멤버 읽기/쓰기 */
    sensorData.temperature = read_sensor();

    taskEXIT_CRITICAL();
}

3.2 Critical Section의 최소화

Critical Section은 가능한 한 짧게 유지해야 합니다. 긴 Critical Section은 인터럽트 응답 지연(Interrupt Latency)을 증가시켜 실시간 성능을 저하시킵니다.

/* 잘못된 예: 불필요하게 긴 Critical Section */
void bad_example(void) {
    taskENTER_CRITICAL();

    uint32_t result = 0;
    for(int i = 0; i < 1000; i++) {
        result += heavy_computation(i);     /* 긴 연산 */
    }
    sharedResult = result;

    taskEXIT_CRITICAL();
}

/* 올바른 예: 공유 자원 접근만 보호 */
void good_example(void) {
    uint32_t result = 0;

    /* 공유 자원 접근 없는 연산은 Critical Section 외부에서 수행 */
    for(int i = 0; i < 1000; i++) {
        result += heavy_computation(i);
    }

    /* 공유 자원 접근 구간만 보호 */
    taskENTER_CRITICAL();
    sharedResult = result;
    taskEXIT_CRITICAL();
}

3.3 스케줄러 일시 중단과의 비교

인터럽트 비활성화 없이 Task 간 경쟁만 방지하려면 스케줄러 일시 중단을 사용할 수 있습니다. 단, 이 방법은 ISR로부터는 보호되지 않습니다.

/* 방법 1: Critical Section (인터럽트 + Task 모두 차단) */
void method_critical(void) {
    taskENTER_CRITICAL();
    shared_data++;
    taskEXIT_CRITICAL();
}

/* 방법 2: 스케줄러 일시 중단 (Task만 차단, ISR은 계속 실행) */
void method_scheduler(void) {
    vTaskSuspendAll();

    shared_data++;              /* ISR로부터는 보호되지 않음 */

    xTaskResumeAll();
}
특성taskENTER_CRITICAL()vTaskSuspendAll()
Task 간 보호가능가능
ISR로부터 보호가능불가능
인터럽트 응답차단됨유지됨
적합한 상황짧은 원자적 연산ISR과 공유하지 않는 긴 연산

3.4 portDISABLE_INTERRUPTS() / portENABLE_INTERRUPTS()와의 차이

portDISABLE_INTERRUPTS() / portENABLE_INTERRUPTS()는 FreeRTOS 스케줄러와 독립적으로 모든 인터럽트를 제어합니다. 중첩 카운터를 유지하지 않으므로 일반적으로는 taskENTER_CRITICAL() 사용이 권장됩니다.

일반적으론 인터럽트를 하드웨어적으로 차단하는 방식보다 크리티컬 섹션에 진입/탈출 하는 방식이 더 안전하므로 이 방식은 그냥 있다 정도로만 알아두시기 권장합니다.

/* portDISABLE_INTERRUPTS(): 중첩 추적 없음, 주의해서 사용 */
portDISABLE_INTERRUPTS();
sharedCounter++;
portENABLE_INTERRUPTS();

/* taskENTER_CRITICAL(): 중첩 추적 포함, 일반적으로 권장 */
taskENTER_CRITICAL();
sharedCounter++;
taskEXIT_CRITICAL();

4. 실습: 공유 변수 보호

실습 1: 기본 Critical Section 적용

Race Condition이 발생하는 카운터 예제에 Critical Section을 적용하여 데이터 일관성을 보장합니다.

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

#define NUM_INCREMENTS  10000
#define NUM_TASKS       3

volatile uint32_t sharedCounter     = 0;
volatile uint32_t taskCompletionCount = 0;

void vSafeCounterTask(void *pvParameters) {
    int taskId = (int)pvParameters;

    printf("[Task %d] 증가 시작\n", taskId);

    for(int i = 0; i < NUM_INCREMENTS; i++) {
        taskENTER_CRITICAL();

        sharedCounter++;

        taskEXIT_CRITICAL();

        if(i % 1000 == 0) {
            taskYIELD();
        }
    }

    printf("[Task %d] 증가 완료\n", taskId);

    taskENTER_CRITICAL();
    taskCompletionCount++;
    taskEXIT_CRITICAL();

    vTaskDelete(NULL);
}

void vResultTask(void *pvParameters) {
    while(1) {
        uint32_t count;

        taskENTER_CRITICAL();
        count = taskCompletionCount;
        taskEXIT_CRITICAL();

        if(count >= NUM_TASKS) {
            uint32_t expectedValue = NUM_INCREMENTS * NUM_TASKS;
            uint32_t actualValue   = sharedCounter;

            printf("\n=== 테스트 결과 ===\n");
            printf("기대값:      %lu\n", expectedValue);
            printf("실제값:      %lu\n", actualValue);

            if(actualValue == expectedValue) {
                printf("상태: 정상 - Critical Section 보호 성공\n");
            } else {
                printf("상태: 오류 - 예상치 못한 불일치 발생\n");
            }

            vTaskDelete(NULL);
        }

        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

int main(void) {
    printf("=== Critical Section 카운터 보호 ===\n");
    printf("각 Task가 %d번씩 카운터 증가\n", NUM_INCREMENTS);
    printf("총 %d개 Task 실행\n\n", NUM_TASKS);

    for(int i = 0; i < NUM_TASKS; i++) {
        char taskName[16];
        snprintf(taskName, sizeof(taskName), "Counter%d", i + 1);
        xTaskCreate(vSafeCounterTask, taskName, 256, (void*)(i + 1), 2, NULL);
    }

    xTaskCreate(vResultTask, "Result", 256, NULL, 1, NULL);

    vTaskStartScheduler();
    while(1);
}

실습 2: 구조체 일관성 보호

여러 멤버로 구성된 구조체에 대해 Critical Section을 적용하여 일관된 읽기/쓰기를 보장합니다.

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

typedef struct {
    uint32_t timestamp;
    int16_t  temperature;
    uint16_t humidity;
    uint8_t  checksum;
} SensorData;

SensorData sensorReading = {0};

uint8_t calculate_checksum(SensorData *data) {
    return (uint8_t)((data->timestamp + data->temperature + data->humidity) & 0xFF);
}

void vSensorUpdateTask(void *pvParameters) {
    uint32_t counter = 0;

    while(1) {
        SensorData newData;
        newData.timestamp   = xTaskGetTickCount();
        newData.temperature = (int16_t)(20 + (counter % 10));
        newData.humidity    = (uint16_t)(50 + (counter % 20));
        newData.checksum    = calculate_checksum(&newData);

        /* 구조체 전체를 Critical Section 내에서 원자적으로 업데이트 */
        taskENTER_CRITICAL();

        sensorReading = newData;

        taskEXIT_CRITICAL();

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

void vSensorReadTask(void *pvParameters) {
    uint32_t errorCount   = 0;
    uint32_t successCount = 0;

    while(1) {
        SensorData localCopy;

        /* 구조체 전체를 Critical Section 내에서 원자적으로 복사 */
        taskENTER_CRITICAL();

        localCopy = sensorReading;

        taskEXIT_CRITICAL();

        uint8_t calculatedChecksum = calculate_checksum(&localCopy);

        if(calculatedChecksum != localCopy.checksum) {
            errorCount++;
            printf("[Reader] 체크섬 오류 감지 #%lu!\n", errorCount);
            printf("  저장 체크섬: %u, 계산 체크섬: %u\n",
                   localCopy.checksum, calculatedChecksum);
        } else {
            successCount++;
        }

        vTaskDelay(pdMS_TO_TICKS(150));
    }
}

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

        printf("\n=== 센서 데이터 상태 ===\n");

        taskENTER_CRITICAL();
        printf("시간: %lu, 온도: %d, 습도: %u\n",
               sensorReading.timestamp,
               sensorReading.temperature,
               sensorReading.humidity);
        taskEXIT_CRITICAL();
    }
}

int main(void) {
    printf("=== 구조체 일관성 보호 실습 ===\n\n");

    xTaskCreate(vSensorUpdateTask, "Update", 256, NULL, 2, NULL);
    xTaskCreate(vSensorReadTask,   "Read",   256, NULL, 2, NULL);
    xTaskCreate(vStatusTask,       "Status", 256, NULL, 1, NULL);

    vTaskStartScheduler();
    while(1);
}

실습 3: ISR과 Task 간 공유 버퍼 보호

ISR에서 데이터를 수신하고 Task에서 처리하는 구조에서 각 환경에 맞는 Critical Section을 적용합니다.

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

#define RX_BUFFER_SIZE  64

typedef struct {
    uint8_t          data[RX_BUFFER_SIZE];
    volatile uint32_t head;
    volatile uint32_t tail;
    volatile uint32_t count;
} RxBuffer;

RxBuffer rxBuffer = {0};

/* ISR 시뮬레이션: 실제 환경에서는 UART IRQ Handler에 해당 */
void simulate_isr_receive(uint8_t byte) {
    UBaseType_t uxSavedInterruptStatus;
    uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();

    if(rxBuffer.count < RX_BUFFER_SIZE) {
        rxBuffer.data[rxBuffer.head] = byte;
        rxBuffer.head = (rxBuffer.head + 1) % RX_BUFFER_SIZE;
        rxBuffer.count++;
    }

    taskEXIT_CRITICAL_FROM_ISR(uxSavedInterruptStatus);
}

BaseType_t buffer_read(uint8_t *outByte) {
    BaseType_t result = pdFALSE;

    taskENTER_CRITICAL();

    if(rxBuffer.count > 0) {
        *outByte = rxBuffer.data[rxBuffer.tail];
        rxBuffer.tail = (rxBuffer.tail + 1) % RX_BUFFER_SIZE;
        rxBuffer.count--;
        result = pdTRUE;
    }

    taskEXIT_CRITICAL();

    return result;
}

/* ISR 시뮬레이션을 위한 Task */
void vSimulatedIsrTask(void *pvParameters) {
    uint8_t byteToSend = 0;

    while(1) {
        simulate_isr_receive(byteToSend);
        byteToSend++;

        vTaskDelay(pdMS_TO_TICKS(20));
    }
}

void vProcessTask(void *pvParameters) {
    uint8_t receivedByte;
    uint32_t totalReceived = 0;

    while(1) {
        if(buffer_read(&receivedByte)) {
            totalReceived++;
            printf("[Process] 수신: 0x%02X, 총 수신 횟수: %lu\n",
                   receivedByte, totalReceived);
        }

        vTaskDelay(pdMS_TO_TICKS(50));
    }
}

void vBufferStatusTask(void *pvParameters) {
    while(1) {
        vTaskDelay(pdMS_TO_TICKS(1000));

        taskENTER_CRITICAL();
        uint32_t currentCount = rxBuffer.count;
        taskEXIT_CRITICAL();

        printf("[Status] 버퍼 사용량: %lu / %d\n",
               currentCount, RX_BUFFER_SIZE);
    }
}

int main(void) {
    printf("=== ISR-Task 공유 버퍼 보호 실습 ===\n\n");

    xTaskCreate(vSimulatedIsrTask, "ISR_Sim",   256, NULL, 3, NULL);
    xTaskCreate(vProcessTask,      "Process",   256, NULL, 2, NULL);
    xTaskCreate(vBufferStatusTask, "BufStatus", 256, NULL, 1, NULL);

    vTaskStartScheduler();
    while(1);
}

실습 4: 종합 문제 - 은행 계좌 보호

Day 12의 Race Condition 예제였던 은행 계좌 문제에 Critical Section을 적용하여 올바른 잔액 관리를 구현합니다.

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

typedef struct {
    volatile int32_t  balance;
    volatile uint32_t depositCount;
    volatile uint32_t withdrawCount;
    volatile uint32_t failedCount;
    char              owner[20];
} BankAccount;

BankAccount account = {1000, 0, 0, 0, "User"};

BaseType_t safe_withdraw(uint32_t amount) {
    BaseType_t result = pdFALSE;

    taskENTER_CRITICAL();

    if(account.balance >= (int32_t)amount) {
        account.balance -= (int32_t)amount;
        account.withdrawCount++;
        result = pdTRUE;
    } else {
        account.failedCount++;
    }

    taskEXIT_CRITICAL();

    return result;
}

BaseType_t safe_deposit(uint32_t amount) {
    taskENTER_CRITICAL();

    account.balance += (int32_t)amount;
    account.depositCount++;

    taskEXIT_CRITICAL();

    return pdTRUE;
}

void vWithdrawTask(void *pvParameters) {
    int taskId = (int)pvParameters;

    for(int i = 0; i < 10; i++) {
        if(safe_withdraw(150)) {
            printf("[Task %d] 출금 성공: 150원, 잔액: %ld\n",
                   taskId, account.balance);
        } else {
            printf("[Task %d] 출금 실패: 잔액 부족 (%ld원)\n",
                   taskId, account.balance);
        }

        vTaskDelay(pdMS_TO_TICKS(100));
    }

    vTaskDelete(NULL);
}

void vDepositTask(void *pvParameters) {
    for(int i = 0; i < 5; i++) {
        safe_deposit(300);
        printf("[Deposit] 입금 성공: 300원, 잔액: %ld\n", account.balance);

        vTaskDelay(pdMS_TO_TICKS(200));
    }

    vTaskDelete(NULL);
}

void vAccountReportTask(void *pvParameters) {
    vTaskDelay(pdMS_TO_TICKS(3000));

    printf("\n");
    printf("=======================================\n");
    printf("         계좌 최종 보고서              \n");
    printf("=======================================\n");

    taskENTER_CRITICAL();
    int32_t  finalBalance   = account.balance;
    uint32_t depositCount   = account.depositCount;
    uint32_t withdrawCount  = account.withdrawCount;
    uint32_t failedCount    = account.failedCount;
    taskEXIT_CRITICAL();

    int32_t expectedBalance = 1000
                            + (int32_t)(depositCount  * 300)
                            - (int32_t)(withdrawCount * 150);

    printf("초기 잔액:     1000원\n");
    printf("입금 횟수:     %lu회 (+%ld원)\n", depositCount,  (int32_t)(depositCount  * 300));
    printf("출금 횟수:     %lu회 (-%ld원)\n", withdrawCount, (int32_t)(withdrawCount * 150));
    printf("실패 횟수:     %lu회\n", failedCount);
    printf("---------------------------------------\n");
    printf("기대 잔액:     %ld원\n", expectedBalance);
    printf("실제 잔액:     %ld원\n", finalBalance);

    if(finalBalance == expectedBalance && finalBalance >= 0) {
        printf("상태:          정상 - Critical Section 보호 성공\n");
    } else {
        printf("상태:          오류 - 잔액 불일치 또는 음수 잔액\n");
    }

    printf("=======================================\n");

    vTaskDelete(NULL);
}

int main(void) {
    printf("=== 은행 계좌 Critical Section 보호 ===\n");
    printf("초기 잔액: %ld원\n\n", account.balance);

    xTaskCreate(vWithdrawTask,     "Withdraw1", 256, (void*)1, 2, NULL);
    xTaskCreate(vWithdrawTask,     "Withdraw2", 256, (void*)2, 2, NULL);
    xTaskCreate(vWithdrawTask,     "Withdraw3", 256, (void*)3, 2, NULL);
    xTaskCreate(vDepositTask,      "Deposit",   256, NULL,     2, NULL);
    xTaskCreate(vAccountReportTask,"Report",    256, NULL,     1, NULL);

    vTaskStartScheduler();
    while(1);
}

5. 디버깅: Critical Section 관련 문제

5.1 인터럽트 응답 지연 측정

Critical Section의 길이를 검증하기 위해 진입/퇴출 시점의 타임스탬프를 기록합니다.

typedef struct {
    TickType_t enterTime;
    TickType_t exitTime;
    TickType_t duration;
} CriticalSectionStats;

CriticalSectionStats csStats = {0};

void instrumented_critical_section(void) {
    csStats.enterTime = xTaskGetTickCount();

    taskENTER_CRITICAL();

    /* 보호가 필요한 연산 */
    sharedCounter++;

    taskEXIT_CRITICAL();

    csStats.exitTime = xTaskGetTickCount();
    csStats.duration = csStats.exitTime - csStats.enterTime;

    if(csStats.duration > pdMS_TO_TICKS(1)) {
        printf("[경고] Critical Section 길이 초과: %lu ticks\n",
               csStats.duration);
    }
}

5.2 중첩 불일치 감지

taskENTER_CRITICAL()taskEXIT_CRITICAL()의 호출 횟수가 맞지 않을 경우를 감지하기 위한 래퍼 함수를 활용합니다.

volatile int32_t csNestCount = 0;

void debug_enter_critical(void) {
    taskENTER_CRITICAL();

    csNestCount++;

    if(csNestCount > 3) {
        printf("[경고] Critical Section 중첩 깊이 이상: %ld\n", csNestCount);
    }
}

void debug_exit_critical(void) {
    csNestCount--;

    if(csNestCount < 0) {
        printf("[오류] Critical Section 불균형 퇴출 감지!\n");
        csNestCount = 0;
    }

    taskEXIT_CRITICAL();
}

학습 정리

오늘 배운 핵심 내용

  1. taskENTER_CRITICAL() / taskEXIT_CRITICAL()

    • 인터럽트를 비활성화하여 원자적 실행을 보장
    • 중첩 호출을 지원하며, 호출 횟수를 반드시 일치시켜야 함
    • configMAX_SYSCALL_INTERRUPT_PRIORITY 이하의 인터럽트만 차단
  2. ISR에서의 Critical Section

    • Task용 매크로 대신 taskENTER_CRITICAL_FROM_ISR() / taskEXIT_CRITICAL_FROM_ISR() 사용
    • 반환값을 저장하고 퇴출 시 전달하여 이전 상태로 복원
  3. 사용 시 주의사항

    • Critical Section 내부에서 블로킹 API, 긴 연산, printf 등은 금지
    • 공유 자원 접근 구간만 보호하여 최소 길이를 유지
    • ISR과 Task 간 공유 시 양쪽 모두 적절한 매크로 적용 필요
  4. 스케줄러 일시 중단과의 차이

    • Critical Section은 ISR까지 포함하여 보호
    • 스케줄러 일시 중단은 Task 간 경쟁만 방지하며, ISR은 계속 실행됨

핵심 개념 요약

개념설명
taskENTER_CRITICAL()인터럽트 비활성화, Task용 Critical Section 진입
taskEXIT_CRITICAL()인터럽트 재활성화, Task용 Critical Section 퇴출
taskENTER_CRITICAL_FROM_ISR()ISR 내에서의 Critical Section 진입, 이전 마스크 반환
taskEXIT_CRITICAL_FROM_ISR()ISR 내에서의 Critical Section 퇴출, 이전 마스크 복원
중첩 카운터중첩 Critical Section의 깊이를 추적하는 내부 카운터
Interrupt LatencyCritical Section으로 인한 인터럽트 응답 지연 시간

실습 과제

과제 1: 멀티 Task 링 버퍼 보호

Day 12 이전 강의에서 작성한 Race Condition이 발생하는 링 버퍼 예제에 Critical Section을 적용하여 안전한 버퍼 구현을 완성하세요.

요구사항:

  • 2개 Producer Task, 1개 Consumer Task
  • 버퍼 읽기/쓰기 함수에 Critical Section 적용
  • 데이터 손실 없이 정확한 count 유지
  • 100회 쓰기 후 count 검증

과제 2: 중첩 Critical Section 구조 설계

하나의 함수가 다른 Critical Section 보호 함수를 내부적으로 호출하는 중첩 구조를 설계하고, 올바르게 동작하는지 검증하세요.

요구사항:

  • 최소 2단계 중첩 구조
  • 각 단계에서의 sharedCounter 접근
  • 중첩 카운터 출력 및 검증
  • 중첩 불일치 발생 시 오류 출력

과제 3: ISR-Task 통신 시스템 구현

타이머 ISR에서 주기적으로 데이터를 생성하고, Task에서 처리하는 시스템을 구현하세요.

요구사항:

  • 타이머 ISR 시뮬레이션 Task (높은 우선순위)
  • 데이터 처리 Task
  • ISR용 / Task용 Critical Section 각각 적용
  • 데이터 손실 없는 전달 검증

디버깅 팁

문제 1: Critical Section 내에서 시스템이 멈춤

/* 원인: Critical Section 내부에서 블로킹 API 호출 */
taskENTER_CRITICAL();
vTaskDelay(pdMS_TO_TICKS(100));     /* 오류: 스케줄러 비활성화 상태에서 대기 불가 */
taskEXIT_CRITICAL();

/* 해결: 블로킹 호출을 Critical Section 외부로 이동 */
taskENTER_CRITICAL();
uint32_t value = sharedData;
taskEXIT_CRITICAL();

vTaskDelay(pdMS_TO_TICKS(100));     /* Critical Section 외부에서 대기 */

문제 2: ISR에서 Task용 Critical Section 매크로 사용

/* 오류: ISR에서 Task용 매크로 사용 */
void UART_IRQHandler(void) {
    taskENTER_CRITICAL();           /* ISR에서 사용 금지 */
    rxBuffer.count++;
    taskEXIT_CRITICAL();
}

/* 해결: ISR 전용 매크로 사용 */
void UART_IRQHandler(void) {
    UBaseType_t uxSaved = taskENTER_CRITICAL_FROM_ISR();
    rxBuffer.count++;
    taskEXIT_CRITICAL_FROM_ISR(uxSaved);
}

문제 3: 불균형 진입/퇴출 호출

/* 오류: 조건부 퇴출로 인한 불균형 */
void bad_conditional(void) {
    taskENTER_CRITICAL();

    if(condition) {
        return;                     /* 퇴출 없이 반환: 인터럽트 영구 비활성화 */
    }

    sharedCounter++;
    taskEXIT_CRITICAL();
}

/* 해결: 모든 경로에서 반드시 퇴출 */
void good_conditional(void) {
    taskENTER_CRITICAL();

    if(!condition) {
        sharedCounter++;
    }

    taskEXIT_CRITICAL();            /* 조건과 무관하게 항상 퇴출 */
}
profile
당신의 코딩 메이트

0개의 댓글