Computer Science
탄탄한 기반 실력을 위한
전공과 이론 지식 모음
Today I Learned!
배웠으면 기록을 해야지
TIL 사진
Flutter 사진
Flutter로 모바일까지
거꾸로캠퍼스 코딩랩 Flutter 앱개발 강사
스파르타코딩클럽 즉문즉답 튜터
카카오테크캠퍼스 3기 학습코치
프로필 사진
박성민
임베디드 세계에
발을 들인 박치기 공룡
임베디드 사진
EMBEDDED SYSTEM
임베디드 SW와 HW, 이론부터 실전까지
ALGORITHM
알고리즘 해결 전략 기록
🎓
중앙대학교 소프트웨어학부
텔레칩스 차량용 임베디드 스쿨 3기
애플 개발자 아카데미 1기
깃허브 사진
GitHub
프로젝트 모아보기
Instagram
인스타그램 사진

Embedded System/FreeRTOS

[FreeRTOS] RTOS의 Context Switch 과정 파헤치기

sm_amoled 2025. 11. 15. 22:52

이전 글에서 Task의 생성과 구조에 대해서 알아봤으니, 이번 글에서는 “그렇다면 생성된 여러 개의 Task 들 간의 실행 흐름을 어떻게 전환하는가?” 에 대해서 알아보자. 커널은 어떻게 Task를 전환하여 Real-Time의 특성을 만족시킬까?

기본적으로 Cortex-M의 CPU는 싱글코어이기 때문에 한 번에 하나의 명령어만 실행할 수 있다. 그러니 만약 여러 Task를 한 번에 또는 특정한 Task를 먼저 실행하려고 한다면 CPU를 효율적으로 사용해줘야 한다. 이걸 어떻게 할까?

아래쪽에 PendSV 예외가 발생했을 때 어떻게 Context-Switch가 발생하는 지에 대해 기똥차게 설명해뒀다. 필요하면 해당 부분만 샥 훑어버리자.

 

혹시 Exception과 관련된 내용들이 궁금하다면, 이 글을 먼저 읽어보자!

SysTick Exception과 Handler

Context-Switch를 위해서는 SysTick Exception이 필요한게 아니라 PendSV Exception이 필요하다.
그치만, 이 PendSV 를 걸어주는 부분을 바로 가서 찾는 것보다는, Time-Slicing으로 Context-Switch가 발생하는 부분이 SysTick 이라는걸 알고있으니 SysTick Handler 코드를 파고들면서 어디에서 Switch가 되나 한 번 살펴보자! 이 흐름을 한 번 보면 Time-Slicing Round-Robin 에 대해서 이해할 수 있게 된다.

SysTick을 설정하고 시작하는 코드는 Kernel Start 쪽에 들어있다. 커널 시작 코드는 다음 글에서 살펴볼 예정! 여기에서는 그렇다면 시스템 Tick 인터럽트가 발생하면 어떤 일들이 일어나나? 에 대해서 보면 된다.

그렇다면 이 SysTick 인터럽트는 언제 발생하고, 핸들링 시점에는 어떤 일을 할까? 우리가 익히 봐서 너무 잘 알고있는 SysTick_Handler 코드에 보면 아래처럼 FreeRTOS를 위한 코드가 자동으로 추가되어있다.

void SysTick_Handler(void)
{
  HAL_IncTick();

    // 스케줄러가 이미 시작한 상태라면
  if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED){
    // Port 계층의 SysTickHandler를 호출한다.
      xPortSysTickHandler();
  }
}

포팅 레이터 쪽에서 구현되어있는 SysTickHandler의 내부는 이렇게 생겼다.

void xPortSysTickHandler( void )
{
    /* The SysTick runs at the lowest interrupt priority, so when this interrupt
    executes all interrupts must be unmasked.  There is therefore no need to
    save and then restore the interrupt mask value as its value is already
    known. */
    portDISABLE_INTERRUPTS();
    {
        /* Increment the RTOS tick. */
        if( xTaskIncrementTick() != pdFALSE )
        {
            /* A context switch is required.  Context switching is performed in
            the PendSV interrupt.  Pend the PendSV interrupt. */
            portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
        }
    }
    portENABLE_INTERRUPTS();
}
  1. SysTick Handler가 가장 낮은 우선순위를 사용하고 있기 때문에, 다른 모든 우선순위를 잠시 Block 하여 로직을 보호. 이때 System Tick 과 관련된 공유자원을 xTaskIncrementTick() 함수 내부에서 사용하고 있기 때문에 혹시 모르는 문제를 막기 위해서 인터럽트를 막아준다.
  2. RTOS의 Tick을 증가
  3. Tick 증가에 성공했다면 PendSV를 요청한다 (Pend 시킴)
  4. 인터럽트 Block 해제 → 이때 PendSV Exception이 바로 걸려서 Handler 가 호출된다. 만약 더 높은 우선순위 Interrupt가 있다면 그걸 먼저 처리한다.

