FreeRTOS 시리즈
FreeRTOS Documentation - FreeRTOS™
이 문서에서 제공하는 PDF를 읽고 정리한 글입니다.
생성 일 : 2026-06-25 최종 수정일 : 2026-07-01
태스크 함수
커널이 실행시키는 태스크 함수는 다음 시그니처를 따라야 한다.
void TaskFunction( void *pvParameters );
일반적으로는 태스크 내에서 무한 반복을 하며 종료되지 않는다.
void TaskFunction( void *pvParameters ) {
/* ... */
for (;;) {
/* ... */
}
vTaskDelete(NULL); // 종료되기 전에 커널이 태스크 제거를 위한 뒷정리 작업을 할 수 있도록 만들어야 한다.
}
태스크 상태의 큰 분류
싱글 코어 시스템의 경우 한 번에 하나의 태스크만 실행한다. 따라서 모든 태스크는 두 가지 상태가 된다. 실행되지 않는 Not Running과 실행되고 있는 Running 상태만 있다.
Not Running 상태에서 Running 상태가 되는건 swapped in, switched in이라고 부르고, 반대의 상황은 swapped out, switched out이라고 부른다.
태스크 생성
태스크 생성을 하는 함수는 6가지나 된다.
xTaskCreate()xTaskCreateStatic()xTaskCreateRestricted()xTaskCreateRestrictedStatic()xTaskCreateAffinitySet(),xTaskCreateStaticAffinitySet()
태스크 생성은 두 가지 메모리 블럭을 할당한다. 하나는 TCB(Task Control Block), 다른 하나는 태스크 스택이다.
생성 함수 이름에 Static이 포함된다면 미리 생성된 블럭을 인자로 넘기고, 그렇지 않으면 런타임에 동적 메모리 할당을 사용해 생성한다.
함수 이름에 Restricted가 포함된다면 해당 함수를 사용하여 시스템 메모리 접근이 제한된 태스크를 생성할 수 있다. 그렇지 않으면 시스템 메모리 전체에 접근 가능한 상태가 된다.
SMP(Symmetry Multi Processing) 환경에서 FreeRTOS를 쓰는 경우 태스크에 코어 선호도(Affinity)를 부여할 수 있다. 함수 이름에 Affinity가 포함된다면 해당 함수를 사용하여 선호도를 부여한 태스크를 생성할 수 있다.
xTaskCreate()
대부분의 프로젝트는 xTaskCreate() 함수를 사용한다.
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode,
const char * const pcName,
configSTACK_DEPTH_TYPE usStackDepth,
void * pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * pxCreatedTask );
-
pvTaskCode태스크로 실행할 함수의 포인터다.
-
pcName태스크 이름이다. 순수 디버깅 용도로만 사용한다.
-
usStackDepth태스크 스택의 사이즈이다. 단, 사이즈가 byte 단위가 아닌 word 단위다. 128바이트를 할당하려면 32를 넘겨야 한다.
-
pvParameters태스크에 전달할 파라미터다. void 포인터를 사용해 아무 타입의 데이터를 넘길 수 있다.
-
uxPriority태스크의 우선순위다. 기본값은
0인데, 가장 낮은 우선순위이다.configMAX_PRIORITIES - 1)보다 더 큰 값으로 설정되어 있으면 최대값으로 설정된다. -
pxCreatedTask태스크 핸들의 주소값을 담는 포인터이다.
xCreateTask()함수가 성공하면 pdPASS를, 그렇지 않으면 pdFAIL을 반환한다.
태스크 우선순위
태스크는 높은 값일수록 높은 우선순위를 가진다. 가장 높은 우선순위는 configMAX_PRIORITIES 매크로에 의해 정해진다. 우선순위 값의 범위는 [0,configMAX_PRIORITIES)이다. FreeRTOS 스케줄러는 항상 높은 우선순위의 태스크를 선택해 실행한다. 같은 우선순위의 태스크들이 있다면, 돌아가면서 실행된다.
스케줄러
FreeRTOS는 두 가지 방식의 스케줄러가 존재한다.
-
일반 스케줄러
C로 작성되어, 공통 소스코드에 포함되어 있다.
configMAX_PRIORITIES보다 더 큰 우선순위를 부여할 수도 있지만 값을 크게 줄 수록 저장 공간을 더 차지하고, 스케줄링 시간이 길어질 수 있다. -
아키텍처 최적화 스케줄러
아키텍처에 최적화된 어셈블리 코드로 쓰여진 스케줄러다. 일반 스케줄러보다 더 좋은 성능을 내며 최악 실행 시간이 모든 우선순위 값에 대해 동일하다.
아키텍처 최적화 스케줄러를 사용하려면 configUSE_PORT_optimized_TASK_SELECTION를 1로 설정하여야 한다. 0이면 일반 스케줄러를 사용한다. 그러나 모든 Port가 각자의 아키텍처 최적화 스케줄러를 가지진 않기 때문에, 그러한 경우라면 이 매크로를 0으로 설정하거나 정의하지 않아야 한다.
시간 측정과 틱 인터럽트
두 개의 태스크가 같은 우선순위와 항상 스케줄러에게 선택되어 실행될 수 있다면, 각 태스크는 time-slice가 시작될 때 Running 상태가 되고, time-slice가 끝날 때 Running 상태에서 벗어난다. 스케줄러는 각 태스크가 실핼되는 time-slice가 끝날 때 곧바로 다음에 실행할 태스크를 선택한다. 이는 time-slice가 끝남을 알리는 주기적인 인터럽트에 의해 발생하고 이를 틱 인터럽트(tick interrupt)라고 한다. 틱 인터럽트의 주기는 configTICK_RATE_HZ 매크로에 의해 정해진다. 예를 들어 1000이면 1ms마다 틱 인터럽트가 발생하는 것이다. 다시 말해 time-slice 간격은 모두 동일하다.
Task A *----* *----*
| | | |
Task B *----* *----* *-- ...
| | | |
Kernel =======*====*====*====*=======
^ ^ ^ ^
tick interrupt 발생
FreeRTOS는 시간을 틱으로 바꿔 표현해주는 pdMS_TO_TICKS() 매크로가 있다. 이 매크로는 ms 단위의 시간을 틱으로 변환하지만 틱은 configTICK_RATE_HZ 값에 의해 발생 횟수가 달라지는 점에 유의해야한다. 예를 들어 틱 주기가 100HZ이면 pdMS_TO_TICKS(250) 매크로의 실행 결과는 250 / ( 1000 / 100 ) = 25다.
시스템에 따라서 틱 주기가 다르기 때문에 ms 단위의 시간으로 다루려고 하면 안된다. 시간을 우선 틱으로 바꾼 다음에 접근해야한다.
태스크 상태
Blocked 상태
태스크 A의 우선순위가 태스크 B의 우선순위보다 높다고 가정하고, 두 태스크 모두 중간에 멈추는 작업이 없이 계속 실행된다고 하자. 이 시스템을 실행하면 아래와 같이 스케줄링된다.
Task A *----*----*----*----*---- ...
| | | |
Task B | | | |
| | | |
Kernel =======*====*====*====*=======
^ ^ ^ ^
tick interrupt 발생
FreeRTOS는 기본적으로 항상 높은 우선순위를 가지는 태스크를 선택하여 실행하므로 태스크 B는 실행 기회를 전혀 잡지 못한다. 따라서 태스크 A와 같이 높은 우선순위를 갖는 태스크들이 작업을 잠시 멈추어야 그 시간동안 다른 태스크들이 실행될 수 있다.
이를 해결하기 위해 이벤트 기반으로 동작하도록 태스크를 구성해야한다. 이벤트 기반 태스크란 이벤트가 발생해야 실행 상태가 되고, 이벤트 발생 전에는 실행되지 않도록 설계된 태스크를 말한다. 높은 우선순위를 갖는 태스크가 이벤트 발생을 기다리며 멈춰있는 동안 스케줄러는 낮은 우선순위를 갖는 태스크를 선택할 수밖에 없다.
이 때 이벤트를 기다리며 멈춰있는 상태를 Blocked 상태라고 한다. 이 상태에 돌입한다면 두 가지 이벤트를 기다린다.
Not Running 상태는 단순히 실행되지 않는 상태를 의미한다. 실행되지 않는 경우는 현재 자신보다 높은 우선순위의 태스크가 실행중이거나 이벤트 발생을 기다리며 멈춰있는 상태가 있다.
- 일시적 이벤트
일정 시간만큼 대기하였거나, 혹은 정해놓은 시간이 되어서 이벤트가 발생하는 경우다.
- 동기화 이벤트
다른 태스크 또는 인터럽트에 의해 발생한다. 큐에 데이터가 들어오길 기다린다면 Blocked 상태가 된다. 태스크가 다시 실행되려면 다른 태스크나 ISR이 큐에 데이터를 넣어주어야 한다. 세마포어나 큐, 이벤트 그룹 등이 이벤트를 발생시킬 수 있다.
이 두 가지 이벤트를 조합해서 기다릴 수도 있다. 10ms동안 큐에 데이터가 들어오길 기다린다면 그 시간 안에 데이터가 들어와서 이벤트가 발생되거나, 10ms가 지나서 이벤트가 발생할 수 있다.
vTaskDelay(), vTaskDelayUntil()
태스크가 일시적으로 Blocked 상태가 되도록 만드는 함수다.
void vTaskDelay( TickType_t xTicksToDelay );
TickType_t를 인자로 받기 때문에, 시간을 틱으로 바꾼 값을 대입해야한다. 앞서 보았던 pdMS_TO_TICKS() 매크로를 이용해야한다. 이 함수를 호출 시 인자로 주어진 틱 카운트만큼 지나야 Blocked 상태에서 Ready 상태로 전이된다.
그렇지만 정확한 시간동안 주기적으로 Blocked 상태와 Ready 상태를 왔다갔다 해야한다면 이 함수는 불충분하다. 호출 시점을 기준으로 주어진 시간만큼 기다리기 때문에, 실행중에 걸린 시간을 반영하지 못한다. 이럴 때에는 vTaskDelayUntil() 함수를 사용해야한다.
void vTaskDelayUntil(
TickType_t * pxPreviousWakeTime,
TickType_t xTimeIncrement );
pxPreviousWakeTime은, 이전에 이 함수를 호출했던 시간을 의미한다. xTimeIncrement는 주기값을 의미한다.
void vTaskFunction( void * pvParameters )
{
TickType_t xLastWakeTime;
xLastWakeTime = xTaskGetTickCount();
for( ;; ) {
/* 작업 코드 */
vTaskDelayUntil( &xLastWakeTime, pdMS_TO_TICKS( 250 ) );
}
}
태스크 코드가 위와 같을 때, vTaskDelayUntil() 함수는 xLastWakeTime으로부터 250ms가 지나야 상태 전이가 일어난다. 작업 코드를 처리하는데 50ms가 걸렸다면 200ms만큼 기다린 후 깨어날 것이고, 100ms가 걸렸다면 150ms만큼 기다린 후 꺠어나게 된다.
Suspended 상태
Not Running 상태에 속하는 상태다. Blocked 상태는 스케줄러에 의해 선택되어 실행될 수 있지만, Suspended 상태는 스케줄러가 선택할 수 없는 상태다. vTaskResume(), vTaskResume(), xTaskResumeFromISR()의 함수를 통해 태스크를 Suspended 상태로 만들거나, 되돌릴 수 있지만 일반적으로 사용하지는 않는다고 한다.
Ready 상태
태스크가 Not Running, Blocked, Suspended 상태가 아니라면 Ready 상태다. 실행될 수 있지만, 당장은 실행되지 않은 상태다.
등장한 모든 상태를 조합하면 아래와 같은 상태 흐름도가 된다.

