본문 바로가기

SW 개발 공부/OS 개발 프로젝트

[나빌로스] 10.컨텍스트 스위칭

 

컨텍스트 스위칭(Context Switching) 과정 정리

컨텍스트 스위칭은 현재 실행 중인 태스크(Task)의 실행 상태(레지스터 값, 스택 포인터 등)를 저장하고, 다음 실행할 태스크의 상태를 복구하여 실행을 이어가는 과정이다.

아래는 Task1 → Task2로 전환되는 과정을 상세히 설명한 것이다.


1. Task1이 실행 중

  • Task1이 실행 중이며, Task1의 스택에는 현재 실행 중이던 함수 호출 정보, 지역 변수 등이 저장되어 있다.
  • Task1의 레지스터(r0~r12, lr, cpsr)는 현재 Task1의 연산 상태를 유지하고 있다.

2. 컨텍스트 저장 (Save_context)

라운드 로빈 스케줄러가 Task1의 실행을 중단하고 Task2로 전환하려고 할 때,
현재 Task1의 컨텍스트를 저장하는 과정이다.

 

1. 현재 실행 중인 태스크(Task1)의 LR(링크 레지스터)과 범용 레지스터(r0~r12)를 스택에 저장 

PUSH {lr}
PUSH {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12}

 

 2. CPSR(프로세서 상태 레지스터)을 저장하여 Task1의 실행 상태를 기억

MRS   r0, cpsr
PUSH {r0}
 

3. Task1의 현재 스택 포인터(sp)를 Task1의 TCB(sCurrent_tcb)에 저장

LDR   r0, =sCurrent_tcb  // 현재 실행 중인 태스크의 TCB 주소 로드
LDR   r0, [r0]           // 현재 TCB 주소를 r0에 로드
STMIA r0!, {sp}          // 현재 태스크의 sp를 저장
 

 

결과: Task1의 모든 실행 상태(레지스터, 스택, CPSR)가 Task1의 스택 및 TCB에 저장됨.

 

추가 질문

 

1) STMIA 명령어란?

 

STMIA (Store Multiple Increment After)는 레지스터의 여러 값을 메모리에 저장하는 명령어다.

  • STMIA Rn!, {Rlist}
    • Rn(베이스 레지스터): 메모리 주소를 가리키는 레지스터
    • Rlist(레지스터 리스트): 메모리에 저장할 레지스터 목록
    • ! (Writeback) 옵션이 있으면, Rn이 자동으로 증가하여 다음 메모리 주소를 가리킴
    • 메모리 저장 후 Rn이 증가 (Increment After)
STMIA r0!, {r1, r2, r3}
  • 예제: 
    • r1, r2, r3의 값을 r0이 가리키는 메모리에 저장
    • 이후 r0가 3 * 4byte = 12byte 증가함

 

 2) LDR r0, =sCurrent_tcb 이후, LDR r0,[r0]를 하는 이유

 

LDR r0, =sCurrent_tcb  수행시, r0에는 sCurrent_tcb의 주소값이 저장되어있다.

따라서 해당 주소값의 []를 처리해줘야 실제 tcb의 주소가 r0에 전달된다.

 

3) __attribute__ ((naked))의 의미

 

 __attribute__ 는 GCC 컴파일러 어트리뷰트 지시어를 사용한다는 의미이다. 이때, ((nake))를 사용하면 자동적으로 stack에 현재 주소를 push 및 pop 하는작업이 제외된다.

((naked))를 선언하지 않는다면 push 및 pop 하는 작업이 수행된다.

 

* naked 선언하지 않은경우 disassembly

push {fp}          ; (1) 프레임 포인터(fp)를 스택에 저장
add  fp, sp, #0    ; (2) 현재 sp 값을 fp에 복사 → 새로운 프레임 설정
b    <Save_context>; (3) Save_context() 호출
b    <Restore_context>; (4) Restore_context() 호출
sub  sp, fp, #0    ; (5) 스택을 원래 위치로 되돌림 (실제로는 효과 없음)
pop  {fp}          ; (6) 저장해둔 fp를 복원
bx   lr            ; (7) 원래 함수 호출 지점으로 복귀

 

✅ naked가 없을 때의 동작

__attribute__((naked))를 사용하지 않으면, **컴파일러가 함수 호출 규약(Calling Convention)**을 자동으로 적용하여 프롤로그(함수 진입)와 에필로그(함수 종료) 코드를 삽입합니다.

1️⃣ 프롤로그 (Function Prologue)

  • push {fp} : 기존 프레임 포인터(fp, 즉 r7 또는 r11 depending on the ABI)를 스택에 저장
  • add fp, sp, #0 : fp를 현재 sp로 설정하여 새로운 스택 프레임 생성

2️⃣ 함수 본문 실행

  • b <Save_context> → 문맥 저장
  • b <Restore_context> → 문맥 복구

