임베디드/임베디드 프로젝트

FreeRTOS 들여다보기4 - Scheduler2(Context switching)

fish9903 2023. 8. 25. 11:16

사용하는 보드와 freeRTOS 버전 정보, 진행 내용 등에 대한 정보는 github에 있습니다. (계속 업데이트 중..)

https://github.com/fish9903/FreeRTOS-STM32G4

 

GitHub - fish9903/FreeRTOS-STM32G4: FreeRTOS (Real Time Operating System) on STM32G474(ARM cortex M4 based mircocontrollers)

FreeRTOS (Real Time Operating System) on STM32G474(ARM cortex M4 based mircocontrollers) - GitHub - fish9903/FreeRTOS-STM32G4: FreeRTOS (Real Time Operating System) on STM32G474(ARM cortex M4 based...

github.com


이전 포스트 내용(scheduler1)과 이어진다.

 

1. Conext Switching

Scheduler는 task를 변경하기 위해 context switching을 한다.

 

Context switching은 CPU에서 실행중인 task를 다른 task로 바꾸는 과정을 의미한다.

RTOS에선 scheduler에 의해 이루어지고, freeRTOS에선 PendSV Handler가 이를 수행한다.

 

Priority scheduling을 하기때문에 RTOS tick interrupt가 발생할 때 마다 scheduler는 현재 실행중인 task와 ready list에 있는 task들을 비교하여 너 높은 우선순위 task가 실행될 수 있도록 관리해야 한다.

 

Task Context switching도 ARM mode 변경 과정과 비슷하게 이루어지는데, 여기서의 context가 무엇인지 살펴보자.

 

ARM mode, register 참고... https://fish9903.tistory.com/entry/ARM-Mode

 

Task의 context는 registerstack 두가지로 나눠 볼 수 있다.

Task는 주어진 작업을 수행하기 위해 register와 stack을 사용한다. 

여기서 register는 R0~R12, R13(SP), R14(LR), R15(PC), PSR가 중요하다.

 

또한 각 task는 자신의 고유한 stack 영역을 가진다. 작업을 수행하면서 push, pop하는 경우 이 stack을 사용한다.

 

따라서 task context switching을 하기 위해선 사용중인 register와 stack영역을 저장, 기억해두어야 한다. (이후에 다시 작업을 이어하기 위해)

이 정보는 TCB(Task Context Block)에 저장된다.

 

TCB struct 일부 (task.c)

각 task는 자신의 TCB를 가진다. TCB는 struct 구조로, task에 대한 정보를 가지고 있다.

대표적으로 위 코드를 보면 pxTopOfStack 포인터 변수를 사용하여 자신의 stack top 주소를 저장하고 이를 사용해 stack을 참조한다.

 

2. Task switching precedure

Task switching 과정을 보자

Task switching 과정은 실행중인 task가 CPU에서 나가는 switching out 과 들어오는 switching in 과정으로 나눌 수 있다.

 

- Task switching out precedure

FreeRTOS에선 실행중인 task가 나갈때 실행되는 작업은 다음과 같다.

 

0. SysTick interrupt 발생

1. 사용중인 register 일부를 자신의 stack 영역에 저장 (프로세서가 자동으로 함)

2. Context switching이 필요한 경우, SysTick timer가 PendSV Exception을 발생시키고 PendSV handler 실행

3. PendSV handler에서 나머지 register도 자신의 stack 영역에 저장 (manual하게 해야 함)

4. 자신의 stack의 top 주소를 갱신하여 TCB의 첫번째 변수(pxTopOfStack)에 저장

(추후에 돌아오기 위해 stack 주소 저장하는 것)

5. CPU에서 실행될 task 선택 

 

3번 이후의 과정은 PendSV handler에서 이루어진다.

PendSV handler 코드를 보자.

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

    __asm volatile
    (
        "	mrs r0, psp							\n"
        "	isb									\n"
        "										\n"
        "	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"
        "	stmdb r0!, {r4-r11, r14}			\n"/* Save the core registers. */
        "	str r0, [r2]						\n"/* Save the new top of stack into the first member of the TCB. */
        "										\n"
        "	stmdb sp!, {r0, r3}					\n"
        "	mov r0, %0 							\n"
        "	msr basepri, r0						\n"
        "	dsb									\n"
        "	isb									\n"
        "	bl vTaskSwitchContext				\n"
        "	mov r0, #0							\n"
        "	msr basepri, r0						\n"
        "	ldmia sp!, {r0, r3}					\n"
        "										\n"
        "	ldr r1, [r3]						\n"/* The first item in pxCurrentTCB is the task top of stack. */
        "	ldr r0, [r1]						\n"
        "										\n"
        "	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"
        "	msr psp, r0							\n"
        "	isb									\n"
        "										\n"
        #ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata workaround. */
            #if WORKAROUND_PMU_CM001 == 1
                "			push { r14 }				\n"
                "			pop { pc }					\n"
            #endif
        #endif
        "										\n"
        "	bx r14								\n"
        "										\n"
        "	.align 4							\n"
        "pxCurrentTCBConst: .word pxCurrentTCB	\n"
        ::"i" ( configMAX_SYSCALL_INTERRUPT_PRIORITY )
    );
}

 

