이전 글에서 Task의 생성과 구조에 대해서 알아봤으니, 이번 글에서는 “그렇다면 생성된 여러 개의 Task 들 간의 실행 흐름을 어떻게 전환하는가?” 에 대해서 알아보자. 커널은 어떻게 Task를 전환하여 Real-Time의 특성을 만족시킬까?
ㅤ
FreeRTOS의 핵심, System Exception
FreeRTOS에서 사용하는 System Exception은 3가지가 있다.
- SysTick : 주기적인 타이머
- PendSV : SW가 의도적으로 발생시키는 Context-Switch를 위한 익셉션
- SVC : SW가 호출하는 익셉션ㅤ
Exception VS Interrupt
- Exception은 프로세서 내부에서 연산의 결과로 발생하는 것
- Interrupt는 프로세서 외부 장치에서 비동기적으로 발생하는 것
ㅤ
System Exception의 우선순위
FreeRTOS에서는 SysTick과 PendSV Exception의 우선순위를 가장 낮추고, SVC Exception이 가장 높은 우선순위를 갖도록 하였다.
- SysTick, PendSV → 우선순위 15 (최하위)
- SVC → 우선순위 0 (최상위)

ㅤ
만약 PendSV의 우선순위가 높았다면 다음과 같은 상황이 발생할 수 있다.
A Task를 실행하다가
- Timer 인터럽트가 발생!
Timer 인터럽트 처리중
- PendSV 인터럽트가 발생!
Timer 인터럽트 처리를 중단하고 PendSV 인터럽트 처리 (Context Switch)
B Task로 전환
-> Timer 인터럽트 처리가 완료되지 않았는데 B로 넘어감
인터럽트에 대한 응답 속도가 느려짐
ㅤ
인터럽트에 대한 처리가 우선시 되도록 하기위해서 PendSV의 우선순위를 최대한 낮춰서 인터럽트의 응답시간을 보장한다.
A Task를 실행하다가
- Timer 인터럽트가 발생!
Timer 인터럽트 처리중
- PendSV 인터럽트가 발생!
Timer 인터럽트 처리를 이어서 진행
처리 이후에 PendSV 인터럽트 처리 (Context Switch)
B Task로 전환
-> Timer 인터럽트 처리가 완료되고나서 B로 넘어감
인터럽트에 대한 응답 속도를 보장
ㅤ
SysTick Exception 또한 Time Slice에 대해서 PendSV를 호출하도록 부르는 역할을 하기 때문에, Context Switch와 관련된 Exception이다. 즉, 시스템의 Real-Time 적인 측면에서는 우선순위가 떨어지기 때문에 우선순위가 최하위로 설정되어야 한다.
ㅤ
아래처럼 포팅 레이어의 xPortStartScheduler 코드에서 인터럽트의 우선순위를 가장 낮게 잡아주는 것을 볼 수 있다.
BaseType_t xPortStartScheduler( void )
{
...
/* Make PendSV and SysTick the lowest priority interrupts. */
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
...
/* Start the first task. */
prvPortStartFirstTask();
...
return 0;
}

/* The lowest interrupt priority that can be used in a call to a "set priority" function. */
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15
#define configPRIO_BITS 4
/* Interrupt priorities used by the kernel port layer itself. These are generic
to all Cortex-M ports, and do not rely on any particular library functions. */
#define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
#define portNVIC_SYSPRI2_REG ( * ( ( volatile uint32_t * ) 0xe000ed20 ) )
#define portNVIC_PENDSV_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 16UL )
#define portNVIC_SYSTICK_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 24UL )
// 0xe000ed20 의 주소에서 PENDSV와 SYSTICK에 해당하는 [16:23], [24:31] bit에
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
재밌는건, 우선순위 8bit에다가
0x0F8bit를 넣는게 아니라0xF0처럼 상위 4 bit 에다가 우선순위를 작성해주고 있다. 아마도 Group Priority + Subgroup Priority를 위한 컨벤션이 아닐까?
→ 질문을 했는데, 이런 용도로도 활용이 가능하다고 한다. Group 우선순위는 누가 누구를 선점하냐 에 대한 우선순위이고, Subgroup 우선순위는 Ready 상태에 있을 때 동일한 Group 우선순위를 가지고 있는 Task 들 중에서 누가 먼저 스케줄러의 선택을 받을 것인가를 결정한다. (Preemption은 아니지만 먼저 처리될 수 있도록 해줌)
ㅤ
이와 반대로 SVC 는 기본적으로는 Unprivileged → Privileged로 권한을 상승시켜서 특정한 작업을 처리하기 위해서 사용하는 인터럽트이다.
ㅤ
단, 현재 FreeRTOS 에서는 가장 처음에 vTaskStartScheduler() 함수를 통해 첫 Task를 실행시키는 곳에서만 사용되고 있다. 이 Task를 시스템에 올리는 핵심적인 과정에서 다른 인터럽트의 방해 없이 온전히 작업을 처리하도록 하기 위해서 우선순위를 가장 높은 0을 주고있다.

근데 얘는 기본값이 0이라서 굳이 따로 0으로 초기화를 해줄 필요는 없다!

초기화 코드가 실행되고나면 이렇게 메모리에 우선순위가 15(F)로 초기화된다..
ㅤ
왜 하필 Context Switch를 위해 PendSV Exception를 쓰지?
그렇다면 또 궁금한 점이 생겼다. 왜 하필 다른 인터럽트들도 있을텐데 Context Switch를 할 때 PendSV를 부르는거지? Cortex-M에서 가지고 있는 Exception 들을 살펴봤다.

