RTOS #5

홍태준·2026년 1월 16일

RTOS

목록 보기
5/20
post-thumbnail

Week 1 Day 5: 스케줄러의 원리

학습 목표

  • FreeRTOS 스케줄링 알고리즘 상세 분석
  • Priority-based Preemptive 스케줄링 동작 원리
  • Round-robin 스케줄링 이해
  • Context Switch 타이밍 분석
  • Tick Interrupt와 스케줄링의 관계

1. FreeRTOS 스케줄링 알고리즘 개요

스케줄링이란?

스케줄링은 어느 Task를 언제 실행할지 결정하는 과정입니다.

베어메탈 방식:

1. 무한루프 내 순차적 실행 
2. 인터럽트 기반 실행(Foreground/Background) 
3. 상태 머신 기반 실행 
4. 타이머 폴링(Timer-based Polling)

RTOS 방식:

1. 우선순위 기반 스케쥴링(Priority-based): 선점형(Preemptive) / 협력형(Cooperative)
2. 시간 기반 스케쥴링: 라운드 로빈(Round Robin) / 시분할(Time Slicing)
3. 알고리즘 기반 스케쥴링: RMS(Rate Monotic Scheduling) / EDF(Eartliest Deadline First)

FreeRTOS 스케줄링 방식

특성설명
Priority-based우선순위가 높은 Task가 먼저 실행
Preemptive높은 우선순위 Task가 낮은 우선순위를 선점
Fixed PriorityTask의 우선순위는 생성 시 결정 (동적 변경 가능)
Time-slicing같은 우선순위 Task는 시간을 나눠 사용 (Round-robin)

위의 알고리즘 기반 스케쥴링은 예제에 추가해넣지 않았습니다. 해당 내용은 추후에 공부해서 추가할 계획입니다.

스케줄링 규칙

규칙 1: 항상 Ready 상태의 Task 중 가장 높은 우선순위를 실행
규칙 2: 같은 우선순위가 여러 개면 Round-robin 방식으로 교대 실행
규칙 3: 현재 Task보다 높은 우선순위가 Ready 되면 즉시 선점
규칙 4: Blocked 상태의 Task는 스케줄링 대상이 아님

2. Priority-based Preemptive Scheduling

우선순위 기반 스케줄링

기본 원리:

현재 실행 중인 Task의 우선순위 < 새로 Ready된 Task의 우선순위
→ 즉시 Context Switch 발생

예제 시나리오

C 버전:

void vTaskLow(void *pvParameters) {
    while(1) {
        printf("Low priority task running\n");
        
        // 낮은 우선순위 작업 수행
        for(volatile int i = 0; i < 1000000; i++);
    }
}

