Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

130199
1107
135858


[리눅스커널] 프로세스: do_exit() 함수 분석 4. 프로세스(Process) 관리

do_exit() 함수의 이름만 보더라도 '종료를 실행한다'라는 동작을 할 것이라 예상할 수 있습니다. 여기서 '종료를 실행한다'의 주체는 프로세스입니다. 이번 절에서는 do_exit() 함수를 분석하면서 프로세스가 종료되는 과정을 살펴보겠습니다. 

do_exit() 함수 선언부와 인자 확인

먼저 do_exit() 함수에 전달되는 인자와 반환값을 확인해 보겠습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/exit.c
void __noreturn do_exit(long code);

먼저 code 인자를 살펴보겠습니다. code라는 인자는 프로세스 종료 코드를 의미합니다. 만약 터미널에서 “kill -9 [pid]”라는 명령어를 입력해 프로세스를 종료하면 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)

001번째 줄을 보면 exit_code가 9입니다. "kill -9 [PID]" 명령어로 프로세스를 종료하니 exit_code 인자로 9가 전달된 것입니다.  

do_exit() 함수의 선언부 왼쪽을 보면 __noreturn 키워드가 보입니다. 이 지시자는 실행 후 자신을 호출한 함수로 되돌아가지 않는다는 뜻입니다. 다른 관점에서 보면 커널 코드를 실행하는 주인공인 프로세스가 종료되니 당연히 이전 함수로 되돌아가지 못합니다. 따라서 반환값은 없습니다.

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

do_exit() 함수에서 do_task_dead() 함수를 호출해서 schedule() 함수를 실행함으로써 함수 흐름을 마무리하는 이유는 무엇일까요? 프로세스는 자신의 프로세스 스택 메모리 공간을 해제할 수 없기 때문입니다. 즉, 프로세스를 해제하는 동작인 do_exit() 함수를 스택 메모리 공간에서 실행하기 때문입니다. 이를 다른 관점에서 설명하자면 자신의 프로세스 스택 공간을 해제하는 일을 프로세스 스택 공간에서 실행하는 것은 불가능합니다. 따라서 schedule() 함수를 호출해 스케줄링한 후 '다음에 실행하는 프로세스'가 종료되는 프로세스의 스택 메모리 공간을 해제시켜 줍니다. 이를 위해 do_task_dead()/schedule() 함수를 호출해서 do_exit() 함수 실행을 마무리하는 것입니다.

do_exit() 함수의 동작 방식 확인

do_exit() 함수의 실행 단계는 다음과 같이 정리할 수 있습니다. 

1. init 프로세스가 종료하면 강제 커널 패닉 유발: 보통 부팅 과정에서 발생함
2. 이미 프로세스가 do_exit() 함수의 실행으로 프로세스가 종료되는 도중 다시 do_exit() 함수가 호출됐는지 점검
3. 프로세스 리소스(파일 디스크립터, 가상 메모리, 시그널) 등을 해제 
4. 부모 프로세스에게 자신이 종료되고 있다고 알림
5. 프로세스의 실행 상태를 task_struct 구조체의 state 필드에 TASK_DEAD 로 설정  
6. do_task_dead() 함수를 호출해 스케줄링을 실행 
do_task_dead() 함수에서 __schedule() 함수가 호출되어 프로세스 자료구조인 태스크 디스크립터와 스택 메모리를 해제 

do_exit() 함수 코드 분석

분석할 do_exit() 함수는 다음과 같습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/exit.c
01 void __noreturn do_exit(long code)
02 {
03 struct task_struct *tsk = current;
04 int group_dead;
...
05 if (unlikely(tsk->flags & PF_EXITING)) {
06 pr_alert("Fixing recursive fault but reboot is needed!\n");
07
08 tsk->flags |= PF_EXITPIDONE;
09 set_current_state(TASK_UNINTERRUPTIBLE);
10 schedule();
11 }
12
13 exit_signals(tsk);  /* sets PF_EXITING */
...
14 exit_mm();
...
15 exit_files(tsk);
16 exit_fs(tsk);
...
17 exit_notify(tsk, group_dead);
...
18 do_task_dead();
...
19 }