xPortSysTickHandler()는 길지 않아서 이해하기 아주 편안했는데, xTaskIncrementTick() 함수를 들어가보는 순간 아찔해졌다. 그래서 넘어갈까 하다가 지금 보지 않으면 평생 보지 않을 것 같아서 짧게만 한 번 살펴보고자 했다. 대신 불필요한 코드들과 옵션에 따라 다르게 동작하는 코드들은 다 떼버리고 핵심만 살펴봤다.

결국 아래 코드에서 하는 일은 요런 것이다.

  1. System Tick을 증가시킨다
  2. Tick에 따른 Delay 리스트를 관리하면서
  3. 만약 Delay 로 인해 Blocked Task 중에서 Block 상태에서 벗어나야하는 Task를 Ready 상태로 만든다

BaseType_t xTaskIncrementTick( void )
{
TCB_t * pxTCB;
TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE;

    // trace 이름 붙은건 디버깅 용이라 안봐도됨.
    traceTASK_INCREMENT_TICK( xTickCount );

    // 만약 스케줄러가 동작 중이라면
    // 일시정지 상태라면 아래의 else로 바로 이동함
    if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
    {
        // xConstTickCount 값을 현재 counter + 1로 설정
        // 코드 내에서 TickCounter와의 비교를 많이 할건데, 
        // 이 값이 비교에만 사용되고 바뀌지 않을 것이기에 const로 지정
        const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;

        // RTOS의 TickCount 값을 1 높이기 
        xTickCount = xConstTickCount;

        // 만약 Overflow가 발생했다면 (0xFFFFFFFF -> 0x00000000)
        if( xConstTickCount == ( TickType_t ) 0U )
        {
            /* pxDelayedTaskList and pxOverflowDelayedTaskList are switched when the tick count overflows. */
            // 현재 사용중인 리스트와 오버플로우 이후를 위한 리스트를 교체
            // vTaskDelay 등으로 딜레이된 각 Task가 "몇 번째 Tick에 깨어나야하는가" 에 대한 정보를 가지고 있는 리스트
            taskSWITCH_DELAYED_LISTS();
        }

        // 혹시 가장 먼저 깨어나야할 Task의 깨어날 시간을 지났다면
        // = 지금 깨워야할 Task가 있다
        if( xConstTickCount >= xNextTaskUnblockTime )
        {
            for( ;; )
            {
                // 알고보니 Delay 중인 Task가 없었음
                if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
                {
                    // 그러면 다음 깨워야하는 시간을 가장 큰 값(0xFFFFFFFF)으로 설정해서
                    // 불필요한 깨워야할 Task 검사를 최대한 덜 하도록 하기
                    xNextTaskUnblockTime = portMAX_DELAY;
                    break;
                }
                // Delay 중인 Task가 있었다면
                else
                {
                    // Delay List의 가장 앞에 있는 Task를 꺼내고
                    pxTCB = listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList ); /*lint !e9079 void * is used as this macro is used with timers and co-routines too.  Alignment is known to be fine as the type of the pointer stored and retrieved is the same. */
                    xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );

                    // 근데 알고보니 아직 시간이 안되었다면
                    if( xConstTickCount < xItemValue )
                    {
                        // 다음 검사할 시간으로 지정하기. 
                        // 이게 남은 Task 중에서 가장 이른 시간에 깨어나는 녀석이므로, 이전에는 검사할 필요 없음.
                        xNextTaskUnblockTime = xItemValue;
                        break;
                    }

                    // Delay에서 깨어날 Task가 있으면 여기로 내려옴

                    // Delay 목록에서 제거하고
                    ( void ) uxListRemove( &( pxTCB->xStateListItem ) );

                    // 만약 기다리던 이벤트가 있었으면 이벤트 목록에서도 제거
                    if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
                    {
                        ( void ) uxListRemove( &( pxTCB->xEventListItem ) );
                    }

                    // 우선순위에 맞는 Ready 목록에 Task를 추가
                    prvAddTaskToReadyList( pxTCB );
                }
            }
        }
    }
    else
    {
        ++xPendedTicks;
    }

    return xSwitchRequired;
}