void vTaskHigh(void *pvParameters) {
    while(1) {
        printf("High priority task running\n");
        
        // 높은 우선순위 작업
        HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
        
        // 1초 대기 (Blocked 상태)
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

int main(void) {
    // 우선순위 1 (낮음)
    xTaskCreate(vTaskLow, "Low", 128, NULL, 1, NULL);
    
    // 우선순위 3 (높음)
    xTaskCreate(vTaskHigh, "High", 128, NULL, 3, NULL);
    
    vTaskStartScheduler();
    while(1);
}

실행 흐름:

시간 →
0ms     : High Task Ready, Low Task Ready
          → High 실행 (우선순위 3 > 1)
          
10ms    : High가 vTaskDelay() 호출
          → High Blocked
          → Low 실행 시작
          
1010ms  : High Task Ready (Delay 끝)
          → Low를 즉시 선점
          → High 실행
          
1020ms  : High가 다시 Blocked
          → Low 재개

C++ 버전:

class Task {
protected:
    TaskHandle_t taskHandle;
    const char* taskName;
    uint16_t stackSize;
    UBaseType_t priority;
    
    virtual void run() = 0;
    
    static void taskEntry(void* pvParameters) {
        Task* task = static_cast<Task*>(pvParameters);
        task->run();
    }
    
public:
    Task(const char* name, uint16_t stack, UBaseType_t prio)
        : taskName(name), stackSize(stack), priority(prio), taskHandle(nullptr) {}
    
    void create() {
        xTaskCreate(taskEntry, taskName, stackSize, this, priority, &taskHandle);
    }
    
    UBaseType_t getPriority() const { return priority; }
    
    virtual ~Task() {}
};

class LowPriorityTask : public Task {
public:
    LowPriorityTask() : Task("Low", 128, 1) {}
    
    void run() override {
        while(1) {
            printf("Low priority task running\n");
            
            // CPU 집중 작업
            for(volatile int i = 0; i < 1000000; i++);
        }
    }
};

class HighPriorityTask : public Task {
public:
    HighPriorityTask() : Task("High", 128, 3) {}
    
    void run() override {
        while(1) {
            printf("High priority task running\n");
            HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
            vTaskDelay(pdMS_TO_TICKS(1000)); //High 실행 완료 후 1000ms동안 block
        }
    }
};

int main(void) {
    LowPriorityTask lowTask;
    HighPriorityTask highTask;
    
    lowTask.create();
    highTask.create();
    
    vTaskStartScheduler();
    while(1);
}

선점(Preemption)이 발생하는 경우

// 케이스 1: Delay 종료
void vTask1(void *pvParameters) {  // 우선순위 1
    while(1) {
        // 계속 실행 중...
    }
}

void vTask2(void *pvParameters) {  // 우선순위 2
    while(1) {
        vTaskDelay(pdMS_TO_TICKS(1000));  // Blocked
        // ← Delay가 끝나면 Task1을 선점하고 실행됨
    }
}

// 케이스 2: ISR에서 높은 우선순위를 가진 Task 깨우기
QueueHandle_t xQueue;

void UART_IRQHandler(void) {
    char data = UART_ReceiveData();
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    
    // ISR에서 Queue에 데이터 전송
    xQueueSendFromISR(xQueue, &data, &xHigherPriorityTaskWoken);
    
    // 더 높은 우선순위 Task가 깨어났다면
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

void vUARTTask(void *pvParameters) {  // 우선순위 3 (높음)
    char receivedData;
    
    while(1) {
        // Queue 대기 (Blocked)
        xQueueReceive(xQueue, &receivedData, portMAX_DELAY);
        
        // ISR에서 데이터가 오면 즉시 깨어나서 실행
        // 낮은 우선순위 Task를 선점
        process_data(receivedData);
    }
}

// 케이스 3: 우선순위 변경
void vControlTask(void *pvParameters) {
    TaskHandle_t xWorkerHandle;
    
    while(1) {
        // 긴급 상황 발생
        if(emergency_detected()) {
            // Worker Task의 우선순위를 높임
            vTaskPrioritySet(xWorkerHandle, 5);
            // → 현재 Task(우선순위 3)를 선점하고 Worker 실행
        }
        
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

3. Round-robin 스케줄링 (Time Slicing)

Time Slicing이란?

같은 우선순위를 가진 여러 Task가 시간을 나눠서 사용하는 방식입니다.

설정

// FreeRTOSConfig.h
#define configUSE_TIME_SLICING    1    // 1 = 활성화, 0 = 비활성화

동작 원리

같은 우선순위 Task들이 매 Tick마다 교대로 실행

예: Priority 2인 Task A, B, C가 있을 때
Tick 0: Task A 실행
Tick 1: Task B 실행
Tick 2: Task C 실행
Tick 3: Task A 실행
...

Round-robin 예제

C 버전:

void vTaskA(void *pvParameters) {
    int count = 0;
    
    while(1) {
        printf("Task A: %d\n", count++);
        
        // Delay 없이 계속 실행
        // Time Slicing 덕분에 CPU 독점하지 않음
        for(volatile int i = 0; i < 100000; i++);
    }
}

void vTaskB(void *pvParameters) {
    int count = 0;
    
    while(1) {
        printf("Task B: %d\n", count++);
        for(volatile int i = 0; i < 100000; i++);
    }
}

void vTaskC(void *pvParameters) {
    int count = 0;
    
    while(1) {
        printf("Task C: %d\n", count++);
        for(volatile int i = 0; i < 100000; i++);
    }
}

int main(void) {
    // 모두 같은 우선순위 2
    xTaskCreate(vTaskA, "A", 128, NULL, 2, NULL);
    xTaskCreate(vTaskB, "B", 128, NULL, 2, NULL);
    xTaskCreate(vTaskC, "C", 128, NULL, 2, NULL);
    
    vTaskStartScheduler();
    while(1);
}

// 출력 예:
// Task A: 0
// Task B: 0
// Task C: 0
// Task A: 1
// Task B: 1
// Task C: 1
// ...

C++ 버전:

class RoundRobinTask : public Task {
private:
    char taskId;
    int count;
    
public:
    RoundRobinTask(char id, const char* name)
        : Task(name, 128, 2), taskId(id), count(0) {}
    
    void run() override {
        while(1) {
            printf("Task %c: %d\n", taskId, count++);
            
            // CPU 집중 작업
            for(volatile int i = 0; i < 100000; i++);
        }
    }
};

int main(void) {
    RoundRobinTask taskA('A', "TaskA");
    RoundRobinTask taskB('B', "TaskB");
    RoundRobinTask taskC('C', "TaskC");
    
    /* 태스크 A, B, C가 순차적으로 실행됨(인터럽트 핸들러 X) */
    taskA.create();
    taskB.create();
    taskC.create();
    
    vTaskStartScheduler();
    while(1);
}

Time Slicing vs Cooperative Scheduling

// Time Slicing 활성화 (configUSE_TIME_SLICING = 1)
void vTask1(void *pvParameters) {
    while(1) {
        work();  // Delay 없어도 매 Tick마다 다른 Task에게 양보
    }
}

// Time Slicing 비활성화 (configUSE_TIME_SLICING = 0)
void vTask2(void *pvParameters) {
    while(1) {
        work();
        taskYIELD();  // 명시적으로 양보해야 함
    }
}

4. Tick Interrupt와 스케줄링

Tick의 역할

Tick은 FreeRTOS의 시간 기준이자 스케줄링 트리거입니다.

틱 인터럽트는 하드웨어 타이머에서 설정한 시간 간격마다 주기적으로 발생하는 인터럽트입니다.

SysTick Interrupt 흐름

// SysTick Handler (매 1ms마다 호출, configTICK_RATE_HZ = 1000)
void SysTick_Handler(void) {
    // FreeRTOS에 Tick 증가 알림
    if(xTaskIncrementTick() != pdFALSE) {
        // Context Switch 필요 (더 높은 우선순위 Task가 Ready됨)
        // PendSV 인터럽트 요청
        portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
    }
}

// xTaskIncrementTick() 내부 동작
BaseType_t xTaskIncrementTick(void) {
    // 1. Tick Count 증가
    xTickCount++;
    
    // 2. Delay 중인 Task 확인
    // vTaskDelay()로 Blocked된 Task들의 타이머 감소
    
    // 3. Timeout된 Task를 Ready로 전환
    for(each blocked task) {
        if(task->timeout == xTickCount) {
            move_to_ready_list(task);
            
            if(task->priority > current_task->priority) {
                return pdTRUE;  // Context Switch 필요
            }
        }
    }
    
    // 4. Time Slicing
    if(configUSE_TIME_SLICING && same_priority_tasks_exist()) {
        return pdTRUE;  // 같은 우선순위 Task 교체
    }
    
    return pdFALSE;
}

Tick과 Task Delay

void vTaskExample(void *pvParameters) {
    while(1) {
        printf("Task running\n");
        
        // 500ms = 500 Ticks (configTICK_RATE_HZ = 1000일 때)
        vTaskDelay(pdMS_TO_TICKS(500));
        
        // 내부적으로:
        // 1. Task를 Blocked 리스트로 이동
        // 2. wakeup_time = current_tick + 500
        // 3. Context Switch 발생
        // 4. 500 Tick 후 Ready 리스트로 복귀
    }
}

Tick 주파수의 영향

// FreeRTOSConfig.h

// 케이스 1: 1000Hz (1ms)
#define configTICK_RATE_HZ    1000
// 장점: 정밀한 타이밍 (1ms 단위)
// 단점: 오버헤드 높음 (1ms마다 인터럽트)

// 케이스 2: 100Hz (10ms)
#define configTICK_RATE_HZ    100
// 장점: 오버헤드 낮음
// 단점: 정밀도 낮음 (10ms 단위)

// 케이스 3: 10000Hz (0.1ms)
#define configTICK_RATE_HZ    10000
// 장점: 매우 정밀
// 단점: 오버헤드 매우 높음 (권장하지 않음)

// 권장: 1000Hz (대부분의 애플리케이션에 적합)

틱 레이트(Tick Rate) 주파수가 너무 높으면 CPU가 본업보다 관리 업무에 부하가 더 걸리기 때문에 오버헤드가 발생합니다. 이 경우의 오버헤드는 태스크 딜레이가 너무 길어 생기는 문제와는 다소 차이가 있습니다(태스크 딜레이로 인한 오버헤드는 관리 및 검색의 부하, 틱 레이트로 인한 부하는 연산 및 문맥 교환의 부하입니다 )


5. Context Switch 상세 분석

Context Switch란?

현재 실행 중인 Task를 멈추고 다른 Task로 전환하는 과정

Context Switch 발생 시점

상황설명
Tick Interrupt매 Tick마다 스케줄러 실행
vTaskDelay()Task가 자발적으로 Blocked
Queue/Semaphore 대기데이터 대기 중 Blocked
높은 우선순위 Ready선점 발생
taskYIELD()명시적 양보

Context Switch 과정

1. 현재 Task의 Context 저장
   - CPU 레지스터 (R0-R12, LR, PC, PSR)
   - Stack Pointer (PSP)
   
2. 스케줄러 실행
   - Ready List에서 가장 높은 우선순위 Task 선택
   
3. 새 Task의 Context 복원
   - 저장된 레지스터 값 복원
   - Stack Pointer 전환
   
4. 새 Task 실행 재개

PendSV를 사용하는 이유

PendSV(Pendable Service Call)는 RTOS에서 컨텍스트 스위칭(문맥 교환)을 위해 사용하는 전용 대기 장치입니다. 주된 작동 방식은 "지연처리"로 예약 -> 양보 -> 실행 -> 교체의 순서로 문맥 교체(태스크 전환 과정)가 진행됩니다.

인터럽트(버튼/강제 진입한 이벤트)가 항상 태스크(데이터 연결)보다 우선순위가 높기 때문에 인터럽트가 발생하면 우선 인터럽트가 다 처리될 때까지 기다렸다가 다시 우선순위에 따라 스케쥴러가 작동하도록 하는게 PendSV입니다

// SysTick Handler (높은 우선순위)
void SysTick_Handler(void) {
    // Tick 처리만 하고
    xTaskIncrementTick();
    
    // Context Switch는 PendSV에 위임
    portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}

// PendSV Handler (가장 낮은 우선순위)
void PendSV_Handler(void) {
    // 실제 Context Switch 수행
    // 1. 현재 Task의 레지스터 저장
    // 2. 다음 Task 선택
    // 3. 새 Task의 레지스터 복원
}

// 왜 PendSV를 사용하는가?
// 1. 다른 인터럽트 처리 완료 후 Context Switch
// 2. Nested Interrupt 문제 방지
// 3. 더 안전하고 효율적인 전환

Context Switch 시간

// STM32F4 (84MHz) 기준 Context Switch 시간

// 측정 방법
void vTask1(void *pvParameters) {
    while(1) {
        GPIO_PIN_HIGH();
        vTaskDelay(1);
        GPIO_PIN_LOW();
    }
}

// 오실로스코프로 측정 결과:
// Context Switch 시간: 약 2-5 us (마이크로초)
// 
// 시간 구성:
// - 레지스터 저장/복원: 1-2 us
// - 스케줄러 실행: 1-2 us
// - PendSV 오버헤드: 0.5-1 us

6. Idle Task

Idle Task란?

모든 Task가 Blocked 상태일 때 실행되는 최저 우선순위 Task입니다.

Idle Task의 특징

// Idle Task는 FreeRTOS가 자동으로 생성
// - 우선순위: 0 (가장 낮음)
// - 절대 Blocked 되지 않음
// - CPU가 항상 할 일이 있도록 보장

void prvIdleTask(void *pvParameters) {
    while(1) {
        // 1. 삭제된 Task의 메모리 정리
        vTaskCleanUpResources();
        
        // 2. Idle Hook 호출 (사용자 정의 가능)
        #if configUSE_IDLE_HOOK
            vApplicationIdleHook();
        #endif
        
        // 3. Tickless Idle (저전력 모드)
        #if configUSE_TICKLESS_IDLE
            portSUPPRESS_TICKS_AND_SLEEP();
        #endif
    }
}

Idle Hook 활용

C 버전:

// FreeRTOSConfig.h
#define configUSE_IDLE_HOOK    1

// 사용자 정의 Idle Hook 함수
void vApplicationIdleHook(void) {
    // CPU가 한가할 때 실행할 작업
    
    // 예 1: LED 깜빡임 (시스템 동작 표시)
    static uint32_t idleCounter = 0;
    if(++idleCounter > 1000000) {
        HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
        idleCounter = 0;
    }
    
    // 예 2: 저전력 모드 진입
    // __WFI();  // Wait For Interrupt
    
    // 예 3: Watchdog 리셋
    // HAL_IWDG_Refresh(&hiwdg);
    
    // 주의: Idle Hook은 절대 Blocked 되면 안 됨!
    // vTaskDelay(), Queue 대기 등 금지!
}

C++ 버전:

class IdleTaskMonitor {
private:
    static uint32_t idleCounter;
    static GPIO& statusLED;
    
public:
    static void init(GPIO& led) {
        statusLED = led;
    }
    
    static void idleHook() {
        // CPU 유휴 시간 모니터링
        if(++idleCounter > 1000000) {
            statusLED.toggle();
            idleCounter = 0;
        }
    }
};

// C 인터페이스 (FreeRTOS가 호출)
extern "C" void vApplicationIdleHook(void) {
    IdleTaskMonitor::idleHook();
}

7. 스케줄링 최적화 기법

우선순위 할당 전략

// 권장 우선순위 할당

// 최고 우선순위 (4): 긴급하고 짧은 작업
// - 인터럽트에서 깨워지는 Task
// - 실시간 제어 Task
#define PRIORITY_CRITICAL    4

// 높은 우선순위 (3): 중요한 주기적 작업
// - 통신 프로토콜 처리
// - 센서 데이터 수집
#define PRIORITY_HIGH        3

// 보통 우선순위 (2): 일반 애플리케이션
// - 데이터 처리
// - UI 업데이트
#define PRIORITY_NORMAL      2

// 낮은 우선순위 (1): 백그라운드 작업
// - 로깅
// - 통계 계산
#define PRIORITY_LOW         1

// 최저 우선순위 (0): Idle Task만 사용

Rate Monotonic Scheduling (RMS)

// RMS 원칙: 주기가 짧은 Task에 높은 우선순위 할당

// 10ms 주기 Task → 우선순위 4
void vFastTask(void *pvParameters) {
    while(1) {
        process_fast();
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

// 100ms 주기 Task → 우선순위 3
void vMediumTask(void *pvParameters) {
    while(1) {
        process_medium();
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

// 1000ms 주기 Task → 우선순위 2
void vSlowTask(void *pvParameters) {
    while(1) {
        process_slow();
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

int main(void) {
    xTaskCreate(vFastTask, "Fast", 128, NULL, 4, NULL);
    xTaskCreate(vMediumTask, "Med", 128, NULL, 3, NULL);
    xTaskCreate(vSlowTask, "Slow", 128, NULL, 2, NULL);
    
    vTaskStartScheduler();
    while(1);
}

CPU 사용률 계산

// FreeRTOSConfig.h
#define configGENERATE_RUN_TIME_STATS    1
#define configUSE_STATS_FORMATTING_FUNCTIONS    1

void vStatsTask(void *pvParameters) {
    char statsBuffer[512];
    
    while(1) {
        vTaskDelay(pdMS_TO_TICKS(5000));
        
        printf("\n=== Task Runtime Stats ===\n");
        vTaskGetRunTimeStats(statsBuffer);
        printf("%s\n", statsBuffer);
    }
}

// 출력 예:
// Task          Abs Time      % Time
// LED1          1250          5%
// LED2          2500          10%
// Sensor        12500         50%
// IDLE          8750          35%
//
// CPU 사용률 = 100% - IDLE% = 65%

8. 실전 스케줄링 시나리오

시나리오 1: 센서 모니터링 시스템

요구사항:

  • 온도 센서: 100ms마다 읽기 (중요)
  • 압력 센서: 500ms마다 읽기 (보통)
  • 디스플레이: 1000ms마다 업데이트 (낮음)
  • UART 통신: 데이터 수신 시 즉시 처리 (긴급)

C 버전:

// UART Task (최고 우선순위)
void vUARTTask(void *pvParameters) {
    char rxData;
    
    while(1) {
        // Queue에서 데이터 대기 (Blocked)
        if(xQueueReceive(uartQueue, &rxData, portMAX_DELAY) == pdTRUE) {
            // 데이터 수신 즉시 처리
            process_uart_data(rxData);
        }
    }
}

// 온도 센서 Task (높은 우선순위)
void vTempSensorTask(void *pvParameters) {
    while(1) {
        float temp = read_temperature();
        
        if(temp > THRESHOLD) {
            // 긴급 처리
            trigger_alarm();
        }
        
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

// 압력 센서 Task (보통 우선순위)
void vPressureSensorTask(void *pvParameters) {
    while(1) {
        float pressure = read_pressure();
        store_pressure_data(pressure);
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

// 디스플레이 Task (낮은 우선순위)
void vDisplayTask(void *pvParameters) {
    while(1) {
        update_display();
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

int main(void) {
    uartQueue = xQueueCreate(10, sizeof(char));
    
    xTaskCreate(vUARTTask, "UART", 256, NULL, 4, NULL);
    xTaskCreate(vTempSensorTask, "Temp", 128, NULL, 3, NULL);
    xTaskCreate(vPressureSensorTask, "Pressure", 128, NULL, 2, NULL);
    xTaskCreate(vDisplayTask, "Display", 256, NULL, 1, NULL);
    
    vTaskStartScheduler();
    while(1);
}

C++ 버전:

class UARTTask : public Task {
private:
    QueueHandle_t& queue;
    
public:
    UARTTask(QueueHandle_t& q) : Task("UART", 256, 4), queue(q) {}
    
    void run() override {
        char rxData;
        
        while(1) {
            if(xQueueReceive(queue, &rxData, portMAX_DELAY) == pdTRUE) {
                processData(rxData);
            }
        }
    }
    
private:
    void processData(char data) {
        // 데이터 처리
    }
};

class SensorTask : public Task {
protected:
    uint32_t samplePeriod;
    
public:
    SensorTask(const char* name, uint32_t period, UBaseType_t prio)
        : Task(name, 128, prio), samplePeriod(period) {}
};

class TemperatureSensorTask : public SensorTask {
public:
    TemperatureSensorTask() : SensorTask("Temp", 100, 3) {}
    
    void run() override {
        while(1) {
            float temp = readTemperature();
            
            if(temp > THRESHOLD) {
                triggerAlarm();
            }
            
            vTaskDelay(pdMS_TO_TICKS(samplePeriod));
        }
    }
    
private:
    float readTemperature() {
        // ADC 읽기
        return 25.0f;
    }
    
    void triggerAlarm() {
        // 알람 발생
    }
};

class PressureSensorTask : public SensorTask {
public:
    PressureSensorTask() : SensorTask("Pressure", 500, 2) {}
    
    void run() override {
        while(1) {
            float pressure = readPressure();
            storeData(pressure);
            vTaskDelay(pdMS_TO_TICKS(samplePeriod));
        }
    }
    
private:
    float readPressure() { return 101.3f; }
    void storeData(float data) { }
};

class DisplayTask : public Task {
public:
    DisplayTask() : Task("Display", 256, 1) {}
    
    void run() override {
        while(1) {
            updateDisplay();
            vTaskDelay(pdMS_TO_TICKS(1000));
        }
    }
    
private:
    void updateDisplay() {
        // LCD 업데이트
    }
};

int main(void) {
    QueueHandle_t uartQueue = xQueueCreate(10, sizeof(char));
    
    UARTTask uartTask(uartQueue);
    TemperatureSensorTask tempTask;
    PressureSensorTask pressureTask;
    DisplayTask displayTask;
    
    uartTask.create();
    tempTask.create();
    pressureTask.create();
    displayTask.create();
    
    vTaskStartScheduler();
    while(1);
}

타이밍 다이어그램

시간 (ms) →
0   100  200  300  400  500  600  700  800  900  1000
|----|----|----|----|----|----|----|----|----|----|
UART: ████─────██──────────────████─────────────  (우선순위 4)
Temp: ──██──██──██──██──██──██──██──██──██──██──  (우선순위 3)
Pres: ────────████────────────████────────────██  (우선순위 2)
Disp: ──────────────────────────────────────████  (우선순위 1)
IDLE: ──────────────██████████────────████████──  (우선순위 0)

█ = 실행 중
─ = Blocked 또는 대기

C vs C++ 비교 정리

스케줄링 관련 구현

측면C 방식C++ 방식
우선순위 관리#define으로 상수 정의enum class로 타입 안전하게 정의
Task 그룹화함수 prefix로 구분클래스 계층으로 그룹화
우선순위 변경vTaskPrioritySet() 직접 호출멤버 함수로 캡슐화
통계 수집전역 변수 사용클래스 멤버로 관리

C 방식 우선순위 정의:

#define PRIORITY_UART        4
#define PRIORITY_SENSOR      3
#define PRIORITY_DISPLAY     1

C++ 방식 우선순위 정의:

enum class Priority : UBaseType_t {
    Low = 1,
    Normal = 2,
    High = 3,
    Critical = 4
};

class Task {
protected:
    Priority priority;
    
public:
    Task(const char* name, Priority prio)
        : priority(prio) {}
    
    void setPriority(Priority newPrio) {
        priority = newPrio;
        vTaskPrioritySet(taskHandle, static_cast<UBaseType_t>(newPrio));
    }
};

실습 과제

과제 1: 우선순위 역전 재현

목표: Priority Inversion 문제를 직접 재현하고 관찰

요구사항:

  • Low Task: 공유 자원 사용, 우선순위 1
  • High Task: 같은 공유 자원 필요, 우선순위 3
  • Medium Task: 무관한 작업, 우선순위 2
  • High Task가 Low Task를 기다리는 동안 Medium Task가 실행되는 것 확인

힌트:

volatile int sharedResource = 0;

void vLowTask(void *pvParameters) {
    while(1) {
        taskENTER_CRITICAL();
        // 공유 자원 사용 (오래 걸리는 작업)
        for(int i = 0; i < 1000000; i++) {
            sharedResource++;
        }
        taskEXIT_CRITICAL();
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

과제 2: CPU 사용률 모니터링

목표: 각 Task의 CPU 사용률을 측정하고 출력

요구사항:

  • 3개의 작업 Task (우선순위 다름)
  • 통계 Task (5초마다 CPU 사용률 출력)
  • vTaskGetRunTimeStats() 사용

과제 3: 동적 우선순위 조정

목표: 상황에 따라 Task 우선순위를 동적으로 변경

요구사항:

  • 센서 Task: 보통 우선순위 2
  • 긴급 버튼을 누르면 센서 Task 우선순위를 4로 상승
  • 10초 후 자동으로 우선순위 2로 복귀

요약

오늘 배운 핵심 내용

  1. Priority-based Preemptive: 높은 우선순위가 낮은 우선순위를 선점
  2. Round-robin: 같은 우선순위는 시간을 나눠 사용
  3. Tick Interrupt: 스케줄링의 트리거, 시간 관리
  4. Context Switch: Task 전환 메커니즘, 2-5us 소요
  5. Idle Task: 최저 우선순위, 시스템 유지 관리

핵심 용어 정리

용어설명
Preemptive높은 우선순위 Task가 낮은 우선순위를 선점
Time Slicing같은 우선순위 Task의 시간 분할 실행
Tick시스템 시간 단위, 스케줄링 기준점
Context SwitchTask 전환, 레지스터 저장/복원
Idle Task우선순위 0, 항상 Ready 상태 유지
RMSRate Monotonic Scheduling, 주기 기반 우선순위

복습 문제

  1. Priority-based Preemptive Scheduling의 핵심 원칙 3가지는?
  2. Time Slicing이 비활성화되면 어떤 일이 발생하는가?
  3. Tick Interrupt에서 PendSV를 사용하는 이유는?
  4. Context Switch가 발생하는 5가지 경우는?
  5. Idle Task의 역할은 무엇인가?
  6. configTICK_RATE_HZ를 높이면 장단점은?
  7. Rate Monotonic Scheduling 원칙은?
  8. 우선순위 할당 시 고려해야 할 사항은?

profile
당신의 코딩 메이트

0개의 댓글