Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

80258
1323
114582


[라즈베리파이] 스케줄링: 컨택스트 스위치(Context Switch)이란 10. Process Scheduling

CPU에서 실행 중인 프로세스를 비우고 새로운 프로세스를 CPU에서 실행시키는 과정을 컨택스트 스위칭이라고 합니다.

컨택스트 스위치란
다음 그림을 보면서 컨택스트 스위칭란 용어에 대해서 살펴봅시다.
 

CPU에서 E란 프로세스가 실행 중이라고 가정합시다. 어떤 프로세스가 CPU에서 실행 중이란 의미는 CPU 레지스터 세트에 프로세스 실행 정보가 채워져 있다는 의미입니다. 어떤 코드가 실행 중이란 의미는 ARM 코어 PC(프로그램 카운터)에 현재 실행 중인 주소를 가르키고 있다는 것입니다.

E란 프로세스는 계속 ARM 코어에서 실행을 하려고 하지만 A, B, C, D 프로세스들이 실행 요청을 합니다. 스케줄러는 ARM 코어에서 실행 중인 E란 프로세스와 A, B, C, D 프로세스들과 우선 순위를 비교합니다.

만약 E 프로세스보다 A란 프로세스가 우선 순위가 높으면 스케줄러는 어떻게 할까요?
이 경우 E란 프로세스를 ARM 코어에서 빼낸 후 A란 프로세스를 ARM 코어에 채워줍니다.

조금 더 구체적으로 설명을 드리면 ARM 코어에서 실행 중인 E란 프로세스 실행 정보로 채워져 있는 ARM 코어 레지스터 세트를 비운 다음 A 프로세스 레지스터 세트를 다시 ARM 코어 레지스터에 채워 줍니다.

이 과정을 컨택스트 스위칭이라고 말합니다.

컨택스트 스위치 자료구조란
리눅스 커널에서 컨택스트 스위칭 세부 동작을 알기 위해서 context_switch() 함수를 분석할 필요가 있습니다. context_switch() 함수를 분석하기 전 컨택스트 스위치란 용어의 의미를 조금 더 생각해 봅시다.

스케줄링 동작의 핵심인 컨택스트 스위칭 세부 동작을 알아봅시다.

컨택스트 스위치란 용어는 컨택스트 + 스위치란 단어가 합쳐져 있습니다.
컨택스트를 바꾼다는 의미입니다. 여기서 컨택스란 용어는 무슨 뜻일까요?  

컨택스트는 프로세스가 실행 중인 그 자체를 의미합니다. 이 표현은 추상적이라 이해하기 어려울 수도 있습니다. 프로세스 실행 그 자체를 어떻게 표현할까요?  그것은 레지스터 세트입니다. CPU 레지스터 세트가 프로세스 실행 자체를 표현하기 때문입니다.

어떤 프로세스가 CPU에서 실행을 하면 CPU 레지스터 세트에 프로세스가 실행 중인 코드와 함수 정보가 채워지게 됩니다.

다음 정보는 ARM 코어에서 실행 중인 프로세스의 레지스터 세트입니다.
pc : [<80f65224>]   lr : [<80f65bcc>]      psr: 0x80f655ac
sp : 0x9b7dfce8   ip : 0x9b7dfd74   fp : 0x9b7dfd64
r10: 0x0   r9 : 0x9f89ea00   r8 : 0x9db8b200
r7 : 0x81709294   r6 : 0x9eb07000   r5 : 0x828ea000   r4 : 0xa6b46780
r3 : 0x00000000   r2 : 0x036db918   r1 : 0x00000089   r0 : 0x72fbe0b8

ARM 코어 레지스터 세트에는 ARM 코어에서 실행 중인 프로세스의 코드 정보로 가득차 있습니다.
그럼 위 레지스터 세트는 어떤 코드를 실행 일 때 정보일까요?

위 레지스터 세트를 채우면서 실행 중인 주인공은 라즈베리파이에서 볼 수 있는 "lxpanel" 프로세스이며 다음 콜스택으로 실행 중이었습니다.
1 crash> bt 718
2 PID: 718   TASK: 9eb07000  CPU: 2   COMMAND: "lxpanel"
3 #0 [<80f65224>] (__schedule) from [<80f65bcc>]
4 #1 [<80f65b80>] (schedule) from [<801e059c>]
5 #2 [<801e0480>] (futex_wait_queue_me) from [<801e11b8>]
6 #3 [<801e10a0>] (futex_wait) from [<801e2e14>]
7 #4 [<801e2cf4>] (do_futex) from [<801e3a58>]
8 #5 [<801e3938>] (sys_futex) from [<80108f20>]

위 콜스택은 리눅스 코어 덤프를 로딩해서 프로세스 정보를 디버깅할 수 있는 크래시 유틸리티 프로그램에서 확인한 내용입니다. "bt [pid]" 명령어를 입력하면 프로세스의 콜스택을 확인할 수 있습니다.

