Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

8200
629
98815


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

do_exit() 함수 이름으로 '종료를 실행한다.'란 동작을 할 것이라 예상할 수 있습니다. 여기서 '종료를 실행한다.' 의 주어는 프로세스가 되겠습니다.

이번 소절에서는 do_exit() 함수를 분석하면서 프로세스가 종료하는 과정을 살펴보겠습니다. 

do_exit() 함수 선언부와 인자 확인하기
먼저 do_exit() 함수에 전달되는 인자와 반환값을 확인합시다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/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. 프로세스 실행 상태를 struct task_struct state 필드에 TASK_DEAD 로 설정합니다.
6. do_task_dead() 함수를 호출해 스케줄링을 실행합니다.
; do_task_dead() 함수에서 __schedule() 함수가 호출되어 프로세스 자료구조인 태스크  
  디스크립터와 스택 메모리를 해제합니다.

do_exit() 함수 코드 분석하기
분석할 do_exit() 함수 코드는 다음과 같습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/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 }

태스크 디스크립터 struct 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 번째 줄 코드에서 태스크 디스크립터 struct task_struct 구조체 flags에 PF_EXITPIDONE 플래그를 설정합니다. 13 번째 줄에서는 프로세스 태스크 디스크립터 필드인 state에 TASK_UNINTERRUPTIBLE로 상태를 지정합니다. 다음 14 번째 줄에서 schedule() 함수를 호출해서 다른 프로세스가 동작하도록 합니다.


do_exit() 함수 내 exit_signals() 함수를 실행하면 struct task_struct 구조체 flags를 PF_EXITING 플래그로 설정합니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/signal.c]
void exit_signals(struct task_struct *tsk)
{
...
tsk->flags |= PF_EXITING; 
...
}

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


다음 13번째 줄 코드를 보겠습니다.
13 exit_signals(tsk);  /* sets PF_EXITING */

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

다음 14번째 줄 코드를 보겠습니다.
14 exit_mm();

프로세스의 메모리 디스크립터 struct 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 프로세스가 종료됩니다.


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

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

#Reference(프로세스 관리)
프로세스 디버깅
   glibc fork 함수 gdb 디버깅





핑백

덧글

댓글 입력 영역