ㅤ
여기 표에서 살펴보면 다양한 익셉션들이 들어있다.
- NMI - Non-Maskable Interrupt : Reset 을 제외하면 가장 우선순위가 높(아야한)다. 마스킹 할 수 없는 인터럽트.
- Hard Fault : exception의 처리 도중 발생한 문제. Hard Fault 까지는 우선순위를 설정할 수 있는 Exception 보다 항상 높은 우선순위를 갖는다.
- Memory Management Fault : 메모리 접근 관련 Fault
- Bus Fault : Bus Transaction 관련 Fault
- Usage Fault : 명령어 관련 Fault (유효하지 않은 명령어, 잘못된 메모리 주소, 명령어 수행 시 상태값 문제 등)
- SVCall - Supervisor Call : OS 환경에서 OS 커널 또는 디바이스 드라이버에게 작업을 요청할 때 사용
- PendSV - Pendable SerVice Call : 시스템 레벨에게 인터럽트 기반의 요청을 보낼 때 사용. 주로 OS 환경에서 Context Switch를 위해 사용.
- SysTick : Timer에 의한 인터럽트. OS환경에서 System Tick으로 사용.
- Interrupt : 주변장치 또는 코드에 의해 비동기적으로 발생하는 인터럽트.
ㅤ
여기에서 미래를 위해 남겨둔 Reserved를 제외하면, 다른 Exception 들은 모두 특수한 목적이 있기 때문에 Context-Switch 요청을 위해서 호출하기 애매하다. 굳이 여기에서 Context-Switch를 위해서 사용할 만한 예외는 SVC 정도가 있다고 생각했는데, 여기에도 약간의 고려해야할 문제가 있다.
ㅤ
중요한 Exception을 처리하다말고 Context-Switch가 되어버리면서 인터럽트 응답 속도가 떨어지는 문제가 발생할 수 있기 때문에, Context-Switch를 위한 Exception은 우선순위가 가장 낮아야한다 → 우선순위가 실제로 가장 낮거나, 우선순위의 설정이 가능(Configurable)해야한다. 만약 PendSV를 사용하지 않는다고 할 때, SVC의 여러 동작 중 하나로 Context-Switch를 넣어주게 된다면 SVCall Exception의 우선순위가 낮아져야하는데, 이러면 낮은 우선순위로 인해 SVC의 처리 속도가 늦춰지게된다. (이건 원하지 않았던 부작용임)
ㅤ
우선순위가 가장 낮게 설정될 수 있다면, 다른 인터럽트들이 모두 처리가 될 때 까지 Context-Switch를 위한 Exception은 자신의 순번을 기다릴 수 있어야(Pendable) 한다. 더 급한 인터럽트들의 처리가 완료되고나면 PendSV 의 Handler를 통해 Switch를 수행한다. 요 부분은 NVIC의 ISPR 레지스터를 통해 아직 인터럽트가 처리되지 않았음을 알 수 있기에, 다른 인터럽트들도 가지고있는 특성이긴 하다!
ㅤ
그렇다면 Exception이 아닌 Interrupt로 Context-Switch를 호출하도록 했다면? 누군가 설정을 변경해 External Interrupt와 연결된 GPIO를 실수로 다른 모드로 활성화시켜버린다면, Context-Switch를 호출시키는 Interrupt가 발생하지 않게되는 또 다른 문제가 생길 수 있다. (해당 Interrupt가 Context-Switch 전용이 아니어서 발생하는 문제)
ㅤ
그래서 다음의 조건을 만족하는 Exception을 일부러 만들어서 프로세서 설계에 추가하였다.
- 우선순위를 최하위로 변경할 수 있어야 한다.
- 특정 목적만을 위해 사용할 수 있어야 한다.
- Pend 가능해야함.
ㅤ
왜 그러면 Context-Switch Exception이 아니라 PendSV Exception 이라는 이름을 쓰지?
라는 것도 사실 궁금했다. 이게 ARM에서 RTOS에서 Context-Switch를 처리하기 위한 전용 Exception으로 Cortex-M에 넣어준 기능이라면, 왜 이름을 CS Exception 같은걸 사용하지 않았을까?
ㅤ
사실 이건 간단한 이유였다. Cortex-M은 RTOS 만을 위해서 사용하는 프로세서가 아니니깐! 만약 RTOS를 사용하지 않고 베어메탈로 펌웨어를 작성한다거나, 네트워크 기능을 위해서 SW를 작성하고 있었다면 이걸 Context-Switch 를 위해서가 아니라 다른 목적으로 Exception을 활용하게 된다. 이때에도 동일한 뉘앙스로 Exception을 호명하기 위해서 이 Exception의 특징을 반영하여 Pendable Service Call 이라는 이름을 붙여주었다.
ㅤ
열심히 쓰다보니 Context-Switch 쪽 내용이 너무 길고 많아져서, 글을 2개로 분리했다.
이어지는 내용이니 Context-Switch에 대한 글을 바로 읽었으면 좋겠다.
'Embedded System > FreeRTOS' 카테고리의 다른 글
| [FreeRTOS] 커널의 시작과 첫번째 Task의 실행 (0) | 2025.11.21 |
|---|---|
| [FreeRTOS] Task의 스케줄링 (1) | 2025.11.19 |
| [FreeRTOS] RTOS의 Context Switch 과정 파헤치기 (0) | 2025.11.15 |
| [FreeRTOS] Task의 생성과 관리 (0) | 2025.11.15 |
| [FreeRTOS] RTOS와 FreeRTOS가 뭔데요? (1) | 2025.11.11 |