지노랩 /JinoLab
FreeRTOS의 스케줄링 심화 설명: 선점형 vs 협력형 본문
이 글에서는 FreeRTOS에서 태스크가 어떻게 CPU를 분배받고 돌아가게 되는지, 즉 **스케줄러(Scheduler)**가 무엇을 어떻게 하는지를 가능한 한 상세히 정리합니다. 특히 선점형(Preemptive)과 협력형(Co-operative) 스케줄링의 차이점을, 예시와 함께 단계별로 살펴보겠습니다.
목차
- 스케줄러의 역할과 호출
- FreeRTOSConfig.h 주요 설정
- 선점형(Preemptive) 스케줄링
- 3.1. 선점(Preemption) 개념
- 3.2. 라운드로빈(Round-Robin) 스케줄링 예시
- 3.3. 우선순위 기반 선점 스케줄링 예시
- 협력형(Co-operative) 스케줄링
- 스케줄러 동작 과정 요약
- 실제 STM32 예제와 점검 포인트
- 정리 및 권장 실습
1. 스케줄러의 역할과 호출
- 태스크(Task): FreeRTOS에서 실행 가능한 최소 단위 코드 + 독립된 스택(stack)을 가짐
- READY 리스트: “실행 준비가 끝나서 곧 CPU를 사용할 수 있는” 태스크들이 대기하는 큐
- 스케줄러(Scheduler):
- READY 리스트에 올라온 태스크 중에서
- 설정한 정책(선점형/협력형, 우선순위, 라운드로빈 등)에 따라
- 가장 적절한 1개를 선택해 CPU에 할당(Dispatch)
컨텍스트 스위치(Context Switch)
- CPU를 빼앗아 오는 쪽 태스크(Context)와 내보내는 쪽 태스크(Context)를 교체하는 과정
- 컨텍스트에는 일반 레지스터, 스택 포인터(PSP or MSP), 프로그램 카운터(PC) 등의 정보가 포함됨
- FreeRTOS에서는 PendSV 예외(PendSV_Handler)를 이용해 컨텍스트 스위치를 수행
스케줄러 호출 순서
- 태스크 생성(xTaskCreate 등) → 커널 내부 자료구조에 TCB(Task Control Block)와 별도 스택을 동적 할당
- 태스크가 생성되면 자동으로 READY 리스트에 추가
- 애플리케이션 main() 함수에서 vTaskStartScheduler() 호출
- vTaskStartScheduler() 내부에서:
- SysTick(혹은 대체한 하드웨어 타이머) 인터럽트를 활성화
- 최초로 “가장 우선순위 높은 READY 태스크”에 컨텍스트 스위치 → CPU 실행
- 이후 매 틱마다(Tick Interrupt) → 스케줄링 정책에 따라 READY 리스트 재정렬 → 컨텍스트 스위치
Tip: vTaskStartScheduler()는 “힙 부족” 등의 이유로 커널이 시작되지 못하면 리턴.
따라서 호출 이후 코드가 진행되면 “커널 시작 실패”로 간주하고 오류 처리 필요.
2. FreeRTOSConfig.h 주요 설정
FreeRTOSConfig.h에서 스케줄러 동작 방식을 결정하는 핵심 매크로들은 다음과 같습니다:
/* FreeRTOSConfig.h */
#define configUSE_PREEMPTION 1 // 1: 선점형 모드(디폴트), 0: 협력형 모드
#define configUSE_TIME_SLICING 1 // 1: 동일 우선순위 태스크 간 라운드로빈 허용
#define configTICK_RATE_HZ 1000 // 1kHz(1ms 틱) → 1ms마다 스케줄러 기회
#define configMAX_PRIORITIES 5 // 태스크 우선순위 레벨: 0 ~ (5-1)=4 (총 5단계)
- configUSE_PREEMPTION
- 1 이면 선점형(Preemptive) 스케줄링
- 0 이면 협력형(Co-operative) 스케줄링
- configUSE_TIME_SLICING
- 동일 우선순위 그룹에서 “틱마다 라운드로빈(Round-Robin)” 교대
- 1이면 같은 우선순위 태스크가 여러 개일 때, 매 틱마다 다음 태스크가 실행됨
- 0이면 동일 우선순위라도, 태스크 스스로 블록(또는 우선순위 변경)할 때까지 계속 실행
- configTICK_RATE_HZ
- “틱(Tick) 인터럽트 주기”를 결정
- 예: 1000Hz → 1ms마다 틱 인터럽트 발생 → 스케줄러가 실행 기회를 가짐
- configMAX_PRIORITIES
- 태스크 우선순위 범위를 어떻게 줄 것인지
- 예: 5 → 우선순위 레벨 0,1,2,3,4 사용 가능 (총 5단계)
참고:
- 선점형이든 협력형이든, 틱은 “시간 확인” 용도로 항상 카운트됨.
- 하지만 협력형이라면 틱이 들어와도 스케줄러가 실행되지 않는다(태스크가 자발적으로 CPU 반납 시에만 스케줄러 실행).
3. 선점형(Preemptive) 스케줄링
3.1. 선점(Preemption) 개념
- **“선점”**은 실행 중인 태스크를 강제로 중단(Preempt)하고, 다른 태스크를 즉시 실행시키는 방식
- 중단된 태스크는 READY 상태로 돌아가서, 다시 스케줄될 때 “중단 직전 상태”에서 재개됨
컨텍스트 스위치의 흐름
- 현재 실행 중인 태스크(예: T1)의 레지스터 상태 및 스택 포인터 저장
- 다음 실행할 태스크(예: T2)의 저장된 레지스터와 스택 포인터 복원
- T2가 실행을 계속 → T1은 READY 리스트 뒤로 이동
선점형 스케줄링은 2가지 세부 방식으로 나뉩니다.
- Round-Robin(라운드로빈) 방식
- 동일 우선순위 태스크가 여러 개일 때, “시간 할당(타임슬라이스)”마다 고르게 CPU를 분배
- configUSE_TIME_SLICING = 1 일 때만 작동
- 우선순위 기반(Prio-based) 방식
- “가장 높은 우선순위(Ready 상태) 태스크”를 최우선 실행
- 틱마다 또는 새로운 태스크가 READY되거나, 우선순위가 바뀔 때마다 재검토 후 실행
3.2. 라운드로빈(Round-Robin) 스케줄링 예시
가정
- 시스템에 우선순위 1인 태스크 T1, T2, T3, T4 총 4개가 존재
- 모두 동일 우선순위(1) → “Round-Robin” 교대 가능
- configTICK_RATE_HZ = 1000 → 1ms마다 틱 발생
- configUSE_TIME_SLICING = 1 → 라운드로빈 사용
READY 리스트 (우선순위 1): ┌───┬───┬───┬───┐
│ T1│ T2│ T3│ T4│
└───┴───┴───┴───┘
- t=0ms → “가장 앞(T1)” 스위칭 → T1 실행(0ms~1ms)
- t=1ms 틱 → “틱 발생” → T1의 타임슬라이스 종료 → 스케줄러 실행 → READY에서 다음 맨 앞이 T2 → T2 실행(1ms~2ms)
- t=2ms 틱 → T2 타임슬라이스 종료 → 스케줄러 → READY 앞→ T3(실행 2ms~3ms)
- t=3ms 틱 → T3 → T4(실행 3ms~4ms)
- t=4ms 틱 → T4 → READY 앞에 남은 T1 → T1 재실행(4ms~5ms)
- 계속 순환 …
핵심 요약
- 동일 우선순위 태스크 그룹 내에서 “매 틱마다 Ready 리스트 맨 앞 태스크” 실행
- 실행한 태스크는 “다시 Ready 리스트 맨 뒤”로 이동 → 순환 구조
- CPU 묶임(Starvation) 방지, 모두가 균등한 CPU 시간 할당
3.3. 우선순위 기반 선점 예시
가정
- Task2 우선순위 = 3 (가장 높음)
- Task1, Task3 우선순위 = 2
- Task4 우선순위 = 1 (가장 낮음)
- configUSE_TIME_SLICING = 1 이더라도, 우선순위가 다를 때는 “우선순위 우선”
READY 리스트: [T2 (prio=3)] [T1 (2)] [T3 (2)] [T4 (1)]
- t=0ms → “최고 우선순위 T2” 바로 실행 → T2(0ms~1ms)
- t=1ms 틱 → T2 아직 READY라면 다시 T2 계속 실행(1ms~2ms)
- t=2ms 틱 → T2 계속 실행(2ms~3ms)
- 만약 이 사이 “T2가 블록(예: vTaskDelay or 이벤트 대기) 상태”로 들어간다면,
- T2 가 블록 → READY에서 빠짐 → 다음 우선순위(2) 대표(T1)가 실행됨(3ms~4ms)
- t=3ms (예: T2 블록) → READY: [T1(2)] [T3(2)] [T4(1)] → “가장 앞 T1” 실행(3ms~4ms)
- t=4ms 틱 → T1 타임슬라이스 종료(여전히 READY라면) → 동순위 라운드로빈 → T3(4ms~5ms)
- t=5ms 틱 → T3 계속(5ms~6ms)
- 가정: 이 사이 T2 “언블록(블록 → READY 복귀)” 발생 → 우선순위 3 → 즉시 선점
- t=6ms → “T2가 READY 복귀” → 우선순위 3이 가장 높으므로 → Preempt → T2 실행(6ms~7ms)
핵심 요약
- “우선순위가 더 높은 READY 태스크”가 생기면 즉시(다음 틱이 아니어도) 스위칭 → 선점 발생
- 동일 우선순위끼리는 라운드로빈(틱마다 교대)
- 블록(Block)→READY 복귀가 “선점 기회”가 됨
4. 협력형(Co-operative) 스케줄링
- “선점(Preemption)”이 전혀 없다 → 스케줄링은 **태스크 스스로 CPU를 반납(양보)**할 때만 발생
- 즉, 태스크가 vTaskDelay(), taskYIELD(), xQueueReceive(..., portMAX_DELAY) 같은 블로킹 API를 호출해야만
● 해당 태스크는 “블록(Blocked)” 상태로 빠지며 → 스케줄러가 다음 우선순위 READY 태스크를 실행 - 틱 인터럽트가 발생해도 스케줄러가 실행되지 않음
→ “틱”은 시간 카운트용으로만 사용되며, CPU 스위칭은 태스크의 자발적 양도 시에만
READY: [T1 (prio=2)] [T2 (prio=1)] [T3 (prio=1)]
t = 0ms → 스케줄러 시작 → T1(2) 실행 (협력형이라도 첫 실행 시 스케줄링)
t = 1ms → 틱 발생 (하지만 협력형 모드 → 스케줄러 호출하지 않음) → T1 계속 실행
t = 2ms → T1 내 vTaskDelay(5ms) 호출 → “T1 블록” → READY: [T2] [T3]
t = 2ms → 스케줄러 → “가장 높은 우선순위 READY” T2(1) → 실행
t = 3ms → 틱 → T2 계속 실행
t = 4ms → T2 내부에서 taskYIELD() 호출 → T2 블록 또는 “양보” → READY: [T3] [T2]
t = 4ms → 스케줄러 → T3(1) → 실행
…
- 장점:
- 디버깅이 비교적 쉬움 (태스크가 의도적으로 CPU를 양보하므로 스케줄 흐름 예측 가능)
- 단점:
- 한 태스크가 의도치 않게(vTaskDelay 등 호출 없이) 무한 루프 돌면 전체 시스템 “먹통”
- 실수(예: 루프 안에 Delay 빼먹음)가 시스템 정지로 직결
5. 스케줄러 동작 과정 요약
- 태스크 생성 (xTaskCreate(...))
- 커널이 동적 메모리(heap)을 사용해
- “스택 공간”
- “TCB(Task Control Block)”
를 할당한 뒤,
- 태스크를 READY 리스트 맨 뒤에 추가
- 커널이 동적 메모리(heap)을 사용해
- vTaskStartScheduler() 호출
- SysTick(또는 대체 타이머) 설정
- 최초 우선순위 결정 후 소프트웨어 컨텍스트 스위치(Reset → 첫 선택 태스크 실행)
- 틱 인터럽트(Tick ISR) 발생 (configTICK_RATE_HZ 주기)
- 선점형: 매 틱마다 스케줄러 실행 → READY 리스트 재검토 → 우선순위/라운드로빈 규칙에 따라 컨텍스트 스위치
- 협력형: 틱은 단순 시간 계수 → 스케줄러 호출하지 않음
→ 태스크가 블록/양도 시에만 스케줄러 호출
- 컨텍스트 스위치(Preemptive일 때)
- 현재 실행중 태스크의 레지스터, PSP/MSP, 우선순위 등 정보 저장
- 새로운 태스크(우선순위 또는 라운드로빈 기준)가 Ready → 새로운 태스크의 저장된 컨텍스트 복원 → 실행 재개
- 태스크 상태 전이
- RUNNING → Blocked: vTaskDelay(), xQueueReceive(..., portMAX_DELAY) 등 블로킹 API 호출
- RUNNING → Ready: taskYIELD(), 선점(Preempted) 등 강제 CPU 반납 시
- Running/Ready → Suspended: vTaskSuspend() 호출
- Ready/Blocked/Running → Deleted: vTaskDelete() 호출
6. 실제 STM32 예제와 점검 포인트
아래는 STM32F4 + FreeRTOS 환경에서, “두 개의 태스크를 생성 후 선점형으로 교대 실행”하는 예입니다. (협력형↔선점형 전환까지 확인)
6.1. FreeRTOSConfig.h 설정 예시
/* FreeRTOSConfig.h */
#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H
/* 선점형 또는 협력형 설정 */
#define configUSE_PREEMPTION 1 // 1: 선점형, 0: 협력형
/* 동일 우선순위 내 라운드로빈 */
#define configUSE_TIME_SLICING 1
/* SysTick 인터럽트 주기: 1000Hz → 1ms 틱 */
#define configTICK_RATE_HZ 1000
/* 태스크 우선순위 레벨 개수(0~4) */
#define configMAX_PRIORITIES 5
/* … 그 외 설정 생략 … */
#endif /* FREERTOS_CONFIG_H */
6.2. main.c 주요 내용
#include "stm32f4xx_hal.h"
#include "FreeRTOS.h"
#include "task.h"
#include <stdio.h>
// Task 핸들러 프로토타입
static void vTaskA(void *pvParameters);
static void vTaskB(void *pvParameters);
int main(void)
{
HAL_Init();
SystemClock_Config(); // 클럭 설정
MX_GPIO_Init(); // LED용 GPIO 초기화
MX_USART2_UART_Init(); // UART2 (printf) 초기화
// 1) TaskA 생성 (stack: 200 워드, 우선순위 2)
BaseType_t xReturnA = xTaskCreate(
vTaskA, // Task 함수
"Task-A", // 디버깅용 이름
200, // 스택 깊이(워드 단위)
NULL, // 파라미터(사용 안함)
2, // 우선순위(0~4)
NULL // TaskHandle (NULL)
);
configASSERT(xReturnA == pdPASS);
// 2) TaskB 생성 (동일 우선순위)
BaseType_t xReturnB = xTaskCreate(
vTaskB,
"Task-B",
200,
NULL,
2,
NULL
);
configASSERT(xReturnB == pdPASS);
// 3) 스케줄러 시작 → 선점형 모드라면 우선순위 2인 두 태스크가 라운드로빈 교대
vTaskStartScheduler();
// 스케줄러 실패 시 무한 루프
for (;;);
}
/*---------------------------------------------------------------------------*/
/* TaskA: 500ms마다 LED 토글 + UART 메시지 */
static void vTaskA(void *pvParameters)
{
for (;;)
{
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
printf("★ Task-A: LED Toggle + Hello World\r\n");
vTaskDelay(pdMS_TO_TICKS(500)); // 500ms 블록 → 협력도 가능
}
}
/*---------------------------------------------------------------------------*/
/* TaskB: 500ms마다 UART 메시지 출력 */
static void vTaskB(void *pvParameters)
{
for (;;)
{
printf("☆ Task-B: Hello World\r\n");
vTaskDelay(pdMS_TO_TICKS(500));
}
}
코드 설명
- xTaskCreate(...) → TCB + Task 스택 동적 할당 후 READY 리스트에 추가
- vTaskStartScheduler() → SysTick(1ms 틱) 활성화 후 “우선순위 2 중 가장 앞인 Task-A” 실행
- 동일 우선순위(2)이기 때문에 매 틱마다 라운드로빈 교대
- vTaskDelay(500) → 태스크가 500ms 동안 블록 → 다음 태스크 실행 기회
6.3. 설정 변경해서 협력형 실행
- configUSE_PREEMPTION = 0 으로 바꾸기 → 협력형 모드
- 빌드 → 실행
- 결과: TaskA와 TaskB 모두 “자신이 vTaskDelay() 호출 시에만 CPU 반납”
- “선점형처럼 틱마다 교대되지 않음”
- 예를 들어 TaskA만 vTaskDelay(500) 있으면, TaskA → Delay(→READY) → TaskB 실행(→Delay) → 다시 TaskA… 순환
실험 포인트
- 만약 vTaskDelay()를 빠뜨리면 (예: TaskA에만 Delay, TaskB에는 없음)
→ TaskB가 CPU를 절대 내주지 않으므로 “TaskB 무한 루프” → 시스템 전반 멈춤
7. 정리 및 권장 실습
- 스케줄러 동작 정리
- READY 리스트에 올라온 태스크
- configUSE_PREEMPTION = 1 → 선점형:
- 우선순위 기반 선택 → “같은 우선순위면” 라운드로빈(틱마다)
- “더 높은 우선순위 READY → 즉시 선점”
- configUSE_PREEMPTION = 0 → 협력형:
- 태스크 자신이 vTaskDelay()나 taskYIELD() 호출 시에만 다음 준비 태스크로 전환
- configTICK_RATE_HZ → 틱 주기 (선점형은 틱마다 스케줄러 호출, 협력형은 틱을 무시)
- configMAX_PRIORITIES → 우선순위 레벨 개수
- 실습 권장
- 우선순위 차이 실험
- TaskA 우선순위 3, TaskB 우선순위 1 로 바꾸고 “TaskA가 항상 즉시 선점” 확인
- configUSE_TIME_SLICING = 0
- 동일 우선순위 라운드로빈 해제 → 태스크가 블록(Delay) 호출 전까지 계속 실행됨 관찰
- 협력형 오류 실험
- 협력형(configUSE_PREEMPTION=0)에서 TaskA에는 Delay, TaskB에는 Delay 없이 무한 루프 → TaskB 독점 멈춤 확인
- 블로킹 상태(State) 실험
- vTaskDelayUntil()을 써서 주기적인 태스크 실행 실험
- 우선순위 역전(Priority Inversion) 탐색
- 세마포어/뮤텍스로 TaskA(높은 우선순위)와 TaskB(낮은 우선순위)가 공유 리소스 경쟁 → 태스크C(중간 우선순위) 개입 시 상황 관찰
- FreeRTOS의 “우선순위 상속(Priority Inheritance)” 기능으로 해결되는지 확인
- 우선순위 차이 실험
결론
- 선점형 스케줄링은 실시간성(Real-Time)이 중요한 시스템에 적합 → “높은 우선순위 태스크”가 언제든 즉시 CPU 사용
- 협력형 스케줄링은 단순하고 예측이 쉬워 소규모 시스템이나 태스크 간 협력이 명확할 때 유리
- FreeRTOS는 두 방식을 모두 지원하므로, configUSE_PREEMPTION 설정만으로 쉽게 전환 가능
- 실습을 통해 틱, 태스크 상태 전이, 컨텍스트 스위치 과정을 눈으로 직접 확인해보는 것이 완전한 이해에 중요
FreeRTOS 스케줄러 동작 원리를 명확히 이해하면, 이후 세마포어·뮤텍스·이벤트 그룹 같은 동기화 기법, 타임드딜레이, 그리고 각종 실시간 응용 예제를 훨씬 수월하게 다룰 수 있습니다.
끝까지 읽어주셔서 감사합니다. 실습하시다가 궁금한 점은 언제든 질문해주세요!
'임베디드 시스템 > RTOS' 카테고리의 다른 글
| FreeRTOS 태스크 Example (0) | 2025.06.14 |
|---|---|
| FreeRTOS 태스크 시작과 SWO 출력 설정 (ITM_SendChar 적용) (1) | 2025.06.14 |
| FreeRTOS 태스크 우선순위 (0) | 2025.06.13 |
| FreeRTOS Task Priority 완전 정복 (0) | 2025.06.12 |
| FreeRTOS 태스크 생성 API(xTaskCreate) 상세 분석 (0) | 2025.06.12 |