ARM Linux Kernel Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

199239
1625
172596


[리눅스커널] 프로세스: do_task_dead() 함수를 호출하고 난 후의 동작 4. 프로세스(Process) 관리

do_task_dead() 함수에서 __schedule() 함수를 호출하고 나면 커널은 어떻게 프로세스를 소멸시킬까요? 먼저 __schedule() 함수를 호출하면 어떤 흐름으로 finish_task_switch() 함수를 호출하는지 살펴보겠습니다. 다음은 이번 절에서 분석할 함수의 목록입니다.

__schedule() 함수
context_switch() 함수
finish_task_switch() 함수

위 함수들이 실행되면서 다음과 같은 과정으로 프로세스가 소멸됩니다.

종료할 프로세스는 do_exit() 함수에서 대부분 자신의 리소스를 커널에게 반납하고 자신의 상태를 TASK_DEAD로 바꾼다.
컨텍스트 스위칭을 한다.
컨텍스트 스위칭으로 다음에 실행하는 프로세스는 finish_task_switch() 함수에서 이전에 실행했던 프로세스 상태(종료할 프로세스)가 TASK_DEAD이면 프로세스 스택 공간을 해제한다.

조금 끔찍한 비유를 들면서 이 동작 방식을 설명하겠습니다. 전쟁 영화에서 '자살을 하기 두려운 병사가 다른 동료에게 자신을 죽여달라고 권총을 주는 장면'을 본 적이 있나요? 이와 조금 비슷한 상황인데, 프로세스가 스스로 자신의 스택 메모리 공간을 해제하지 못하니 컨텍스트 스위칭을 수행한 후 다음에 실행되는 프로세스에게 자신의 스택 메모리 공간을 해제하고 소멸시켜 달라고 부탁하는 것입니다. 

실행 흐름을 설명했으니 __schedule() 함수를 보겠습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/core.c
01 static void __sched notrace __schedule(bool preempt)
02 {
...
03 if (likely(prev != next)) {
...
04 rq = context_switch(rq, prev, next, &rf);
05 } else {
06 rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);
07 rq_unlock_irq(rq, &rf);
08 }
...
09 }

04번째 줄에서 context_switch() 함수를 호출해 컨텍스트 스위칭을 실행합니다.

다음으로 context_switch() 함수를 보겠습니다.

[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/core.c]
01 static __always_inline struct rq *
02 context_switch(struct rq *rq, struct task_struct *prev,
03        struct task_struct *next, struct rq_flags *rf)
04 {
...
05 switch_to(prev, next, prev);
06 barrier();
07
08 return finish_task_switch(prev);
09 }

08 번째 줄에서는 finish_task_switch() 함수를 호출합니다. schedule() 함수를 호출하면 결국 finish_task_switch() 함수가 호출됩니다.

---
__schedule() 함수와 context_switch() 함수를 실행해 프로세스가 컨텍스트 스위칭이 이뤄지는 과정은 10장 '스케줄링'의 10.9절을 참고하세요.
---