유휴 태스크
FreeRTOS는 항상 실행 가능한 태스크들 중에 1개를 선택해 실행하는 구조를 가지고 있다. 따라서 태스크를 아무것도 만들지 않았을 때에도 항상 실행 가능한 태스크는 1개 이상으로 존재해야하므로, FreeRTOS는 스케줄러를 실행하는 vTaskStartScheduler()함수가 호출되면 항상 기본 태스크(이하 유휴 태스크)를 한 개 만든다. 얘는 그냥 무한루프만 도는 태스크지만 우선순위가 0이어서 다른 태스크보다 먼저 실행되지 않는다.
만약 vTaskDelete()를 사용하여 태스크가 자기 자신을 삭제하면, 삭제된 태스크의 커널 리소스는 유휴 태스크가 정리한다. 따라서 유휴 태스크가 실행될 시간을 충분히 가질 수 있도록 해야 한다.
이 태스크가 실행되면 저전력 모드에 진입하게 된다.
우선순위 0 태스크
사용자가 임의로 우선순위가 0인 태스크를 만들 수도 있다. 그런 경우 라운드 로빈 방식으로 태스크가 선택되기 때문에 유휴 태스크와 함께 스케줄링된다. FreeRTOS는 유휴 태스크가 실행되는 시간을 조절할 수 있는 기능을 제공하는데, configIDLE_SHOULD_YIELD 매크로를 사용하여 유휴 태스크의 실행 시간을 조절할 수 있다.
유휴 태스크 hook
FreeRTOS는 유휴 태스크가 실행될 때 어떠한 작업을 할 수 있도록 유휴 태스크 hook을 제공한다. 이 hook 함수(또는 콜백)는 유휴 태스크의 매 반복마다 호출되어 실행된다. 일반적인 용도는 다음과 같다.
- 지속적인 백그라운드 작업 수행(이 작업을 위해 태스크를 새로 만들지 않는 것이 장점이라고 함)
- 유휴 시간 체크
- 저전력 모드 진입(그러나, 매 틱 인터럽트마다 CPU가 깨어나므로 tickless모드로 동작하는 것보단 효율이 떨어짐)
hook 함수를 작성할 때 무조건 지켜야하는 규칙은 다음과 같다.
- 스스로 blocked 상태 또는 suspended 상태에 빠지면 안된다.
vTaskDelete()가 호출된 상황에서 hook 함수가 끝나지 않는다면 커널 자원들이 절대 해제되지 않으므로 예측 가능한 시간안에 hook 함수는 종료되어야 한다.
hook 함수의 시그니처는 다음과 같다.
void vApplicationIdleHook( void );
위 함수가 실제로 유휴 태스크에 의해 호출되려면 configUSE_IDLE_HOOK 매크로가 1로 설정되어야 한다.
태스크 우선순위 변경
태스크가 생성된 이후에도 우선순위 변경이 가능하다. 함수 시그니처는 다음과 같다.
void vTaskPrioritySet(
TaskHandle_t xTask,
UBaseType_t uxNewPriority );
xTask는 우선순위를 변경할 태스크의 핸들, UBaseType_t는 그냥 typedef unsigned long UBaseType_t로, 설정하려는 우선순위 값이다. 우선순위 최대값(configMAX_PRIORITIES - 1)보다 더 큰 값을 넣으면 최대값으로 깎여 설정된다. 핸들에 NULL을 넣는다면 자신의 우선순위 값을 수정하게 된다.
현재 태스크의 우선순위 값은 다음 함수로 알 수 있다.
UBaseType_t uxTaskPriorityGet( TaskHandle_t xTask );
우선순위 값을 알고자 하는 태스크 핸들을 인자로 넘기면 반환값으로 우선순위를 얻을 수 있다. 만약 NULL값을 인자로 넘긴다면 자신의 우선순위 값을 얻는다.
태스크 삭제
태스크를 삭제하려면 우선 INCLUDE_vTaskDelete 매크로가 1이어야 한다. 태스크를 삭제하는 함수 시그니처는 다음과 같다.
void vTaskDelete( TaskHandle_t xTaskToDelete );
xTaskToDelete는 삭제할 태스크의 핸들을 의미하고, NULL을 넘긴다면 태스크 자기 자신을 의미한다.
일단 태스크가 삭제됐다면 다시는 Running 상태가 될 수 없고 존재하지 않는 태스크가 된다. 태스크가 동적 메모리 할당을 사용해 생성되었다면, 유휴 태스크가 이 메모리를 해제하는 작업을 수행한다. 앞서 살펴보았듯 유휴 태스크가 Starvation 상황에 빠진다면 메모리 해제가 일어나지 않게 된다.
유휴 태스크가 해제하는건 커널이 할당해준 메모리(TCB, 스택)뿐이다. 태스크 구현중에 할당된 메모리나 다른 자원들은 vTaskDelete를 호출하기 전에 태스크가 스스로(또는 다른 태스크가) 해제해야한다.
스레드 로컬 공간(Thread Local Storage)
재진입성(re-entrancy)을 가지는 함수들은 언제든지 다시 호출해도 다른 함수에 영향을 주지 않지만, 재진입성이 없는 함수(이하 non-reentrant 함수)들은 다른 함수들에 영향을 주지 않도록 데이터를 다룰 때 임계 영역(critical section)에 대한 관리가 필요하다. 임계 영역을 처리하게 된다면 RTOS의 성능에 영향을 주기 때문에, 스레드 로컬 공간(이하 TLS)을 쓰는 편이 더 낫다. FreeRTOS는 개발자가 데이터를 각자의 TCB에 있는 TLS에 저장할 수 있게 해준다.
대표적인 사용 예시는 errno다. 이 변수는 TLS에 저장되므로 다른 스레드에 영향을 주지 않는다.
FreeRTOS에서는 non-reentrant 함수들이 멀티스레드환경에서 잘 동작하도록 TLS를 관리하는 두 개의 라이브러리인 newlib와 picolibc를 제공한다. 각각, configUSE_NEWLIB_REENTRANCE 또는 configUSE_PICOLIBS_TLS 매크로를 정의하여 사용할 수 있다.
직접 TLS 정의하기
다음 매크로를 정의한다면 라이브러리 대신 사용자가 직접 정의한 코드들을 호출하여 TLS를 관리할 수 있다.
-
configUSE_C_RUNEIME_TLS_SUPPORT매크로가
1로 정의되어야만 C Runtime TLS 지원이 활성화된다. -
configTLS_BLOCK_TYPETLS 데이터를 저장할 타입을 정의한다.
-
configINIT_TLS_BLOCKTLS 블록을 초기화할 때 실행될 코드를 정의한다.
-
configSET_TLS_BLOCK새 태스크로 스위칭 될 때 실행될 코드를 정의한다. 이 코드는 C Runtime 함수들이 TLS를 참조할 수 있도록 포인터나 레지스터를 업데이트하는 작업을 수행한다.
-
configDEINIT_TLS_BLOCKTLS 블록을 해제할 때 실행될 코드를 정의한다.
개발자가 구현한 애플리케이션이 직접 TLS를 관리할 수도 있다. 이를 위해 configNUM_THREAD_LOCAL_STORAGE_POINTERS 매크로를 0이상의 값으로 설정해야한다.
void * pvTaskGetThreadLocalStoragePointer(
TaskHandle_t xTaskToQuery,
BaseType_t xIndex );
void vTaskSetThreadLocalStoragePointer(
TaskHandle_t xTaskToSet,
BaseType_t xIndex,
void * pvValue );
위 함수들을 사용해서 직접 정의한 TLS를 TCB에 등록하거나, 등록된 TLS를 조회할 수 있다.
태스크 스케줄링
기본적으로 FreeRTOS는 하나의 코어가 한 태스크를 실행한다. 멀티코어시스템에서도 FreeRTOS가 실행될 수 있지만 이해를 위해 일단 싱글코어시스템을 전제로 설명한다.
다시 살펴보면, Blocked, Suspended 상태가 아닌 태스크들을 Ready 상태에 있다고 하고 이들 중 가장 높은 우선순위를 갖는 태스크를 실행하게 되며 그 태스크는 Running 상태가 된다. Blocked 상태에 있는 태스크들은 다시 Ready 상태가 될 수 있는데, 두 가지 경우가 있었다. 하나는 일시적 이벤트로, 일정 시간만큼 또는 일정 주기만큼 기다린 후 깨어나는 것이다. 다른 하나는 동기화 이벤트로, 다른 태스크나 ISR이 이벤트를 발생시켜 자신이 깨어나게 되는 것이다.
스케줄링은 Ready 상태에 있는 태스크들 중 Running 상태가 될 태스크 하나를 선택하는 알고리즘이다.
스케줄링에 영향을 주는 매크로는 다음 세 가지다.
-
configUSE_PREEMPTION선점형 스케줄링을 사용할지 결정한다. 값이
0이면 비선점형(협조적) 스케줄링을 사용한다. 이 경우 작업이 끝날 때까지 컨텍스트 스위칭이 발생하지 않는다. -
configUSE_TIME_SLICING시분할, 즉 라운드로빈을 적용할지 결정한다. 값이
0이면, 동일한 우선순위의 다른 작업이 끝날 때까지 자신의 작업을 수행할 수 없습니다. 값이1이면 우선순위일 때 틱 인터럽트가 발생할 때마다 라운드로빈에 의해 컨텍스트 스위칭이 발생한다. -
configUSE_TICKLESS_IDLE틱 인터럽트마다 실행될 틱 인터럽트 훅을 호출할지 결정한다.
0이라면 호출하지 않게 되므로 소비전력을 낮출 수 있다.
시분할 알고리즘을 사용한 고정 우선순위 선점형 스케줄링 (Fixed priority preemptive scheduling with Time slicing algorithm)
우선순위가 고정되어 있고 시분할 스케줄링을 사용할 때 예시는 다음과 같다.
-
예시 1

