Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

15192
888
89788


4.6.3 do_exit() 함수 분석 4장. 프로세스 관리

do_exit() 함수로 커널이 프로세스를 종료 시키는 세부 동작 못지 않게 프로세스가 종료되는 흐름을 파악하는 것이 중요합니다. 그 이유는 무엇일까요? 

유저 어플리케이션 프로세스나 커널 프로세스가 예외 상황에서 의도하지 않게 종료해서 문제가 발생하는 경우가 있습니다. 이런 문제를 만났을 때 커널 어느 함수부터 분석을 해야할 지 결정할 수 있습니다.

이번에는 do_exit() 함수가 어떻게 실행되는지 알아봅시다.

- exit() 시스템 콜 실행
유저 어플리케이션 프로세스가 자신을 종료하려면 리눅스 저수준 함수로 exit() 함수를 호출합니다. 이 때 시스템 콜을 발생시킨 다음 sys_group_exit() 시스템콜 핸들러를 실행합니다.

- 다른 프로세스가 프로세스 종료 시그널을 전달했을 경우
프로세스 종료 시그널을 다른 프로세스가 전달했을 경우에도 do_exit() 함수가 호출될 수 있습니다. 또한 커널 함수로 send_signal() 함수를 호출하면 특정 프로세스 종료 시그널을 전달할 수 있습니다. 프로세스 종료 시그널을 받은 프로세스는 do_exit() 함수를 실행해서 자신을 종료합니다.

이렇게 프로세스를 종료할 때 do_exit() 함수를 호출합니다. 이 함수는 프로세스가 쓰는 리소스를 해제하는 역할을 수행합니다.

먼저 do_exit() 함수에 전달되는 인자와 반환값을 확인합시다.
void __noreturn do_exit(long code);

do_exit() 함수는 프로세스에 대한 리소스를 정리한 후 do_task_dead() 함수를 호출한 후 schedule() 함수를 실행합니다. 따라서 do_exit() 함수를 끝까지 실행하지 않습니다. 그래서 함수 선언부에 __noreturn 이란 매크로로 지정한 것입니다.

do_exit() 함수에서 do_task_dead() 함수를 호출해서 schedule() 함수 실행으로 함수 흐름을 마무리하는 이유는 무엇일까요?

그 동안 프로세스 실행 흐름에 대해 살펴봤듯이 프로세스는 자신의 스택 공간에서 함수를 실행합니다. do_exit() 함수는 프로세스 스택 공간에서 실행을 시작하는 것입니다.

그런데 프로세스 리소스를 해제하는 함수를 호출하면서 태스크 디스크립터 대부분 멤버와 스택 공간까지 해제합니다. do_exit() 함수 실행을 끝낸 다음에 복귀를 못하는 것입니다. 따라서 do_task_dead()/schedule() 함수를 호출해서 do_exit() 함수 실행을 마무리합니다.

이번에는 이 함수에 전달하는 code란 인자를 점검합시다. code라는 인자는 프로세스 종료 코드를 의미합니다.

만약 kill -9 라는 명령어로 프로세스를 종료하면 code 인자로 9가 전달됩니다.

다음은 Trace32로 do_exit() 함수를 호출했을 때 브레이크 포인트를 걸고 본 디버깅 정보입니다.
-000|do_exit(?)
-001|do_group_exit(exit_code = 9)
-002|get_signal(?)
-003|do_notify_resume(regs = 0A4233EC0, thread_flags = 9)
-004|work_pending(asm)

"kill -9 [PID]" 명령어로 프로세스를 종료하니 exit_code 인자에 9가 전달된 것입니다.  

do_exit() 함수는 다음 순서로 프로세스를 종료합니다.
- 이미 프로세스가 종료 중 인데 다시 do_exit() 함수를 호출하는지 점검합니다.
- 프로세스 리소스(파일 디스크립터, 가상 메모리, 시그널) 등등을 해제합니다.
- 부모 프로세스에게 자신이 종료되고 있다고 알립니다.
- 프로세스 실행 상태를 struct task_struct state 멤버에 TASK_DEAD 로 설정합니다.
- do_task_dead() 함수를 호출해서 schedule() 함수를 실행하여 스케줄링 됩니다..

분석할 do_exit() 함수 코드는 다음과 같습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/exit.c]
1 void __noreturn do_exit(long code)
2 {
3 struct task_struct *tsk = current;
4 int group_dead;
...
5 if (unlikely(in_interrupt()))
6 panic("Aiee, killing interrupt handler!");
7 if (unlikely(!tsk->pid))
8 panic("Attempted to kill the idle task!");
...
9 if (unlikely(tsk->flags & PF_EXITING)) {
10 pr_alert("Fixing recursive fault but reboot is needed!n");
11
12 tsk->flags |= PF_EXITPIDONE;
13 set_current_state(TASK_UNINTERRUPTIBLE);
14 schedule();
15 }
16
17 exit_signals(tsk);  /* sets PF_EXITING */
...
18 exit_mm();
...
19 exit_files(tsk);
20 exit_fs(tsk);
...
21 do_task_dead();
...
22}

먼저 다음 코드를 보겠습니다.
5 if (unlikely(in_interrupt()))
6 panic("Aiee, killing interrupt handler!");
7 if (unlikely(!tsk->pid))
8 panic("Attempted to kill the idle task!");