위 코드에는 어딜봐도 Context-Switch 에 대한 코드가 없다. 그저 “특정 시간이 되면 깨운다” 는 Event 에 따른 Task의 상태 변경만 수행한다.

Context-Switch는 portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; 라는 명령어가 담당하고 있다. 이 명령어는 시스템에게 PendSV 익셉션이 발생했음을 알리는 역할만 한다. 그 다음, Interrupt의 발생을 다시 활성화하는 portENABLE_INTERRUPTS()를 실행하면 Context-Switch 로직이 호출된다.

PendSV Exception과 Handler

그렇다면 Context-Switch는 이 PendSV를 활성화하도록 NVIC Interrupt Control Register에 값을 넣어주면 발생한다는 것을 유추할 수 있다.

 #define portNVIC_INT_CTRL_REG  ( * ( ( volatile uint32_t * ) 0xe000ed04 ) )

portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;

위 코드의 매크로 상수가 가리키는 주소는 ICSR 레지스터이고, 여기에는 현재 Pending 중인 인터럽트에 대한 정보를 담고있다. 이 중에서 PENDSVSET bit 에 값을 넣어주면서 PendSV 예외를 걸고있다.

그러면 프로세서에서는 HW 적으로 PendSV가 처리 대기 상태가 된다. 이때 portENABLE_INTERRUPTS(); 코드가 실행되면서 인터럽트 처리가 허용이 된다면 (다른 우선순위가 높은 인터럽트가 없다는 전제 하에) PendSV에 대한 처리를 시작한다.

그렇다면 PendSV로 어떤 코드를 자동으로 호출하게 될까? 우선 startup 코드 상에서 PendSV에 대해서 호출하는 핸들러 코드는 PendSV_Handler 이다. 이 코드가 가리키는 곳은 isr vector 테이블에 구워져있게 된다. 그렇다면 우리의 코드에서 PendSV_Handler 를 찾아보자.

// startup 코드

g_pfnVectors:
  .word  _estack
  .word  Reset_Handler

  .word  NMI_Handler
  .word  HardFault_Handler
  .word  MemManage_Handler
  .word  BusFault_Handler
  .word  UsageFault_Handler
  .word  0
  .word  0
  .word  0
  .word  0
  .word  SVC_Handler
  .word  DebugMon_Handler
  .word  0
  .word  PendSV_Handler
  .word  SysTick_Handler

... 중간생략 ... 

   .weak      SVC_Handler
   .thumb_set SVC_Handler,Default_Handler

   .weak      DebugMon_Handler
   .thumb_set DebugMon_Handler,Default_Handler

   .weak      PendSV_Handler
   .thumb_set PendSV_Handler,Default_Handler

   .weak      SysTick_Handler
   .thumb_set SysTick_Handler,Default_Handler              

PendSV_Handler 로 프로젝트 내에서 탐색을 해보면, config 파일에서 이 함수의 이름을 xPortPendSVHandler 로 바꾸어서 사용해주는 것을 볼 수 있다. 네이밍 컨벤션을 맞추기 위함이겠지? 다른 핸들러들의 이름도 아래처럼 변경되어있다.

/* Definitions that map the FreeRTOS port interrupt handlers to their CMSIS
 standard names. Based on the architecture  choosen, the below definitons may be needed*/

 #define xPortPendSVHandler PendSV_Handler
 #define vPortSVCHandler SVC_Handler
 #define xPortSysTickHandler SysTick_Handler

그렇다면 이 함수 이름을 한 번 따라가볼까? 방심하고 따라갔는데 갑자기 어셈블리어가 와락 등장한다. 그러나 핵심은 결국 아래 3가지 동작을 수행하는 것이다.

  1. 현재 작업중인 컨텍스트를 Stack 에 저장
  2. 다음 Task를 선택
  3. 선택된 Task의 컨텍스트를 불러오기