3개의 태스크가 존재한다. 태스크1은 가장 높은 우선순위를 갖고 이벤트가 있어야지만 깨어난다. 태스크2는 중간 우선순위를 갖고 주기적으로 깨어난다. 태스크3은 낮은 우선순위를 갖고 이벤트가 있어야지만 깨어난다.
t1에는 태스크2가 깨어날 시간이 되어 깨어났고 이 떄 가장 높은 우선순위이므로 태스크2가 실행된다. 태스크2가 실행을 마치고 나서는 유휴 태스크만 남기 때문에 유휴 태스크가 실행된다.
그 뒤 t3에 이벤트가 발생해서 태스크3이 깨어났고, 이 때에도 태스크3이 가장 높은 우선순위이므로 바로 실행된다. t4까지 작업을 마친 후 다시 유휴 태스크가 실행된다.
t5에는 다시 태스크3이 깨어나는 이벤트가 발생해 태스크3이 실행되지만 t6에 태스크2가 깨어난다. 이 때 태스크2의 우선순위가 제일 높으므로 태스크2가 코어를 선점하게 된다. 태스크3은 다시 Ready 상태가 되며 t7에 작업이 끝난 이후 다시 실행된다.
t10에서 선점이 한 번 더 발생한다. 주기적으로 실행되는 태스크2의 실행 도중 태스크1이 깨어나게 되면서 태스크1이 코어를 선점한다. 태스크2가 종료된 후 바로 태스크3이 실행되는데, 왜냐하면 t9 ~ t12사이에 태스크3이 깨어났지만 더 높은 우선순위를 갖는 태스크들이 Ready 또는 Running 상태에 있기 때문에 실행 기회를 갖지 못했다.
-
예시 2