이 코드는 task switching out --> task switching in 순서로 작성되어 있다.

중요한 부분을 읽어보자

 

mrs r0, psp

r0 register에 psp 값을 넣고 있다.

PSP는 (Process Stack Pointer)로 Stack Pointer 중 하나이다. 해당 프로세스의 stack의 top 주소를 저장하고 있는 변수이다. (MSP = Main Stack Pointer, kernel stack에서 사용)

이 값을 우선 r0에 넣어 저장해둔다. (백업)

 

ldr r3, pxCurrentTCBConst
ldr r2, [r3]

그리고 r3에 pxCurrentTCBConst 값을 넣고 있다.

이 변수는 전역 변수이고 현재 실행중인 task의 TCB 주소를 저장하고 있다. (task.c에 선언되어 있음)

이 포인터가 가리키는 주소를 r3에 넣은 뒤, 이를 역참조하여 r2에 해당 주소에 있는 값을 저장한다.

 

예)

pxCurrentTCBConst = 0x1000, 0x1000에는 0x2000이라는 값이 저장되어 있으면

r3 = 0x1000, r2 = 0x2000 값이 들어간다.

그리고 현재 실행중인 task의 TCB 주소는 r2값인 0x2000임

현재 실행중인 TCB의 주소가 0x2000이고, TCB struct의 첫번째 변수가 pxTopOfStack이므로 0x2000주소에 접근하면 이 변수에 접근할 수 있다.

r2에 0x2000을 넣었으므로 pxTopOfStack 변수가 0x2000값을 가진다.

 

stmdb	r0!, {r4-r11, r14}
str	r0, [r2]

다음으로 나머지 register들(r4-11, r14)을 개인 stack에 쌓는다. r0가 psp 값을 저장하고 있으므로, r0를 참고하여 그 위치부터 쌓는다.

(r0-r3, r12, LR, PC, PSR reigster는 이미 processor가 자동으로 task 개인 stack에 저장한다)

 

다 쌓고 나면 stack의 top이 바뀔수도 있기 때문에 r2주소에 있는 값을 r0로 갱신한다.

r2는 현재 실행중인 TCB의 주소를 저장하고 있으므로 이를 역참조하면 현재 실행중인 TCB struct에 접근할 수 있다. TCB struct의 첫번째 변수는 pxTopOfStack이므로 r0의 값인 PSP의 값이 이 변수에 들어가는 것이다. Stack의 top이 바뀐 것을 반영하는 과정이다.

 

bl vTaskSwitchContext

그리고 vTaskSwitchContext 로 넘어간다.

vTaskSwitchContext의 일부분

vTaskSwitchContext는 task.c에 구현되어 있다. 여기서는 가장 높은 우선순위를 가진 task를 선택하는 작업을 한다. (taskSELECT_HIGHEST_PRIORITY_TASK())

 

pxCurrentTCB도 갱신된다

taskSELECT_HIGHEST_PRIORITY_TASK() 매크로에서 우선순위가 가장 높은 task를 ready list에서 찾고 그것으로 pxCurrentTCBConst 주소의 값을 갱신하는 작업을 한다.

결국 실행중인 task를 내보낸 것이다.

(pxCurrentTCBConst 주소의 값이 다음에 실행될 task TCB의 주소로 바뀜)

 

예)

pxCurrentTCBConst = 0x1000, 0x1000에는 0x2000이라는 값이 저장되어 있었다.

다음에 실행될 TCB struct가 0x3000에 있다면 0x1000주소의 값은 0x3000이라는 값으로 변경됨