레지스터 세트에서 프로그램 카운터는 80f65224이며 __schedule() 함수 내 주소를 의미합니다.

ARM 코어 프로그램 카운터 레지스터에 어떤 함수 주소가 저장돼 있다는 것을 무엇을 의미할까요?
이는 현재 ARM 코어에서 해당 주소에 있는 어셈블리 코드를 실행 중이라고 봐야 합니다. 
프로그램 카운터에 코드 주소를 지정하면 ARM ALU에서 어셈블리 코드에 대한 사칙연산을 수행합니다.

컨택스트란 CPU에서 실행 중인 프로세스의 레지스터 세트를 의미합니다. 

컨택스트란 실행 그 자체이면 실행 중인 프로세스 정보가 채워진 CPU 레지스터 세트라고 설명을 드렸습니다. 이 내용을 조금 더 확장하면, 컨택스트 스위칭이란 CPU 레지스터 세트를 바꾼다고 볼 수 있습니다.

컨택스트 스위칭 자료구조는 무엇일까?
그러면 컨택스트 스위칭으로 CPU 레지스터 세트를 어딘가에 저장할 것입니다.
이 과정에서 어떤 자료구조가 필요한 것입니다. 그러면 컨택스트, 즉 CPU 레지스터 세트를 표현한 자료구조는 무엇일까요?

다음 8 번째 줄 코드와 같이 struct thread_info 구조체에서 cpu_context 필드를 볼 수 있습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/include/asm/thread_info.h]
1 struct thread_info {
2 unsigned long flags; /* low level flags */
3 int preempt_count; /* 0 => preemptable, <0 => bug */
4 mm_segment_t addr_limit; /* address limit */
5 struct task_struct *task; /* main task structure */
6 __u32 cpu; /* cpu */
7 __u32 cpu_domain; /* cpu domain */
8 struct cpu_context_save cpu_context; /* cpu context */

커널은 프로세스가 생성될 때 함수를 호출하고 실행할 수 있는 스택 공간을 부여하는데,
프로세스 스택 최상단 주소 공간에 struct thread_info 필드 데이터가 있습니다.

8 번째 줄 코드 오른쪽 주석문을 보면 역시 /* cpu context */ 이란 주석문을 볼 수 있습니다.
struct cpu_context_save 구조체인 cpu_context 필드가 컨택스트인 레지스터 세트를 표현합니다.

이번에는 struct cpu_context_save 구조체 코드를 조금 더 분석해 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/include/asm/thread_info.h]
1 struct cpu_context_save {
2 __u32 r4;
3 __u32 r5;
4 __u32 r6;
5 __u32 r7;
6 __u32 r8;
7 __u32 r9;
8 __u32 sl;
9 __u32 fp;
10 __u32 sp;
11 __u32 pc;
12 __u32 extra[2]; /* Xscale 'acc' register, etc */
13 };

struct cpu_context_save 구조체 필드를 보겠습니다. 

2~7번째 줄 코드를 보면 r4, r5, r6, r7, r8, r9 이란 필드를 볼 수 있습니다.
또한 8~11번째 줄 코드에서 sl, fp, sp, pc 란 필드가 있습니다.

각각 필드들은 CPU에서 실행했던 ARM 레지스터 세트를 의미합니다. 

코어 덤프에서 ARM 레지스터 세트를 어디에 저장하는지 확인해 봅시다.
1  crash> ps 718
2    PID    PPID  CPU   TASK    ST  %MEM     VSZ    RSS  COMM
3 >  718      1   2  9eb07000  WA   0.1   13540   1704  lxpanel

3 번째 줄 정보로 보아 프로세스 태스크 디스크립터 주소가 9eb07000 이라는 사실을 알 수 있습니다. 참고로, TASK란 필드는 프로세스 태스크 디스크립터를 의미합니다.
 