유휴 태스크와 같은 우선순위를 갖는 태스크2가 존재하고 가장 높은 우선순위를 갖는 태스크1이 존재한다. 태스크1은 동기화 이벤트가 있어야만 깨어난다.
태스크2는 유휴 태스크와 동일한 우선순위이므로 라운드로빈 방식으로 유휴 태스크와 번갈아 실행된다. t6에서 태스크1이 깨어나는 이벤트가 발생하게 되어 태스크1이 코어를 선점하게 되고, 작업을 마친 t7이후에는 다시 태스크2와 유휴 태스크가 번갈아 실행된다.
-
예시 3
예시 2의 문제점은 유휴 태스크와 애플리케이션 태스크가 실행되는 시간이 같다는 것이다. 어차피 유휴 태스크는 아무것도 하지 않는데 그 시간동안 태스크2를 실행시키면 작업의 효율이 올라간다. 이를 위해서는
configIDLE_SHOULD_YIELD매크로를1로 설정하면 된다.
configIDLE_SHOULD_YIELD를1로 설정하면 유휴 태스크는 같은 우선순위 값을 갖는 태스크가 존재할 때 한 번만 루프를 돌고 그 태스크에게 실행 시간을 양보한다.모든 타임 슬롯마다 한 번만 실행한 후 태스크2에게 실행 시간을 양보한 것을 볼 수 있다.
시분할이 없는 우선순위 기반 선점형 스케줄링
선점형 스케줄링을 사용하지만 configUSE_TIME_SLICING 매크로의 값이 0일 때를 의미한다.
우선순위에 의해 선점이 발생하지만, 동일한 우선순위를 갖는 태스크들이 실행 시간을 공평하게 가지지는 않는다. 왜냐하면 매 틱 인터럽트마다 컨텍스트 스위칭이 발생하지 않기 때문이다. 이 스케줄링 알고리즘에서 컨텍스트 스위칭은 다음 상황에서만 발생한다.
-
더 높은 우선순위를 갖는 태스크가 등장할 때
-
Running 상태의 태스크가 Ready 또는 Blocked, Suspended 상태로 갈 때
컨텍스트 스위칭의 횟수가 적게 발생하므로 오버헤드가 줄어드는 것은 사실이지만, 우선순위가 같은 태스크들이 동일한 실행 기회를 부여받지 못하는 문제가 있다.