코드를 한 번 살펴보면 다음과 같다. 그런데, 코드만 보고 이해하기에는 조금 까다롭다. 그래서 시청각 자료를 준비했다. 아래에서 그림과 비교하면서 이해를 해보자.

너무 길면 숨 참고 스크롤 조금만 내리자. 친근한 그림이 나온다!

#define xPortPendSVHandler PendSV_Handler

void xPortPendSVHandler( void )
{
    /* This is a naked function. */

    __asm volatile
    (
// ******** 현재 작업중인 TASK를 저장하기 ******** 
    "    mrs r0, psp                                  \n"
    // 현재 Task의 SP를 r0에 가져오기
    "    isb                                              \n"
    "                                                    \n"
    // r2에 현재 실행중인 TCB를 가리키기
    "    ldr    r3, pxCurrentTCBConst            \n" /* Get the location of the current TCB. */
    "    ldr    r2, [r3]                              \n"
    "                                                    \n"
    "    tst r14, #0x10                            \n" /* Is the task using the FPU context?  If so, push high vfp registers. */
    "    it eq                                          \n"
    "    vstmdbeq r0!, {s16-s31}                \n"
    "                                                    \n"
    // r4~r11, LR를 Stack에 저장
    "    stmdb r0!, {r4-r11, r14}            \n" /* Save the core registers. */
    // 변경된 SP를 TCB의 pxTopOfStack에 저장 (TCB 주소의 가장 첫 메모리영역)
    "    str r0, [r2]                              \n" /* Save the new top of stack into the first member of the TCB. */
    "                                                    \n"

// ******** TASK 전환 ********
  // TCB 지정에 사용한 레지스터는 Stack에 잠시 저장
    "    stmdb sp!, {r0, r3}                      \n"
    // configMAX_SYSCALL_INTERRUPT_PRIORITY를 BASEPRI에 설정하여 
    // 낮은 우선순위의 인터럽트를 차단
    "    mov r0, %0                                   \n"
    "    msr basepri, r0                            \n"
    "    dsb                                              \n"
    "    isb                                              \n"

    // vTaskSwitchContext 에서 다음 Task를 선택
    "    bl vTaskSwitchContext                  \n"

    // 낮은 우선순위의 인터럽트 차단을 다시 해제
    "    mov r0, #0                                  \n"
    "    msr basepri, r0                            \n"

// ******** 다음 작업할 TASK를 불러오기 ********
    // 잠시 Stack에 담았던 SP, CurrentTCB의 주소를 불러오기
    "    ldmia sp!, {r0, r3}                      \n"
    "                                                    \n"
    // 새로 선택된 CurrentTCB의 SP를 가져오기
    "    ldr r1, [r3]                              \n" /* The first item in pxCurrentTCB is the task top of stack. */
    "    ldr r0, [r1]                              \n"
    "                                                    \n"
    // 기존 Context를 복원
    "    ldmia r0!, {r4-r11, r14}            \n" /* Pop the core registers. */
    "                                                    \n"
    "    tst r14, #0x10                            \n" /* Is the task using the FPU context?  If so, pop the high vfp registers too. */
    "    it eq                                          \n"
    "    vldmiaeq r0!, {s16-s31}                \n"
    "                                                      \n"
    // 새로운 Task의 SP를 PSP 레지스터에 올리기
    "    msr psp, r0                                  \n"
    "    isb                                                 \n"
    "                                                    \n"
    "                                                      \n"
    // 하드웨어가 자동으로 R0~R3, R12, LR, PC, CPSR을 복원
    "    bx r14                                          \n"
    "                                                    \n"
    "    .align 4                                    \n"
    "pxCurrentTCBConst: .word pxCurrentTCB    \n"
    ::"i"(configMAX_SYSCALL_INTERRUPT_PRIORITY)
    );
}

PendSV Handler가 Context-Switch를 수행하는 방법

나름 이 설명이 명쾌하다고 생각한다. 나름… 더 상세하게 설명할 수 있는 방법은 있겠지만… 나는 노력했다.

우선 현재 A Task를 실행중이라고 해보자. 그리고 여기에서 PendSV 인터럽트가 걸렸고, B Task로 작업 흐름이 넘어가는 상황이라고 해보자.