이번엔 다음 명령어를 입력해서 9eb07000 주소를 태스트 디스크립터 구조체로 캐스팅해보겠습니다. 
1 crash> struct task_struct 9eb07000
2 struct task_struct {
3  state = 256,
4  stack = 0x9b7de000,
5  usage = {
6    counter = 3
7  },

4 번째 줄 정보로 보아 프로세스 스택 최상단 주소는 0x9b7de000이란 정보를 알 수 있습니다.

이번엔 프로세스 스택 최상단 주소인 0x9b7de000 를 struct thread_info.cpu_context 필드로 캐스팅해보겠습니다.
1  crash> struct thread_info.cpu_context 0x9b7de000
2   cpu_context = {
3     r4 = 0xa6b46780,
4     r5 = 0x828ea000,
5     r6 = 0x9eb07000,
6     r7 = 0x81709294,
7     r8 = 0x9db8b200,
8     r9 = 0x9f89ea00,
9     sl = 0x0,
10    fp = 0x9b7dfd64,
11    sp = 0x9b7dfce8,
12    pc = 0x80f655ac,
13    extra = {0x0, 0x0}
14  }
  
출력 메시지를 보면 cpu_context 세부 필드에 레지스터 세트 정보를 확인할 수 있습니다.
r4~pc 필드가 해당 프로세스가 실행 중일 때 ARM 레지스터 세트 값입니다. 

달리 보면 이 프로세스가 컨택스트 스위칭을 실행할 때 저장한 ARM 레지스터 세트 값입니다.

그런데 pc 필드를 보면 0x80f655ac 주소를 확인할 수 있습니다.
0x80f655ac 주소의 정체는 무엇일까요? 0x80f655ac 주소 근처 코드를 보면 확인할 수 있습니다.
1 crash> dis 0x80f6559c 100
2 0x80f6559c <__schedule+0x37c>:  ldr     r2, [r5, #4]
3 0x80f655a0 <__schedule+0x380>:  mov     r0, r6
4 0x80f655a4 <__schedule+0x384>:  ldr     r1, [r6, #4]
5 0x80f655a8 <__schedule+0x388>:  bl      0x8010f96c <__switch_to>
6 0x80f655ac <__schedule+0x38c>:  bl      0x80158eec <finish_task_switch>

5 번째 줄 코드를 보면 __switch_to() 함수를 호출하는 어셈블리 코드입니다.
즉, 컨택스트 스위칭될 때 함수 주소라 할 수 있습니다.

5 번째 줄 코드는 C 코드로는 어떤 형태일까요?
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/sched/core.c]
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() 함수 6 번째 줄 코드입니다.

다시 컨택스트 스위칭이란 용어 의미를 정리합시다. 먼저 prev와 next 프로세스에 대해 소개합니다.
 

테이블에 소개된 내용을 정리하면 prev 프로세스는 CPU를 비울 프로세스, next는 다음에 CPU를 점유하면서 실행할 프로세스입니다.

prev와 next 프로세스 용어를 써서 컨택스트 스위칭에 대한 용어를 정리하면 다음과 같습니다.
 

컨택스트 스위칭이란 다음과 같은 문장으로 정리할 수 있겠습니다.
CPU에서 실행 중인 프로세스 정보로 채워진 CPU 레지스터 세트를 프로세스 스택 공간에 저장하고 다음에 실행할 프로세스의 레지스터 세트를 스택 공간에서 로딩해 CPU 레지스터 세트에 채우는 동작입니다.

CPU에서 실행 중인 프로세스 정보로 채워진 CPU 레지스터 세트를 프로세스 스택 공간에 저장하고 다음에 실행할 프로세스의 레지스터 세트를 스택 공간에서 로딩해 CPU 레지스터 세트에 채우는 동작입니다.

context_switch() 함수 분석
이번에는 current_thead_info()->cpu_context 필드 중심으로 컨택스트 스위칭 코드 동작을 알아보겠습니다. 이를 위해 context_switch() 함수에 전달하는 인자를 살펴볼 필요가 있습니다.
[https://elixir.bootlin.com/linux/v4.14.49/source/kernel/sched/core.c#L2750]
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  
+ 다음에 실행할 프로세스 
+ 스케줄러가 다음에 실행할 프로세스로 선택

이전 절에 봤던 ftrace 로그를 다시 살펴보면서 prev 인자의 의미를 확인합시다.
lxpanel-718   [002] d...  7831.739824: sched_switch: prev_comm=lxpanel prev_pid=718 prev_prio=120 prev_state=D ==> next_comm=Xorg next_pid=552 next_prio=120

먼저 가장 왼쪽 메시지를 보면 현재 CPU2에서 lxpanel(pid-718) 프로세스가 실행 중 이란 정보를 확인할 수 있습니다.

휴면에 진입할 프로세스는 lxpanel(pid=718) 이고 다음에 실행될 프로세스는 Xorg(pid=552) 인 것입니다.
struct task_struct *prev: prev_comm=lxpanel prev_pid=718 prev_prio=120
struct task_struct *next: ext_comm=Xorg next_pid=552 next_prio=120


여기서 한 가지 궁금한 점이 생깁니다.
Q: 현재 스케줄링 관련 코드는 어느 프로세스가 실행하고 있을까요?

schedule() 함수에서 prev로 지정된 함수입니다.
ftrace 로그를 봐도 마찬가지 정보를 확인할 수 있습니다.
lxpanel-718   [002] d...  7831.739824: sched_switch: prev_comm=lxpanel prev_pid=718 prev_prio=120 prev_state=D ==> next_comm=Xorg next_pid=552 next_prio=120

스케줄링을 실행하는 주체가 프로세스입니다. 프로세스는 자신에게 주어진 특정 미션을 수행하기 위해 생성됐다고 볼 수 있습니다. 그런데 프로세스가 해야 할 일에 스케줄링 동작이 추가된 것입니다. 따라서 스케줄링이 자주 실행하면 프로세스 실행에 부하를 줄 수 밖에 없습니다.

소프트웨어 세상에서 가장 어려운 과제가 최적화인 것 같습니다.
하지만 스케줄링을 적게 실행하면서 프로세스를 최대한 골고루 실행하는 것이 스케줄러의 역할이자 숙명인 것 같습니다. 트레이드 오프란 변명은 통하지 않습니다.


컨택스트 스위칭 동작을 휴면에 진입할 프로세스와 다음에 실행할 프로세스 입장에서 생각해봅시다.

먼저 휴면에 진입할 프로세스는 실행 정보를 어딘가에 저장해야 합니다. 다음에 스케줄러가 자신을 깨우면 다시 실행할 정보입니다. 이 실행 정보는 current_thread_info()->cpu_context 필드에 저장합니다. 여기서 실행 정보는 레지스터 세트를 의미합니다.

이번에 컨택스트 스위칭 동작을 휴면에 진입할 프로세스와 다음에 실행할 프로세스 입장에서 생각해봅시다.

먼저 휴면에 진입할 프로세스(struct task_struct *prev) 관점으로 생각해 봅시다.
휴면에 진입할 프로세스는 실행 정보를 어딘가에 저장해야 합니다. 다음에 스케줄러가 자신을 깨우면 다시 실행할 정보입니다. 이 실행 정보는 current_thread_info->cpu_context 필드에 저장합니다. 여기서 실행 정보는 레지스터 세트를 의미합니다.

위에서 언급한 ftrace를 예로 들겠습니다. 
struct task_struct *prev: prev_comm=lxpanel prev_pid=718 prev_prio=120

lxpanel 프로세스는 자신의 current_thread_info()->cpu_context 필드에 현재 실행 중인 레지스터 세트를 저장해야 합니다.

이번에는 다음에 실행할 프로세스(struct task_struct *next) 관점으로 생각해 봅시다.
다시 실행할 프로세스는 이전에 자신이 실행했던 정보를 ARM 레지스터에 로딩해야 합니다. 
어디서 이전에 자신이 실행했던 정보를 로딩할까요? current_thead_info()->cpu_context 필드에서 로딩합니다. 이전에 휴면에 진입할 때 저장했던 정보입니다.

위에서 언급한 ftrace를 예로 들겠습니다. 
struct task_struct *next: ext_comm=Xorg next_pid=552 next_prio=120

Xorg 프로세스는 자신의 current_thread_info()->cpu_context 필드에 있는 레지스터 세트를 ARM 레지스터로 로딩합니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/include/asm/thread_info.h]
1 struct cpu_context_save {
2 __u32 r4;
3 __u32 r5;
4 __u32 r6;
5 __u32 r7;
6 __u32 r8;
7 __u32 r9;
8 __u32 sl;
9 __u32 fp;
10 __u32 sp;
11 __u32 pc;
12 __u32 extra[2]; /* Xscale 'acc' register, etc */
13 };

ARM 레지스터 중에 PC(Program Counter)가 있습니다. 이 레지스터는 현재 프로세스가 실행 중인 코드 주소를 저장합니다. 이 주소는 11번째 줄 pc 필드에 저장돼 있습니다. 

PC 못지 않게 중요한 레지스터가 있습니다. 스택(Stack) 포인터 레지스터라고 하며 ARM에서 r13입니다. 이 주소는 10 번째 줄 sp 필드에서 확인할 수 있습니다.

context_switch() 함수부터 분석할 함수는 다음과 같습니다.
context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next, struct rq_flags *rf)
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() 주소

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:       e8ac6ff0        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 레지스터 세트를 저장합니다.

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:       e894aff0        ldm     r4, {r4, r5, r6, r7, r8, r9, sl, fp, sp, pc}

r4에 저장된 레지스터 정보를 ARM 레지스터에 로딩합니다. 이 순간부터 기존에 실행했던 프로그램 카운터로 이동해 다시 실행을 재개합니다.


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)

Reference(프로세스 스케줄링)

스케줄링 소개
프로세스 상태 관리
   어떤 함수가 프로세스 상태를 바꿀까?
스케줄러 클래스
런큐
CFS 스케줄러
   CFS 관련 세부 함수 분석  
선점 스케줄링(Preemptive Scheduling)   
프로세스는 어떻게 깨울까?
스케줄링 핵심 schedule() 함수 분석
컨택스트 스위칭
스케줄링 디버깅
   스케줄링 프로파일링
     CPU에 부하를 주는 테스트   
     CPU에 부하를 주지 않는 테스트

핑백

덧글

댓글 입력 영역