유휴 태스크와 태스크2는 모두 같은 우선순위이지만 먼저 실행되는건 유휴 태스크다. 따라서 t1부터 유휴 태스크가 실행되며 태스크2는 실행 기회를 얻지 못한다.
t6에서 태스크1이 Ready 상태가 되고 가장 높은 우선순위이므로 바로 코어를 선점한다. 그러다가 t7에서 Blocked 상태가 되어 컨텍스트 스위칭이 발생하는데, 이 때 태스크2가 선택된다.
FreeRTOS 내부에서는 동일한 우선순위를 갖는 태스크들 중 다음에 실행될 태스크를 고르기 위해 각 우선순위 값마다 Ready List를 둡니다. FreeRTOS는 이 Ready List를 사용해 라운드로빈을 구현합니다. 연결리스트로 구현된 리스트의 첫번째 태스크를 꺼내 실행하고 실행을 마친 태스크는 제일 뒤에 추가합니다.
태스크2가 실행되다가 t9에서 다시 태스크1이 Blocked 상태에서 깨어나 선점하게 된다. t10에 다시 컨텍스트 스위칭이 발생하는데 라운드로빈 방식에 의해 유휴 태스크가 실행된다.
협조적 스케줄링 (Cooperative Scheduling)
보통은 FreeRTOS를 쓸 때 선점형 스케줄링을 사용하지만 협조적 스케줄링도 사용 가능하다. 선점형 스케줄링과 다르게 협조적 스케줄링에서는 더 높은 우선순위를 가진 태스크가 Ready 상태로 변하더라도 컨텍스트 스위칭이 발생하지 않는다. 협조적 스케줄링에서는 다음 두 가지 상황에서 컨텍스트 스위칭이 발생한다.
-
Running 태스크가 Blocked 상태로 바뀔 때
-
Running 태스크가
taskYIELD()함수를 사용하여 스스로 양보할 때

