컨텍스트 스위칭
- 우리는 태스크의 컨텍스트를 태스크의 스택에 저장하고있다
- 따라서 컨텍스트 스위칭을 다음 과정으로 진행된다
- 현재 동작하고 있는 태스크의 컨텍스트를 스택에 백업
- 다음 동작할 태스크의 컨텍스트 블록을 스케줄러에서 받음
- 2에서 가져온 태스크 컨텍스트 블록에서 스택 포인터를 읽음
- 3에서 읽은 태스크의 스택에서 컨텍스트를 읽어서 arm 코어에 복구
- 다음 동작할 태스크의 직전 프로그램 실행 위치로 이동
- 이를 코드로 작성하면 아래와 같다
static KernelTcb_t* sCurrent_tcb;
static KernelTcb_t* sNext_tcb;
void Kernel_task_scheduler(void)
{
sCurrent_tcb = &sTask_list[sCurrent_tcb_index];
sNext_tcb = Scheduler_round_robin_algorithm();
Kernel_task_context_switching();
}
- 컨텍스트 스위칭 코드는 우선 아래와 같이 껍데기만 작성하자
__attribute__((naked)) void Kernel_task_context_switching(void)
{
__asm__("B Save_context");
__asm__("B Restore_context");
}
__attribute__ ((naked))라는 atrtribute를 추가했다
- 컴파일러가 함수를 컴파일할 때 자동으로 만드는 스택 백업, 복구, 리턴 관련 어셈블리인 prologue/epilogue 코드를 생성하지 않는 gcc 컴파일러 어트리뷰터 기능이다
- 관련 어셈블리 없이 내부에 코딩한 코드 자체만 그대로 남는다
- 현재는 Save_context, Restore_context가 없어 빌드오류가 난다, 이부분은 나중에 어셈블리로 작성할 것이다
컨텍스트 백업
- 컨텍스트는 현재 동작중인 태스크의 스택에 직접 백업할 것이다
typedef struct KernelTaskContext_t
{
uint32_t spsr;
uint32_t r0_r12[13];
uint32_t pc;
} KernelTaskContext_t;
- C언어에서 구조체의 멤버변수는 메모리 주소 작은 값부터 큰 값 순서로 배정된다
- 스택은 메모리가 큰 값에서 작은 값으로 진행한다
- 따라서, 스택에는
pc, r0_r12, spsr 순서로 저장해야 의도한대로 메모리 주소에 값이 저장된다
static __attribute__((naked)) void Save_context(void)
{
// save current task context into current task stack
__asm__("PUSH {lr}");
__asm__("PUSH {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12}");
__asm__("MRS r0, cpsr");
__asm__("PUSH {r0}");
// save current save stack pointer into current task context block
__asm__("LDR r0, =sCurrent_tcb");
__asm__("LDR r0, [r0]");
__asm__("STMIA r0!, {sp}");
}
- 이 순서로 Save_context 함수를 구현해보면 위와 같다
- 처음 나중에 태스크가 다시 실행했을 때 복귀할 주소인 link register, 즉
lr 레지스터를 pc 멤버변수에 저장한다
- 범용 레지스터 r0~r12를 저장하고
- cpsr을 spsr에 저장하는데, 프로그램 상태 레지스터는 바로 메모리에 저장할 수 없으므로 r0를 사용한다
- r0는 이미 위에서 백업했으니 덮어써도 상관 없다
- 현재 동작중인 task context block의 포인터 변수를 읽어 r0에 저장하고
- r0 주소에 담긴 값을 읽는다
- task context block의 첫번째 멤버변수인 sp의 값을 읽게 된다
- sCurrent_tcb->sp의 위치에 arm 코어의 sp 레지스터 값을 저장한다
- 여기서의 간단한 동작을 위해
KernelTcb_t의 첫번째 멤버변수를 sp로 지정하였다
컨텍스트 복구
static __attribute__((naked)) void Restore_context(void)
{
// save current save stack pointer into current task context block
__asm__("LDR r0, =sNext_tcb");
__asm__("LDR r0, [r0]");
__asm__("LDMIA r0!, {sp}");
// save current task context into current task stack
__asm__("POP {r0}");
__asm__("MRS cpsr, r0");
__asm__("POP {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12}");
__asm__("POP {pc}");
}
- 컨텍스트를 복구시킬 Restore_context 함수는 Save_context의 역순이다
- sNext_tcb에서 스택 포인터 값을 읽어온다
- 스택에 저장되어 있는 cpsr의 값을 꺼내 arm 코어의 cpsr 레지스터에 값을 쓴다
- r0~r12까지의 범용 레지스터도 복구한다
- 이후로는 r0~r12 값을 변경하면 context가 깨진다
- 스택에 마지막으로 남은 lr값을 꺼내 pc에 저장해 태스크 코드로 점프시킨다
yield 만들기
- 선점형/비선점형 멀티태스킹
- 태스크가 스케줄링을 요청하지 않았는데, 커널이 강제로 스케줄링 하는 경우
선점형 멀티태스킹 시스템이라고 한다
- 반대로, 태스크가 스케줄링을 요청해야 스케줄링 하는 시스템을
비선점형 멀티태스킹 시스템이라고 한다
- 시분할 시스템
- 정기적으로 발생하는 타이머 인터럽트에 연동해 태스크를 전환하도록 스케줄링을 구성
- 일반적으로 시분할 시스템은 선점형 멀티태스킹 시스템이다
- yield 함수
- 우리는 시분할이 아닌 시스템에 비선점형 스케줄링을 사용할 것이다
- 즉, 태스크가 명시적으로 커널에 스케줄링을 요청해야 한다
- 이러한 동작을 하는 함수의 이름을 일반적으로 yield를 많이 쓴다
- yield를 커널 API로 만들어 외부에서 사용해보자
# kernel/Kernel.c
void Kernel_yield(void)
{
Kernel_task_scheduler();
}
- 함수는 매우 간단하게 호출되면 바로 스케줄러, 컨텍스트 스위칭을 호출 할 것이다
커널 시작하기
- 커널을 시작하기 위해서는 스케줄러를 실행시키면 된다
- 그러나 우리의 스케줄러는 현재 동작 중인 태스크가 있음을 가정하고 있고, 현재는 커널이 시작하는 시점에 동작하는 태스크가 없다
- 최초에 동작하는 태스크는 컨텍스트 백업은 생략하고 컨텍스트 복구만 시키면 된다
- 최초 스케줄링을 처리하는 코드를 작성해보자
# kernel/task.c
void Kernel_task_init(void)
{
sAllocated_tcb_index = 0;
sCurrent_tcb_index = 0;
for (uint32_t i = 0; i < MAX_TASK_NUM; i++)
{
sTask_list[i].stack_base = (uint8_t*)(TASK_STACK_START + i * USR_TASK_STACK_SIZE);
sTask_list[i].sp = (uint32_t)(sTask_list[i].stack_base) + USR_TASK_STACK_SIZE - 4;
sTask_list[i].sp -= sizeof(KernelTaskContext_t);
KernelTaskContext_t* ctx = (KernelTaskContext_t*)sTask_list[i].sp;
ctx->pc = 0;
ctx->spsr = ARM_MODE_BIT_SYS;
}
}
void Kernel_task_start(void)
{
sNext_tcb = &sTask_list[sCurrent_tcb_index];
Restore_context();
}
- init 부분에서 current tcb index를 0으로 설정해주었다
- 커널 시작을 위해 최초 한 번만 호출하는 Kernel_task_start 함수를 추가했다
- 0으로 설정될 sCurrent_tcb_index번째 tcb를 context 복구시켜 arm 코어에 쓴다
- 커널 초기화 함수를 모아둘 Kernel_start 함수를 만들고 태스크 시작 함수를 넣자
# kernel/Kernel.c
void Kernel_start(void)
{
Kernel_task_start();
}
- 이후 Kernel_start는 Main의 Kernel_init 함수에서 호출시키자
# boot/Main.c
void dummy_task(void)
{
debug_printf("dummy task\n");
while (true);
}
static void Kernel_init(void)
{
uint32_t taskId;
Kernel_task_init();
taskId = Kernel_task_create(dummy_task);
if (taskId == NOT_ENOUGH_TASK_NUM)
{
debug_printf("dummy task creation failed\n");
}
Kernel_start();
}
- 스케줄링을 위한 준비가 다 되었으니, task를 만들어서 실제로 동작하는지 확인해보자
void task_1(void)
{
debug_printf("task_1\n");
uint32_t local = 0;
while (true)
{
debug_printf("task_1 SP=0x%x\n", &local);
Kernel_yield();
}
}
void task_2(void)
{
debug_printf("task_2\n");
uint32_t local = 0;
while (true)
{
debug_printf("task_2 SP=0x%x\n", &local);
Kernel_yield();
}
}
static void Kernel_init(void)
{
uint32_t taskId;
Kernel_task_init();
if (Kernel_task_create(task_1) == NOT_ENOUGH_TASK_NUM)
{
debug_printf("dummy task creation failed\n");
}
if (Kernel_task_create(task_2) == NOT_ENOUGH_TASK_NUM)
{
debug_printf("dummy task creation failed\n");
}
Kernel_start();
}
- 스케줄링이 정상 동작해서 task_1의 SP는 0x8FFFF0, task_2의 SP는 0x9FFFF0으로 반복적으로 나오는 것을 확인할 수 있다

- 이 스택 포인터 주소가 제대로 할당된 것인지 확인해보자
# include/MemoryMap.h
...
#define TASK_STACK_START 0x00800000 // 태스크 스택 영역
...
- task stack start 값이 0x00800000이다
- 스택은 큰 주소에서 작은 주소로 진행하므로, 스택 포인터에는 스택 공간의 가장 큰 주소를 할당한다
- 우리는 각 태스크 스택 사이에 4byte의 간격을 패딩으로 두었고, 컴파일러가 사용하는 몇개 스택 이후 로컬 변수가 잡히므로 주소는 0x8FFFF0으로 출력된 것으로 볼 수 있다
- task1, task2 모두 동일한 방식으로 SP에 값이 들어간 것을 알 수 있다