본문 바로가기

Firmware/RTOS

임베디드 OS 개발 프로젝트 11(Context Switching - 1)

.
├── boot
│   ├── Entry.S
│   ├── Handler.c
│   └── Main.c
├── hal
│   ├── HalInterrupt.h
│   ├── HalTimer.h
│   ├── HalUart.h
│   └── rvpb
│       ├── Interrupt.c
│       ├── Interrupt.h
│       ├── Regs.c
│       ├── Timer.c
│       ├── Timer.h
│       ├── Uart.c
│       └── Uart.h
├── include
│   ├── ARMv7AR.h
│   ├── memio.h
│   ├── MemoryMap.h
│   ├── stdarg.h
│   └── stdint.h
├── kernel
│   ├── task.c
│   └── task.h
├── lib
│   ├── armcpu.c
│   ├── armcpu.h
│   ├── stdio.c
│   ├── stdio.h
│   ├── stdlib.c
│   └── stdlib.h
├── Makefile
└── navilos.ld

 

컨텍스트 스위칭이란 말 그대로 테스크를 전환한다는 뜻이다.

 

테스크란 동장하는 프로그램이고, 동작하는 프로그램의 정보는 컨텍스트이다. 이 컨텍스트를 어딘가에 저장하고 또 다른 어딘가에서 컨텍스트를 가져다가 프로세서 코어에 복구하면 다른 프로그램이 동작하게 된다 (책 10장)

 

앞에서 Navilos의 Task의 Context는 Task의 Stack에 저장하겠다고 결정했다. 

그러므로 Navilos의 Context의 Switching은 다음 과정으로 진행 된다.

 

1. 현재 동작하고 있는 Task의 Context를 현재 Stack에 백업한다.

2. 다음에 동작할 Task Control Block을 scheduler에서 받는다.

3. 2에서 받은 Task Control Block 에서 Stack Pointer를 읽는다.

4. 3에서 읽은 Task의 Stack에서 Context를 읽어서 ARM 코어에 복구한다.

5. 다음 동작할 Task의 직전 프로그램 실행 위치로 이동한다.

 

이전에 만들었던 스케줄러 알고리즘을 실행하기 위해 다음과 같은 함수를 만든다.

 

Kernel_task_scheduler

void Kernel_task_scheduler(void)
{
    sCurrent_tcb = &sTask_list[sCurrent_tcb_index];
    sNext_tcb = Scheduler_round_robin_algorithm();

    Kernel_task_context_switching();
}

 현재 실행중인 Task를 sCurrent_tcb에 저장한다.

 

 저장된 sCurrent_tcb와 sCurrent_tcb_index를 가지고 라운드 로빈 알고리즘을 실행한다.

 

라운드 알고리즘을 통해 다음에 실행할 tcb를 구한다.(sNext_tcb)

 

그다음 Kernel_task_context_switching 함수를 통해 Task를 Switching 한다.

 

 

Kernel_task_context_switching

__attribute__ ((naked)) void Kernel_task_context_switching(void)
{
    __asm__ ("B Save_context");
    __asm__ ("B Restore_context");

}

 

함수 앞에보면 컴파일 지시어 __attribute__ ((naked))가 붙은게 보인다.

 

C언어는 기본적으로 함수 호출을 할 때 Stack을 확보및 복귀할 수 있게 LR을 업데이트하는 Assembly Code가 들어가게 된다.

 