t1부터 태스크3이 실행 기회를 얻어 실행된다. t2에 태스크 3이 큐에 데이터를 씀으로 태스크2가 Ready 상태로 바뀌었지만, 선점이 발생하지 않는 스케줄링이므로 실행되지 못한다. t3에서도 세마포어에 의해 이벤트가 발생해 태스크1이 Ready 상태가 되었지만, 실행되지 못한다.
t4가 되어서야 태스크3이 taskYIELD()를 호출해 실행을 양보했다. 이 때는 더 높은 우선순위를 가진 태스크1이 실행 기회를 갖게 된다.
t5에 태스크1이 Blocked 상태가 되어 컨텍스트 스위칭이 발생한다. 이 때는 태스크2가 가장 높은 우선순위이므로 태스크2가 실행된다. 태스크2는 t6에서 Blocked 상태가 되고, 태스크3은 실행 순서를 양보만 했을 뿐 Ready 상태에 있으므로 태스크3이 다시 실행된다.
선점형 스케줄링에서는 한 태스크가 실행되는 도중 다른 태스크가 실행될 수 있으므로 데이터 오염이 발생할 수 있지만, 협조적 스케줄링에서는 태스크의 실행 순서를 개발자가 관리할 수 있으므로 데이터 오염이 발생하지 않도록 만들 수 있다. 반대로, 협조적 스케줄링에서는 즉시 사용자에게 응답해야하는 작업을 처리하기 어렵다는 단점이 있다.