사전 정보) Exception이 걸리면 기본적으로 HW가 8개의 레지스터 정보를 저장한다. 이건 ARM에서 정한 표준이다. Stack에 순서대로 xPSR, PC, LR, R12, R3~R0 을 저장한다. [xPSR, PC, LR, R12] 는 현재 프로세스(Task)의 실행 환경을 저장하기 위함이고, [R3~R0] 은 ‘어떤 어셈블리 코드에서든 함부로 쓰자고 합의한’ 임시 레지스터이기 때문에 다른 Context를 실행하다가 돌아오면 원래 레지스터 값이 남아있을 가능성의 거의 없다. 그래서 HW가 자동으로 저장을 해준다.

Cortex-M 에서는 이외에도 이만큼의 레지스터를 제공하고 있고, 넘어간 Task 에서 어떤 짓을 할 지 모르기 때문에 혹시나 하는 마음에 다른 레지스터들도 저장해줄 필요가 있다.

자, 그럼 Task A → Task B 로 전환되는 과정을 한 번 살펴보자!! 💃 🕺

(1) 실행중인 Task의 TCB(Task Control Block)를 가리키는 pxCurrentTCB 에는 A의 TCB 주소가 담겨있다. 그리고 CPU의 레지스터 뱅크에는 Task A 의 실행 환경이 담겨있다.

(2) 작업을 하다가 Context Switch가 발생하는 경우 PendSV 인터럽트가 발생하면서 Main으로 제어 흐름이 넘어갈 때, HW 적으로 8개의 레지스터 정보는 자동으로 Stack에 저장되고 Handler Mode로 전환되면서 Main으로 넘어온다. (이때는 실행 흐름이 Handler Mode 이기 때문에 SP가 Main Stack Pointer가 된다)

(3) 필요한 레지스터들을 SW 적으로 저장한다. 이때 저장해야하는 코드는 Exception이 발생해서 Thread 모드에서 여기로 왔다는 의미인 EXC_RETURN (LR 레지스터에 담겨있음)과 R4-R11 레지스터들. 이건 HW가 자동으로 넘겨주지 않지만, 값 보존을 위해서 어셈블리 코드로 직접 A Stack 에 넣어준다. A Stack의 top 정보를 TCB에 반영하는 것도 포함되어야 한다.

// ******** 현재 작업중인 TASK를 저장하기 ******** 
    "    mrs r0, psp                                  \n"
    // 현재 Task의 SP를 r0에 가져오기
    // 실행 흐름이 Handler Mode로 넘어왔기에 내 SP는 MSP를 가리키지만
    // Thread Mode 일 때의 SP에 PSP 라는 이름으로 접근이 가능하여, r0로 Stack의 TOP을 가져온다.
    "    isb                                              \n"
    "                                                    \n"
    // r2에 현재 실행중인 TCB를 가리키기
    "    ldr    r3, pxCurrentTCBConst            \n" /* Get the location of the current TCB. */
    "    ldr    r2, [r3]                              \n"
    "                                                    \n"
    // 여기는 FPU 쓰는지 검사하고 추가로 작업하는 코드. 우선은 넘어가자.
    "    tst r14, #0x10                            \n" /* Is the task using the FPU context?  If so, push high vfp registers. */
    "    it eq                                          \n"
    "    vstmdbeq r0!, {s16-s31}                \n"
    "                                                    \n"
    // r4~r11, LR를 Stack에 저장
    "    stmdb r0!, {r4-r11, r14}            \n" /* Save the core registers. */
    // 변경된 SP를 TCB의 pxTopOfStack에 저장 
    // (r2가 담고있는 TCB 주소의 가장 첫 메모리영역이 pxTopOfStack 이라 가능함)
    "    str r0, [r2]                              \n" /* Save the new top of stack into the first member of the TCB. */
    "                                                    \n"

(4) 그 다음, 커널 쪽에서 CurrentTCB가 가리키는 Task를 전환한다.

// ******** TASK 전환 ********
  // TCB 지정에 사용한 레지스터는 Stack에 잠시 저장
    "    stmdb sp!, {r0, r3}                      \n"
    // configMAX_SYSCALL_INTERRUPT_PRIORITY를 BASEPRI에 설정하여 
    // 낮은 우선순위의 인터럽트를 차단
    // == Enter Critical Section
    "    mov r0, %0                                   \n"
    "    msr basepri, r0                            \n"
    "    dsb                                              \n"
    "    isb                                              \n"

    // vTaskSwitchContext 에서 다음 Task를 선택
    "    bl vTaskSwitchContext                  \n"

    // 낮은 우선순위의 인터럽트 차단을 다시 해제
    // == Exit Critical Section
    "    mov r0, #0                                  \n"
    "    msr basepri, r0                            \n"