(gdb) disas /s Kernel_task_context_switching
Dump of assembler code for function Kernel_task_context_switching:
kernel/task.c:
88	{
   0x0000140c <+0>:	push	{r11}		; (str r11, [sp, #-4]!)
   0x00001410 <+4>:	add	r11, sp, #0

89	    __asm__ ("B Save_context");
   0x00001414 <+8>:	b	0x142c <Save_context>

90	    __asm__ ("B Restore_context");
   0x00001418 <+12>:	b	0x144c <Restore_context>

91	
92	}
   0x0000141c <+16>:	nop	{0}
   0x00001420 <+20>:	add	sp, r11, #0
   0x00001424 <+24>:	pop	{r11}		; (ldr r11, [sp], #4)
   0x00001428 <+28>:	bx	lr

 

 

이게 문제가 되는게 Save_context는 현재 실행하고 있는 Task의 Context를 저장한다.

그런데 Task Context 정보가 Task_context_switching 호출로 인해 리턴주소와 sp 값이 바뀐상태로 저장되기 때문에 사용중인 Task Context 가 저장 되는게 아닌 Kernel_task_context_switching 의 레지스터가 섞여 저장되게 된다.

 

당연히 context를 Restore 할 때 문제가 된다.

 

함수 앞에 __attribute__ ((naked)) 가 붙으면 Stack이 그대로 유지된다.

 

(gdb) disas /s Kernel_task_context_switching
Dump of assembler code for function Kernel_task_context_switching:
kernel/task.c:
89	{
90	    __asm__ ("B Save_context");
   0x0000140c <+0>:	b	0x1418 <Save_context>

91	    __asm__ ("B Restore_context");
   0x00001410 <+4>:	b	0x1438 <Restore_context>

92	
93	}

 

 

Save_context 

static __attribute__ ((naked)) void Save_context(void)
{
    // save current task context into the current task stack
    __asm__ ("PUSH {lr}");  //__attribute__ 가 없으면 lr 이 현재 Task의 lr 이 아닌 Save_context의 lr이 들어가게 된다.
    __asm__ ("PUSH {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12}");
    __asm__ ("MRS   r0, cpsr");  //current program status register
    __asm__ ("PUSH {r0}");
    // save current task stack pointer into the current TCB
    __asm__ ("LDR   r0, =sCurrent_tcb");  //r0에 sCurrent_tcb의 값을 복사한다.
    __asm__ ("LDR   r0, [r0]");    //r0에 sCurrent_tcb의 Stack pointer 값을 복사한다.
    __asm__ ("STMIA r0, {sp}");    //sCurrent_tcb의 stack ponter 값을 갱신한다.
}

 

현재 사용중인 Task의 lr, r1 - r9 레지스터를 스택에 저장한다.(현재 tcb stack값은 레지스터 저장 전 메모리를 나타낸다)

 

CPSR은 특수 레지스터이므로 R0 레지스터에 쓴 후 Stack에 저장한다. (MRS  r0, cpsr)

 

R0 값을 다시 STACK에 push 하여 CPSR 값을 저장한다.

 

현재 실행하고 있는 Task의 sp의 값을 저장하기 위해 R0 에 sCurrent_tcb 를 복사한다. ( LDR r0, =sCurrent_tcb)

 

sCurrent_tcb는 포인터 변수이므로 []을 사용하여 주소가 나타내는 값을 가지고 온다 (LDR r0, [r0])

 

sCurrent_tcb 구조체의 첫번째 멤버는 sp 이다.  즉 sCurrent_tcb의 포인터 변수가 나타내는 값은 sp 이다.

 

현재 실행하고 있는 Task의 sp 값을 자기 자신의 Tcb sp에  update 시킨 후 r0 값을 증가시킨다.

(굳이 왜?? 나중에 r0!에서 !제거 후 Test 및 디버깅 진행 필요)

->디버깅 결과: 굳이 !로 update 할 필요가 없다.  바로 Restore context에서 r0 값이 load되기 때문이다. 

 

현재 sCurrent_tcb의 sp 값은 범용레지스터 저장 전 메모리를 나타내고 있다.

__asm__ ("LDR   r0, [r0]");   

 

나중에 Restore를 위해 sCurrent_tcb의 sp 값을  범용레지스터 저장 후 stack pointer로 업데이트 해준다.

__asm__ ("STMIA r0, {sp}"); 

 

Restore_context

static __attribute__ ((naked)) void Restore_context(void)
{
    // restore next task stack pointer from the next TCB
    __asm__ ("LDR   r0, =sNext_tcb"); // 다음에 실행할 Task의 tcb를 r0에 load한다.
    __asm__ ("LDR   r0, [r0]");  //r0의 stack pointer를 load한다.
    __asm__ ("LDMIA r0!, {sp}"); //불러 온 stack pointer를 범용 sp에 load 한다.
    // restore next task context from the next task stack
    __asm__ ("POP  {r0}");  // sNext_tcb에 저장 된 r0(cpsr)을 복구 한다.
    __asm__ ("MSR   cpsr, r0"); // cpsr을 복구 한다.
    __asm__ ("POP  {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12}"); //r0 ~ r12 까지 register를 복구한다.
    __asm__ ("POP  {pc}");  //pc register를 복구한다.
}

Stack은 FILO 이므로 저장했던 순서와 반대로 불러와 복구 한다.

 

 

 

 

-----------------------------------------------------------------------------------------------------------------------------------

STMIA, LDMIA Assembly 명령어.

 

LDMIA 와 STMIA는 서로 반대이다. 

 

여기서는 LDMIA만 언급한다.

 

위 그림을 보고 포인터 개념으로 접근하면 이해하기가 쉽다.

 

LDMIA R1, {R0,R2,R4}

 

 

R1이 나타내는 주소를 BASE로 하여 배열처럼 INDEX를 증가하여 R0,R2,R4를 가지고 온다.

 

그런데 자세히 보면 Save_context함수의  base addr register에 !가 붙은걸 알 수 있다.

 

!가 붙으면 R1이 붙으면 Load 한 레지스터 만큼 Base Register가 update 된다.

box 안에 R1 :=R1 + 32가 붙으며 만약 !가 없으면 +32로 update가 되지 않는다.

-----------------------------------------------------------------------------------------------------------------------------------