먼저 05~11번째 줄을 보겠습니다.

05 if (unlikely(tsk->flags & PF_EXITING)) {
06 pr_alert("Fixing recursive fault but reboot is needed!\n");
07
08 tsk->flags |= PF_EXITPIDONE;
09 set_current_state(TASK_UNINTERRUPTIBLE);
10 schedule();
11 }

태스크 디스크립터를 나타내는 task_struct 구조체의 flags에 PF_EXITING 플래그가 설정됐을 때 06~10번째 줄 코드를 실행합니다. 이것은 프로세스가 do_exit() 함수를 실행하는 도중에 다시 do_exit() 함수가 호출됐을 때 예외를 처리하는 코드입니다.

예외 처리 코드의 실행 조건을 확인했으니 다음 코드를 보겠습니다.

12 tsk->flags |= PF_EXITPIDONE;
13 set_current_state(TASK_UNINTERRUPTIBLE);
14 schedule();

12번째 줄에서 task_struct 구조체의 flags에 PF_EXITPIDONE 플래그를 설정합니다. 13번째 줄에서는 프로세스 태스크 디스크립터 필드인 state에 TASK_UNINTERRUPTIBLE로 상태를 지정합니다. 다음 14 번째 줄에서는 schedule() 함수를 호출해서 다른 프로세스가 동작하게 합니다.

---
do_exit() 함수 내에서 exit_signals() 함수가 실행되면 task_struct 구조체의 flags를 PF_EXITING 플래그로 설정합니다.
https://elixir.bootlin.com/linux/v4.19.30/source/kernel/signal.c
void exit_signals(struct task_struct *tsk)
{
...
tsk->flags |= PF_EXITING; 
...
}

task_struct 구조체의 flags가 PF_EXITING 플래그이면 현재 프로세스가 do_exit() 함수를 실행 중이라는 의미입니다.
---

다음으로 13번째 줄을 보겠습니다.

13 exit_signals(tsk);  /* sets PF_EXITING */

프로세스의 task_strtuct 구조체의 flags 필드를 PF_EXITING로 바꿉니다. 종료할 프로세스가 처리할 시그널이 있으면 retarget_shared_pending() 함수를 실행해 시그널을 대신 처리할 프로세스를 선정합니다.

다음으로 14번째 줄을 보겠습니다.

14 exit_mm();

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

다음 코드를 보겠습니다.

15 exit_files(tsk);
16 exit_fs(tsk);

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

다음으로 17 번째 줄을 보겠습니다.

17 exit_notify(tsk, group_dead);

부모 프로세스에게 현재 프로세스가 종료 중이라는 사실을 통지합니다.

마지막으로 18 번째 줄에서 do_task_dead() 함수를 호출합니다.

18 do_task_dead();

이미 do_exit() 함수에서 메모리와 파일 리소스를 해제했고, 부모 프로세스에게 자신이 종료 중이라는 사실도 통보했습니다. 이제는 마지막 소멸 단계입니다.

---
부팅 도중 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] CPU2: stopping
[  837.981571 / 10-11 11:11:00.958][6] CPU: 2 PID: 339 Comm: mmc-cmdqd/0 Tainted: P   

유저 프로세스의 부모 프로세스가 종료되면 누가 부모 프로세스 역할을 대신할까요? 바로 init 프로세스가 대신 부모 프로세스가 됩니다. init 프로세스는 유저 레벨 프로세스를 생성하며 관장하는 역할을 수행합니다. 그런데 init 프로세스가 종료된다면 시스템은 정상적으로 동작할 수 없는 심각한 상황입니다. init 프로세스가 불의의 상황으로 종료되면 강제로 커널 패닉을 발생시킵니다.

보통 리눅스 커널 버전을 업그레이드한 후 root 파일 시스템이나 시스템을 초기화하는 데 필요한 디바이스 노드를 생성하지 못했을 때 init 프로세스가 종료됩니다.
---

#프로세스

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


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

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





핑백

덧글

댓글 입력 영역