여기에서 코드를 살펴보면, 인터럽트 차단 → vTaskSwitchContext 로 branch → 인터럽트 허용 을 수행하고 있다. 전형적인 Critical Section 을 설정하는 코드. 여기에서 vTaskSwitchContext 함수가 다음 실행할 Task를 선택하는 (CurrentTCB의 값을 변경하는) 로직을 포함하고 있다. 이 부분은 메모리 흐름을 파악한 후 아래에서 한 번 살펴보자.

(5) B Stack에 있는 데이터들을 SW 적으로 레지스터로 이동한다. 여기에서 이동시켜줘야 하는 레지스터들은 A 에서 해준 작업과 딱 반대로 하면 된다.

// ******** 다음 작업할 TASK를 불러오기 ********
    // 잠시 Stack에 담았던 SP, CurrentTCB를 가리키는 주소를 불러오기
    "    ldmia sp!, {r0, r3}                      \n"
    "                                                    \n"
    // 새로 선택된 CurrentTCB가 가리키는 값에서 TCB의 Stack Top를 가져오기
    "    ldr r1, [r3]                              \n" /* The first item in pxCurrentTCB is the task top of stack. */
    "    ldr r0, [r1]                              \n"
    "                                                    \n"
    // 기존 Context를 복원
    "    ldmia r0!, {r4-r11, r14}            \n" /* Pop the core registers. */
    "                                                    \n"
    // 이것도 FPU 관련 코드임
    "    tst r14, #0x10                            \n" /* Is the task using the FPU context?  If so, pop the high vfp registers too. */
    "    it eq                                          \n"
    "    vldmiaeq r0!, {s16-s31}                \n"

(6) 마지막으로 bx lr 명령어로 레지스터들을 HW 적으로 가져온다. 어디에서 데이터들을 가져올 지를 알려주기 위해서 PSP (Process Stack Pointer)에 Task B의 Stack Top 값을 넣어준다. 그리고 어떤 값들을 복구시키고 어떤 모드로 전환시킬지 알려주기 위해서 LR 레지스터에 EXC_RETURN 값을 지정해준 다음 (이건 앞서 5단계에서 명령어로 r14를 미리 넣어줬음) bx lr 명령어로 레지스터 뱅크에 나머지 레지스터 값들을 자동으로 넣어준다.

 


여기에서 EXC_RETURN의 주소는 0xFFFF.... 이다. 메모리 맵 상에서 0xFF는 절대 명령어가 있을 주소가 아닌데, 이게 bx 명령어로 전달되면 프로세스에서 "아 이거는 명령어로 branch하라는게 아니라 다른 의미의 명령이다" 라고 해석해서 예외로부터의 복귀를 위한 HW적인 레지스터 복구를 처리한다.

 

 

 

(7) CPU는 결국 CPU 레지스터 뱅크에서 PC에 들어있는 명령어 주소를 수행하기 때문에, 다음 실행되는 명령어는 B Task의 PC (return address) 값이다. 그럼 지금 상황은 다음과 같다.

  • 기존에 B 가 실행하던 레지스터 값들을 모두 복구시켰음
  • B의 Stack은 기존 상태로 돌아갔음
  • 지금 실행되는 명령어는 B에서 마지막으로 실행하던 명령어 다음 줄임

이제 B Task로 실행흐름이 완전히 넘어왔다. 이 과정이 PendSV 핸들러에서 수행하는 Context-Switch 과정이다!

그렇다면 다음 실행할 Task는 어떻게 찾을까?

중간에 갑자기 등장했던 vTaskSwitchContext 함수는 C언어로 아래처럼 구현되어있다. (C언어야 반가워!)

void vTaskSwitchContext( void )
{
    // 스케줄러가 잠시 멈춰있다면 -> Context-Switch 안함
    if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
    {
        /* The scheduler is currently suspended - do not allow a context switch. */
        xYieldPending = pdTRUE;
    }
    // 스케줄러가 실행중이라면
    else
    {
        xYieldPending = pdFALSE;

        /* Check for stack overflow, if configured. */
        taskCHECK_FOR_STACK_OVERFLOW();

        // 여기에서 다음 실행할 우선순위가 가장 높은 Task를 선택한다.
        taskSELECT_HIGHEST_PRIORITY_TASK(); 
    }
}

