컨택스트 스위칭 세부 코드 분석
이전 소절에서 다룬 컨택스트 스위칭이란 다음과 같은 문장으로 정리할 수 있습니다.
CPU에서 실행 중인 프로세스 정보로 채워진 CPU 레지스터 세트를 프로세스 스택 공간에 저장하고 다음에 실행할 프로세스의 레지스터 세트를 스택 공간에서 로딩해 CPU 레지스터 세트에 채우는 동작입니다.
이번에는 context_switch() 함수 분석으로 컨택스트 스위칭 코드 동작을 알아보겠습니다. 먼저 context_switch() 함수에 전달하는 인자를 확인합시다.
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/sched/core.c]
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next, struct rq_flags *rf)
2~3번째 인자인 struct task_struct *prev와 struct task_struct *next를 살펴보겠습니다.
struct task_struct *prev
- 현재 실행 중인 프로세스
- 스케줄링으로 CPU를 비우고 휴면에 진입할 프로세스
struct task_struct *next
- 다음에 실행할 프로세스
- 스케줄러가 다음에 실행할 프로세스로 선택
context_switch() 함수부터 분석할 함수는 다음과 같습니다.
- context_switch()
- switch_to(prev, next, prev);
- __switch_to(prev,task_thread_info(prev), task_thread_info(next));
실제 컨택스트 스위칭을 실행하는 코드는 __switch_to 레이블에서 확인할 수 있습니다.
__switch_to 레이블은 어셈블리 코드로 구현돼 있습니다.
컨택스트 스위칭은 아키텍처에 의존적인 동작입니다. ARM64/x86 아키텍처 별로 프로세스 별로 실행된 레지스터를 로딩하거나 저장하는 동작이 다릅니다.
context_switch() 함수 분석을 시작하기 전에 인자와 반환값을 알아보겠습니다.
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next, struct rq_flags *rf)
struct rq* 주소를 반환합니다.
각각 인자의 의미는 다음과 같습니다.
- struct rq *rq: 런큐 주소
- struct task_struct *prev: 휴면될 프로세스 태스트 디스크립터 주소
- struct task_struct *next: 다음 프로세스로 실행할 태스크 디스크립터 주소
- struct rq_flags *rf: 런큐 플래그
이어서 컨택스트 스위칭에 실행 흐름에 초점을 맞춰 코드 분석을 하겠습니다.
다음 context_switch() 함수 코드를 봅시다.
[https://elixir.bootlin.com/linux/v4.14.49/source/kernel/sched/core.c#L2750]
1 static __always_inline struct rq *
2 context_switch(struct rq *rq, struct task_struct *prev,
3 struct task_struct *next, struct rq_flags *rf)
4 {
...
5 /* Here we just switch the register state and the stack. */
6 switch_to(prev, next, prev);
7 barrier();
8
9 return finish_task_switch(prev);
10 }
context_switch() 함수에서 switch_to() 함수 호출 이전에 메모리 디스크립터는 백업하고 로딩하는 코드 분석은 생략합니다.
위 코드 6번째 줄 코드를 보겠습니다.
switch_to() 함수를 호출해서 새로운 프로세스로 실행 시작 준비를 합니다.
switch_to() 함수 구현부를 보면 다음 코드로 치환됩니다.
[https://elixir.bootlin.com/linux/v4.14.49/source/arch/arm/include/asm/switch_to.h]
1 extern struct task_struct *__switch_to(struct task_struct *, struct thread_info *, struct thread_info *);
3
4 #define switch_to(prev,next,last) \
5 do { \
6 __complete_pending_tlbi(); \
7 last = __switch_to(prev,task_thread_info(prev), task_thread_info(next)); \
8 } while (0)
7 번째 줄 코드를 보면 __switch_to() 함수를 호출합니다.
여기서 __switch_to() 함수에 전달하는 인자를 정리합시다.
- prev: 휴면에 진입할 프로세스 태스크 디스크립터 주소
- task_thread_info(prev): 휴면에 진입할 프로세스 current_thread_info() 주소
- task_thread_info(next): 다음 실행할 프로세스 current_thread_info() 주소
__switch_to() 코드를 검색하니 구현부는 어셈블리 코드입니다.
[https://elixir.bootlin.com/linux/v4.14.49/source/arch/arm/kernel/entry-armv.S]
/*
* Register switch for ARMv3 and ARMv4 processors
* r0 = previous task_struct, r1 = previous thread_info, r2 = next thread_info
* previous and next are guaranteed not to be the same.
*/
ENTRY(__switch_to)
UNWIND(.fnstart )
UNWIND(.cantunwind )
add ip, r1, #TI_CPU_SAVE
ARM( stmia ip!, {r4 - sl, fp, sp, lr} ) @ Store most regs on stack
THUMB( stmia ip!, {r4 - sl, fp} ) @ Store most regs on stack
THUMB( str sp, [ip], #4 )
THUMB( str lr, [ip], #4 )
ldr r4, [r2, #TI_TP_VALUE]
ldr r5, [r2, #TI_TP_VALUE + 4]
#ifdef CONFIG_CPU_USE_DOMAINS
mrc p15, 0, r6, c3, c0, 0 @ Get domain register
str r6, [r1, #TI_CPU_DOMAIN] @ Save old domain register
ldr r6, [r2, #TI_CPU_DOMAIN]
#endif
코드를 보기 어려우니 바이너리 유틸리티로 어셈블리 코드를 봅시다.
1 80707db8 <__switch_to>:
2 80707db8: e281c018 add ip, r1, #24
3 80707dbc: e8ac6ff0 stmia ip!, {r4, r5, r6, r7, r8, r9, sl, fp, sp, lr}
4 80707dc0: e592405c ldr r4, [r2, #92] ; 0x5c
5 80707dc4: e5925060 ldr r5, [r2, #96] ; 0x60
6 80707dc8: ee1d7f50 mrc 15, 0, r7, cr13, cr0, {2}
7 80707dcc: ee0d4f70 mcr 15, 0, r4, cr13, cr0, {3}
8 80707dd0: ee0d5f50 mcr 15, 0, r5, cr13, cr0, {2}
9 80707dd4: e5817060 str r7, [r1, #96] ; 0x60
10 80707dd8: e1a05000 mov r5, r0
11 80707ddc: e2824018 add r4, r2, #24
12 80707de0: e59f000c ldr r0, [pc, #12] ; 80707df4 <__switch_to+0x3c>
13 80707de4: e3a01002 mov r1, #2
14 80707de8: ebe8d560 bl 8013d370 <atomic_notifier_call_chain>
15 80707dec: e1a00005 mov r0, r5
16 80707df0: e894aff0 ldm r4, {r4, r5, r6, r7, r8, r9, sl, fp, sp, pc}
17 80707df4: 80c77088 sbchi r7, r7, r8, lsl #1
18 80707df8: e320f000 nop {0}
19 80707dfc: e320f000 nop {0}
어셈블리 코드를 보기 앞서 인자를 점검합시다.
- r0 = prev: 휴면에 진입할 프로세스 태스크 디스크립터 주소
- r1 = task_thread_info(prev): 휴면에 진입할 프로세스 current_thread_info() 주소
- r2 = task_thread_info(next): 다음 실행할 프로세스 current_thread_info() 주소
ARM 프로세서는 함수 인자를 r0~r5 레지스터를 통해 전달합니다.
__switch_to 레이블 코드는 2단계로 나눌 수 있습니다.
- 1단계: 휴면에 진입할 프로세스 레지스터 세트를 cpu_context 필드에 백업
- 2단계: 다음 실행할 프로세스 current_thread_info()->cpu_context 필드 값을 ARM 레지스터로 로딩
먼저 1단계 코드를 분석하겠습니다.
2 번째 줄 코드입니다.
2 80707db8: e281c018 add ip, r1, #24
current_thread_info() 주소에서 24만큼 더합니다. struct thread_info 구조체에서 cpu_context 필드 오프셋이 24바이트이기 때문입니다.
3 번째 줄 코드를 분석하겠습니다.
3 80707dbc stmia ip!, {r4, r5, r6, r7, r8, r9, sl, fp, sp, lr}
struct thread_info 구조체에서 cpu_context 필드에 r4, r5, r6, r7, r8, r9, sl, fp, sp, lr 레지스터 세트를 저장합니다.
이 동작은 다음 그림에서 검은색으로 된 부분입니다.

다음 3 번째 줄 코드를 실행하면 스택 최상단 주소 struct thread_info 구조체 cpu_context 필드에 접근해 레지스터 세트를 저장합니다.
3 80707dbc stmia ip!, {r4, r5, r6, r7, r8, r9, sl, fp, sp, lr}
다음 2단계 코드 분석에 들어갑니다.
코드 분석 전 r2 레지스터에 다음 실행할 프로세스 current_thread_info() 주소가 저장됐다는 사실을 떠올립시다.
11 번째 줄 코드를 봅시다.
11 80707ddc: e2824018 add r4, r2, #24
r2에서 24만큼 더한 결과는 r4에 저장합니다.
current_thread_info() 주소에서 24만큼 더합니다. struct thread_info 구조체에서 cpu_context 필드 오프셋이 24바이트이기 때문입니다.
이 어셈블리 명령어 연산 결과 r4는 current_thread_info()->cpu_context 필드 주소를 저장합니다.
다음 16 번째 줄 코드를 보겠습니다.
16 80707df0 ldm r4, {r4, r5, r6, r7, r8, r9, sl, fp, sp, pc}
r4에 저장된 레지스터 정보를 ARM 레지스터에 로딩합니다. 이 순간부터 기존에 실행했던 프로그램 카운터로 이동해 다시 실행을 재개합니다.

다음 16 번째 줄 코드를 실행하면 next 프로세스 스택 최상단 주소에 있는 struct thread_info 구조체 cpu_context 필드에 저장된 레지스터를 CPU 레지스터에 채웁니다.
16 80707df0 ldm r4, {r4, r5, r6, r7, r8, r9, sl, fp, sp, pc}
"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!"
Thanks,
Austin Kim(austindh.kim@gmail.com)
Reference(프로세스 스케줄링)
스케줄링 소개
프로세스 상태 관리
어떤 함수가 프로세스 상태를 바꿀까?
스케줄러 클래스
런큐
CFS 스케줄러
CFS 관련 세부 함수 분석
선점 스케줄링(Preemptive Scheduling)
프로세스는 어떻게 깨울까?
스케줄링 핵심 schedule() 함수 분석
컨택스트 스위칭
스케줄링 디버깅
스케줄링 프로파일링
CPU에 부하를 주는 테스트
CPU에 부하를 주지 않는 테스트
# Reference: For more information on 'Linux Kernel';
디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 1
디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 2
최근 덧글