r3는 아직 pxCurrentTCBConst의 값인 0x1000값을 가지고 있음

 

여기까지가 task switch out 과정이다.

 

- Task switching in precedure

실행중인 task를 내보냈다면 다음에 실행할 task를 올려야 한다.

 

이전단계에서 pxCurrentTCBConst 주소에 있는 값이 다음에 실행될 task TCB의 주소로 바뀌었고, r3는 계속해서 pxCurrentTCBConst 값을 저장하고 있다는 것을 기억하면서 코드를 계속해서 보자

 

ldr r1, [r3]
ldr r0, [r1]

r1에 r3의 주소에 있는 값을 저장한다. 그리고 r1의 주소에 있는 값을 r0에 저장한다.

 

예)

pxCurrentTCB의 주소에 있는 값을 변경한 후의 상태는 pxCurrentTCBConst = 0x1000, 0x1000에는 0x3000이라는 값이 저장되어 있다.

r3는 아직 pxCurrentTCBConst의 값인 0x1000값을 가지고 있음

따라서 r1 = 0x3000

0x3000에는 다음에 시작될 task의 TCB struct가 저장되어 있음

따라서 r1주소에 있는 값을 r0로 load하면 TCB struct의 첫번째 변수 pxTopOfStack가 r0에 저장된다.

이 값은 새로운 task의 stack top을 가리키고, 이 값이 r0에 저장됨

(r0 = 새로운 task의 private stack top을 가리킴)

 

ldmia r0!, {r4-r11, r14}

그리고 stack에 있던 register값들을 pop하고 원래 register에 저장한다. (지금 들어온 작업이 이전에 하던 작업을 계속할 수 있게)

r0가 새로운 task의 private stack top을 가리키므로, 해당 위치에서부터 값들을 pop해서 원래 register에 저장한다.

 

지금 이 과정과 앞으로 과정은 새로 시작될 task의 개인 stack에서 이루어진다.

이 task도 실행하던 도중에 중단되어, 개인 stack에 이전의 context가 저장되어 있을 수 있기 때문에 개인 stack에서 pop하는 과정(resume) 필요

 

msr psp, r0

그리고 stack이 변경되었기 때문에 psp값을 r0로 갱신한다.

 

bx r14

마지막으로, 이 과정이 다 끝났으면 context switching 과정을 중단하고 나간다.

r14는 LR(Link Register)로, 함수 호출 전에 이 reigster에 복귀 주소를 저장하고 실행한다.

따라서 r14로 점프하면 함수 호출 전에 여기에 저장해두었던 복귀 주소로 이동한다.

당연히 여기서 함수는 PendSV handler이다.

 

3. 그림으로 보기

Context switching 과정

대략적인 과정은 위와 같다.

Task1이 실행중이다가 Systick timer interrupt가 발생하면 SysTick Handler가 이를 처리하고, PenSV Handler를 호출한다.

PenSV handler에서 context switching이 완료되면 task2가 실행된다.

 

A 구간의 상태를 보자

A 구간의 상태

일부 register(PSR< PC, LR, R0-R3, R12)는 processor가 Systick interrupt가 발생하면 자동으로 Task1의 개인 stack에 저장한다. Stack의 내용이 변경되었기 때문에 stack pointer인 PSP가 갱신된다.

 

다음으로 PendSV Handler 수행 단계를 보자

나머지 register(r4 - r11, r14)도 stack에 쌓였다. 당연히 PSP값도 자동으로 갱신된다.

그리고 바뀐 PSP값을 Task1의 TCB struct의 pxTopOfStack 변수에 저장한다.

아직까진 pxCurrentTCB는 Task1의 TCB를 가리키고 있다.

여기까지가 실행중인 Task1 context를 자신의 stack, TCB에 저장하는 과정이다.

 

다음으론 다음 task인 Task2가 들어오는 과정을 보자

위 그림과 똑같다. 단지 Task2로만 바뀐 그림이다.

pxCurrentTCB가 Task2의 TCB를 가리키게 되었고, Task2가 중단되기 전에 저장해둔 상태(private stack)를 볼 수 있다.

Task2를 resume하기 위해 private stack에 있던 register 값들을 원래 register에 복귀시킨 결과이다.

여기까지가 PendSV handler가 하는 역할이다.

 

Stack에 남아있는 것들은 processor가 자동으로 pop한다.

이 것이 구간 B 에서 하는 일이다.