내용을 쭉 훑어보면 결국 Scheduler 가 활성화 되어있는 상태에서 taskSELECT_HIGHEST_PRIORITY_TASK 함수를 통해 다음 실행할 Task를 선택한다. (Suspend 상태가 아닌 경우!)

이 매크로 함수는 아래처럼 생겼는데,

// 현재 Ready 상태인 Task 중에서 가장 우선순위가 높은 Task를 찾아서 pxCurrentTCB의 값을 교체한다. 
#define taskSELECT_HIGHEST_PRIORITY_TASK()                                                        \
{                                                                                                \
UBaseType_t uxTopPriority;                                                                        \
                                                                                                \
    /* Find the highest priority list that contains ready tasks. */                                \
    portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );                                \
    configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 );        \
    listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );        \
} /* taskSELECT_HIGHEST_PRIORITY_TASK() */

// 더 우선순위가 높은 Task로 pxCurrentTCB의 값을 교체한다.
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )                                        \
{                                                                                            \
List_t * const pxConstList = ( pxList );                                                    \
    /* Increment the index to the next item and return the item, ensuring */                \
    /* we don't return the marker used at the end of the list.  */                            \
    ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;                            \
    if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) )    \
    {                                                                                        \
        ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;                        \
    }                                                                                        \
    ( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;                                            \
}

GPT를 돌려서 매크로 함수를 기본 함수 형태로 잠시 바꿔봤다. (아주 편하구만!)

여기에서 하는 일은 현재 등록된 가장 높은 우선순위를 찾고, 해당 우선순위의 Ready List 에서 Task를 하나 가져오고 pxCurrentTCB에 넣어준다. Round-Robin을 위한 작업도 들어있음.

/**
 * @brief 가장 높은 우선순위의 Ready Task를 선택하여 pxCurrentTCB를 업데이트
 */
void taskSelectHighestPriorityTask(void)
{
    UBaseType_t uxTopPriority;
    List_t *pxConstList;

    // 1단계: 가장 높은 우선순위 찾기
    // uxTopReadyPriority의 각 비트는 해당 우선순위에 Ready Task가 있는지 표시
    // 예: 0b00101000 → 우선순위 3, 5에 Ready Task 있음
    // CLZ(Count Leading Zeros)로 최상위 1 비트 위치 찾기
    uxTopPriority = (31UL - __builtin_clz(uxTopReadyPriority));

    // 2단계: 해당 우선순위의 Ready List 참조
    pxConstList = &(pxReadyTasksLists[uxTopPriority]);

    // 3단계: Round-Robin 방식으로 다음 Task 선택
    // pxIndex를 다음 항목으로 이동
    pxConstList->pxIndex = pxConstList->pxIndex->pxNext;

    // 4단계: 리스트 끝(Marker) 확인
    // 원형 리스트에서 xListEnd는 실제 Task가 아닌 마커
    // 끝에 도달했으면 다시 한 번 이동 (첫 번째 Task로)
    if (pxConstList->pxIndex == &(pxConstList->xListEnd)) {
        pxConstList->pxIndex = pxConstList->pxIndex->pxNext;
    }

    // 5단계: 선택된 ListItem의 Owner(TCB)를 현재 Task로 설정
    pxCurrentTCB = (TCB_t *)(pxConstList->pxIndex->pvOwner);
}

SysTick 타이머에 의한 Context-Switch 정리

지금까지의 과정을 다시 돌아보면 SysTick 타이머에 의한 Context-Switch는 다음 순서로 진행된다.

  1. SysTick 인터럽트가 발생
  2. SysTick Handler에서 PendSV 인터럽트를 발생
  3. PendSV에서 Task A 의 실행 환경을 모두 Task A Stack 에 저장
  4. Hander Mode 에서 pxCurrentTCB 가 가리키는 Task를 다음 Task로 변경
  5. Task B의 Stack 에서 실행 환경을 모두 복구
  6. Task B 수행

이게 되는게 마법이 아니라 사람이 한땀한땀 작성한 코드였다고??? 그럴리가 없는데,,,,

320x100