특이한 상황에 부팅 도중 init 프로세스가 중료하는 경우가 있습니다. 이 때 다음와 같은 커널 로그와 함께 커널 패닉이 발생합니다.
[  837.981513 / 10-11 11:11:00.958][4] Kernel panic - not syncing: Attempted to kill init! exitcode=0x0000000b
[  837.981513 / 10-11 11:11:00.958][4] 
[  837.981547 / 10-11 11:11:00.958][6] CPU6: stopping
[  837.981571 / 10-11 11:11:00.958][6] CPU: 6 PID: 339 Comm: mmc-cmdqd/0 Tainted: P        W  O   3.18.31-perf #1

init 프로세스는 유저 레벨 프로세스를 생성하고 관장하는 역할을 수행합니다. 그래서 init 프로세스가 불의의 상황으로 종료하면 강제로 커널 패닉을 유발합니다.

보통 리눅스 커널 버전을 업그레이드 후 root 파일 시스템이나 다바이스 노드를 생성 못했을 때 init 프로세스가 종료됩니다.
 
다음 코드를 보겠습니다.
9 if (unlikely(tsk->flags & PF_EXITING)) {
10 pr_alert("Fixing recursive fault but reboot is needed!n");
11
12 tsk->flags |= PF_EXITPIDONE;
13 set_current_state(TASK_UNINTERRUPTIBLE);
14 schedule();
15 }

프로세스가 종료 중이면 do_exit() 함수에서 호출되는 exit_signals() 함수에서  태스크 디스크립터 struct task_struct flags를 PF_EXITING로 설정합니다.
void exit_signals(struct task_struct *tsk)
{
...
tsk->flags |= PF_EXITING;

프로세스가 종료 중인데 다시 do_exit() 함수를 실행해서 중복 종류를 시도하는 경우 예외 처리 코드입니다.

프로세스 태스크 디스크립터 멤버인 state에 TASK_UNINTERRUPTIBLE로 상태를 지정하고 schedule() 함수를 호출해서 다른 프로세스가 동작하도록 합니다.

다음 코드를 보겠습니다.
17 exit_signals(tsk);  /* sets PF_EXITING */

프로세스 struct task_strtuct.flags를 PF_EXITING 으로 변경합니다.
종료할 프로세스가 처리할 시그널이 있으면 retarget_shared_pending() 함수를 실행해서 시그널을 대신 처리할 프로세스를 선정합니다.

다음 코드를 보겠습니다.
18 exit_mm();

프로세스의 메모리 디스크립터(struct mm_struct) 리소스를 해제하고 메모리 디스크립터 사용 카운트를 1만큼 감소합니다.

다음 코드를 봅시다.
19 exit_files(tsk);
20 exit_fs(tsk);

프로세스에 쓰고 있는 파일 디스크립터 정보를 해제합니다.

schedule() 함수를 호출하면, finish_task_switch() 함수에서 do_exit() 함수로 종료하는 프로세스의 태스크 디스크립터와 스택 메모리를 해제합니다.
static struct rq *finish_task_switch(struct task_struct *prev)
__releases(rq->lock)
{
...
if (unlikely(prev_state == TASK_DEAD)) {
if (prev->sched_class->task_dead)
prev->sched_class->task_dead(prev);

kprobe_flush_task(prev);

/* Task is done with its stack. */
put_task_stack(prev);

put_task_struct(prev);
}

put_task_stack() 함수를 호출해서 프로세스 스택 메모리 공간을 해제하여 커널 메모리 공간에 반환합니다. 바로 put_task_struct() 함수를 실행해서 프로세스를 표현하는 자료구조인 struct task_struct 가 위치한 메모리를 해제합니다.

참고로, finish_task_switch() 함수에 ftrace 필터를 걸고 이 함수가 호출되는 함수 흐름을 확인하면 다음과 같습니다.
->transport-8537  [002] d..3 714.329199: finish_task_switch+0x28/0x20c <-__schedule+0x2bc/0x84c
     ->transport-8537  [002] d..3   714.329206: <stack trace>
 => schedule+0x3c/0x9c
 => schedule_timeout+0x288/0x378
 => unix_stream_read_generic+0x588/0x754
 => unix_stream_recvmsg+0x70/0x94
 => sock_read_iter+0xd4/0x100
 => new_sync_read+0xd8/0x11c
 => vfs_read+0x118/0x178
 => SyS_read+0x60/0xc0
 => ret_fast_syscall_+0x4/0x28

프로세스 스케줄링 동작 때 호출되는 함수입니다.

"이 포스팅이 유익하다고 생각되시면 댓글로 응원해주시면 감사하겠습니다.  
혹시 글을 읽고 궁금점이 있으면 댓글로 질문 남겨주세요. 상세한 답글 올려 드리겠습니다!"


Reference(프로세스 관리)
4.9 프로세스 컨택스트 정보는 어떻게 저장할까?
 4.9.1 컨택스트 소개
 4.9.2 인터럽트 컨택스트 정보 확인하기
 4.9.3 Soft IRQ 컨택스트 정보 확인하기
 4.9.4 선점 스케줄링 여부 정보 저장
4.10 프로세스 디스크립터 접근 매크로 함수
 4.10.1 current_thread_info()
 4.10.2 current 매크로란
4.11 프로세스 디버깅
 4.11.1 glibc fork 함수 gdb 디버깅
 4.11.2 유저 프로그램 실행 추적 


#Reference 시스템 콜

Reference(워크큐)
워크큐(Workqueue) Overview


핑백

덧글

댓글 입력 영역