3️⃣ 에필로그 (Function Epilogue)

  • sub sp, fp, #0 : (의미 없는 동작, 보통은 mov sp, fp가 와야 함)
  • pop {fp} : 저장했던 fp를 복원
  • bx lr : lr로 복귀 (즉, 원래 호출한 함수로 돌아감)

🔥 __attribute__((naked))가 없을 때의 특징

함수 호출 규약을 자동으로 적용 (스택 프레임 자동 관리)
✅ 함수가 종료될 때 정확한 리턴을 보장
🚨 하지만 인터럽트 핸들러나 컨텍스트 스위칭처럼 직접 스택을 다루는 경우 불필요한 코드가 추가될 수 있음


3. 다음 태스크(Task2) 선택

라운드 로빈 알고리즘을 이용해 다음 실행할 태스크(Task2)를 결정한다.

sCurrent_tcb_index++;
sCurrent_tcb_index %= sAllocated_tcb_index;
sNext_tcb = &sTask_list[sCurrent_tcb_index];

 

결과: Task2가 다음 실행할 태스크로 결정됨.


4. 컨텍스트 복구 (Restore_context)

Task2의 실행을 복원하는 과정이다.

1. Task2의 스택 포인터(sp)를 Task2의 TCB에서 가져와서 현재 sp로 설정

LDR   r0, =sNext_tcb  // 다음 실행할 태스크의 TCB 주소 로드
LDR   r0, [r0]        // 다음 실행할 TCB 주소를 r0에 로드
LDMIA r0!, {sp}       // Task2의 sp를 설정

 

2. Task2의 CPSR 값을 복구하여 Task2가 중단된 시점의 CPU 상태를 원래대로 돌림

POP  {r0}       // 스택에서 CPSR 값 가져오기
MSR  cpsr, r0   // CPSR 설정 (Task2의 실행 상태 복원)

 

3. Task2의 범용 레지스터(r0~r12)와 LR을 복원

POP {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12}

 

4. Task2가 실행 중이던 위치(PC)로 복귀하여 실행을 이어감

POP {pc}  // PC 복구 → Task2의 실행 위치로 복귀
 

결과: Task2의 실행 상태가 복구되어 Task2가 실행됨.

 

추가 질문

 

1) LDMIA(Load Multiple Increment After)

 

LDMIA (Load Multiple Increment After)는 메모리에서 여러 개의 값을 로드하여 레지스터에 저장하는 명령어다.

  • LDMIA Rn!, {Rlist}
    • Rn(베이스 레지스터): 메모리 주소를 가리키는 레지스터
    • Rlist(레지스터 리스트): 메모리에서 로드할 레지스터 목록
    • ! (Writeback) 옵션이 있으면, Rn이 자동으로 증가하여 다음 메모리 주소를 가리킴
    • 메모리에서 로드한 후 Rn이 증가 (Increment After)

 

예제 1: 기본 동작

LDMIA r0! , {r1,r2,r3}

 

동작 과정:

  1. r0가 가리키는 메모리 주소에서 r1을 가져온다.
  2. r0 + 4에서 r2를 가져온다.
  3. r0 + 8에서 r3를 가져온다.
  4. r0 값이 r0 + 12로 증가한다.

5. Task2 실행

  • Task2는 이전에 실행 중이던 위치에서 그대로 실행을 이어간다.
  • Task2의 스택과 레지스터는 이전에 저장된 상태 그대로 복구되어 Task2는 자신의 코드가 계속 실행되고 있다고 인식한다.

6. 이후 스케줄링 (Task2 → Task3)

  • 이후에도 타이머 인터럽트가 발생하면 Task2의 컨텍스트를 저장하고, Task3를 선택하여 실행하는 방식으로 반복된다.
  • 태스크가 라운드 로빈 방식으로 순환하면서 실행된다.

📌 컨텍스트 스위칭 과정 요약

1️⃣ Task1이 실행 중
2️⃣ Save_context()

  • Task1의 레지스터(r0~r12, lr)와 CPSR 값을 스택에 저장
  • Task1의 스택 포인터(sp)를 TCB에 저장
    3️⃣ 다음 실행할 태스크(Task2)를 선택
    4️⃣ Restore_context()
  • Task2의 스택 포인터(sp)를 TCB에서 복원
  • Task2의 CPSR을 복원
  • Task2의 레지스터(r0~r12, lr)와 PC를 복원하여 실행 재개
    5️⃣ Task2가 실행됨

이 과정을 반복하여 여러 태스크가 순환하면서 실행된다.


📌 컨텍스트 스위칭이 필요한 이유

멀티태스킹 지원: 여러 태스크가 실행될 수 있도록 함
CPU 리소스 분배: 특정 태스크가 CPU를 독점하지 않도록 함
인터럽트 처리 가능: 특정 태스크가 중단되더라도 실행을 이어갈 수 있음