__schedule() 함수에서 finish_task_switch() 함수까지 호출되는 흐름을 살펴봤으니 이번에는 finish_task_switch() 함수를 분석할 차례입니다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/core.c
01 static struct rq *finish_task_switch(struct task_struct *prev)
02 __releases(rq->lock)
03 {
...
04 if (unlikely(prev_state == TASK_DEAD)) {
05 if (prev->sched_class->task_dead)
06 prev->sched_class->task_dead(prev);
07
08 kprobe_flush_task(prev);
09
10 put_task_stack(prev);
11
12 put_task_struct(prev);
13 }

04번째 줄은 이전에 실행했던 프로세스 상태가 TASK_DEAD일 때 05~12번째 줄을 실행하는 조건문입니다.

10번째 줄을 보겠습니다. put_task_stack() 함수를 호출해서 프로세스 스택 메모리 공간을 해제하고 커널 메모리 공간에 반환합니다.

이어서 12번째 줄에서는 put_task_struct() 함수를 실행해 프로세스를 표현하는 자료구조인 task_struct가 위치한 메모리를 해제합니다.

finish_task_switch() 함수에 ftrace 필터를 걸고 finish_task_switch() 함수가 호출되는 함수 흐름을 확인하면 다음과 같습니다.

01  TaskSchedulerSe-1803  [000] ....  2630.705218: do_exit+0x14/0xbe0 <-do_group_exit+0x50/0xe8
02  TaskSchedulerSe-1803  [000] ....  2630.705234: <stack trace>
03  => do_exit+0x18/0xbe0
04  => do_group_exit+0x50/0xe8
05  => get_signal+0x160/0x7dc
06  => do_signal+0x274/0x468
07  => do_work_pending+0xd4/0xec
08  => slow_work_pending+0xc/0x20
09  => 0x756df704
10 TaskSchedulerSe-1803  [000] dns.  2630.705438: sched_wakeup: comm=rcu_sched pid=10 prio=120 target_cpu=000
11 TaskSchedulerSe-1803  [000] dnh.  2630.705466: sched_wakeup: comm=jbd2/mmcblk0p2- pid=77 prio=120 target_cpu=000
12 TaskSchedulerSe-1803  [000] d...  2630.705479: sched_switch: prev_comm=TaskSchedulerSe prev_pid=1803 prev_prio=120 prev_state=R+ ==> next_comm=rcu_sched next_pid=10 next_prio=120
13       rcu_sched-10    [000] d...  2630.705483: finish_task_switch+0x14/0x230 <-__schedule+0x328/0x9b0
14       rcu_sched-10    [000] d...  2630.705504: <stack trace>
15 => finish_task_switch+0x18/0x230
16 => __schedule+0x328/0x9b0
17 => schedule+0x50/0xa8
18 => rcu_gp_kthread+0xdc/0x9fc
19 => kthread+0x140/0x170
20 => ret_from_fork+0x14/0x28
 
위 ftrace 메시지는 TaskSchedulerSe-1803 프로세스가 종료되는 과정을 담고 있습니다.

01~08번째 줄 메시지를 보겠습니다.

01  TaskSchedulerSe-1803  [000] ....  2630.705218: do_exit+0x14/0xbe0 <-do_group_exit+0x50/0xe8
02  TaskSchedulerSe-1803  [000] ....  2630.705234: <stack trace>
03  => do_exit+0x18/0xbe0
04  => do_group_exit+0x50/0xe8
05  => get_signal+0x160/0x7dc
06  => do_signal+0x274/0x468
07  => do_work_pending+0xd4/0xec
08  => slow_work_pending+0xc/0x20

TaskSchedulerSe-1803 프로세스가 종료 시그널을 받고 do_exit() 함수를 호출합니다.

다음 12번째 줄입니다.
12 TaskSchedulerSe-1803  [000] d...  2630.705479: sched_switch: prev_comm=TaskSchedulerSe prev_pid=1803 prev_prio=120 prev_state=R+ ==> next_comm=rcu_sched next_pid=10 next_prio=120

TaskSchedulerSe-1803 프로세스에서 pid가 10인 rcu_sched 프로세스로 스케줄링됩니다.

마지막으로 13~20번째 줄을 보겠습니다.

13       rcu_sched-10    [000] d...  2630.705483: finish_task_switch+0x14/0x230 <-__schedule+0x328/0x9b0
14       rcu_sched-10    [000] d...  2630.705504: <stack trace>
15 => finish_task_switch+0x18/0x230
16 => __schedule+0x328/0x9b0
17 => schedule+0x50/0xa8
18 => rcu_gp_kthread+0xdc/0x9fc
19 => kthread+0x140/0x170
20 => ret_from_fork+0x14/0x28

rcu_sched 프로세스는 finish_task_switch() 함수에서 TaskSchedulerSe-1803 프로세스의 마지막 리소스를 정리합니다. 

TaskSchedulerSe-1803 프로세스는 do_exit() 함수에서 태스크 디스크립터의 여러 필드를 해제했습니다. 그런데 do_exit() 함수를 TaskSchedulerSe-1803 프로세스의 스택 공간에서 실행 중이니 스스로 자신의 스택 공간을 해제할 수 없습니다. 따라서 스케줄링을 한 후 다음에 실행하는 프로세스인 rcu_sched가 종료되는 TaskSchedulerSe-1803 프로세스의 스택 메모리 공간을 해제하는 것입니다.

이번 절에서는 프로세스가 생성되고 종료되는 흐름을 살펴봤습니다. 이어서 프로세스를 표현하고 관리하는 프로세스 자료구조를 살펴보겠습니다.

* 유튜브 강의 동영상도 있으니 같이 들으시면 좋습니다. 




#프로세스

프로세스 소개 
프로세스 확인하기  
프로세스는 어떻게 생성할까?  
유저 레벨 프로세스 실행 실습  
커널 스레드  
커널 내부 프로세스의 생성 과정   
프로세스의 종료 과정 분석  
태스크 디스크립터(task_struct 구조체)  
스레드 정보: thread_info 구조체  
프로세스의 태스크 디스크립터에 접근하는 매크로 함수  
프로세스 디버깅  
   * glibc의 fork() 함수를 gdb로 디버깅하기  


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

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


# Reference: For more information on 'Linux Kernel';

디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 1

디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 2


 





핑백

덧글

댓글 입력 영역