Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

230224
1178
109352


[리눅스커널][스케줄링] 컨택스트 스위칭: context_switch() 세부 코드 분석 10. Process Scheduling

컨택스트 스위칭 세부 코드 분석
이전 소절에서 다룬 컨택스트 스위칭이란 다음과 같은 문장으로 정리할 수 있습니다.
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,
       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에 부하를 주지 않는 테스트

 

덧글

댓글 입력 영역