do_task_dead() 함수 분석하기
이어서 do_task_dead() 함수 코드를 분석하겠습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/sched/core.c]
01 void __noreturn do_task_dead(void)
02 {
03 set_special_state(TASK_DEAD);
04 current->flags |= PF_NOFREEZE;
05
06 __schedule(false);
07 BUG();
08 for (;;)
09 cpu_relax();
10 }
03 번째 줄 코드에서 set_special_state() 함수를 호출해 프로세스 상태를 TASK_DEAD 플래그로 바꿉니다. 04 번째 줄에서는 프로세스 태스크 디스크립터의 flags 필드에 PF_NOFREEZE 플래그와 OR 비트 연산을 수행합니다.
current는 현재 실행 중인 프로세스 태스크 디스크립터 자료구조로 struct task_struct 구조체 타입입니다.
06 번째 줄 코드에서는 __schedule(false) 함수를 호출해 스케줄링을 요청합니다.
__schedule() 함수에 false 를 전달하는 이유는 선점 스케줄링을 실행하지 않겠다는 의미입니다.
do_task_dead() 함수를 호출하고 난 후 동작
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://github.com/raspberrypi/linux/blob/rpi-4.19.y/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://github.com/raspberrypi/linux/blob/rpi-4.19.y/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() 함수를 실행해 프로세스 컨택스트 스위칭하는 과정은 스케줄링 챕터에서 자세히 다룹니다.
__schedule() 함수에서 finish_task_switch() 함수까지 호출되는 흐름을 살펴봤으니 finish_task_switch() 함수를 분석할 차례입니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/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() 함수를 실행해서 프로세스를 표현하는
자료구조인 struct task_struct 가 위치한 메모리를 해제합니다.
finish_task_switch() 함수에 ftrace 필터를 걸고 이 함수가 호출되는 함수 흐름을 확인하면 다음과 같습니다.
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 프로세스 스택 메모리 공간을 해제합니다.
지금까지 프로세스가 생성하고 종료되는 흐름을 살펴봤습니다.
최근 덧글