1. 프로세스 생명주기와 부모-자식 관계
운영체제에서 프로세스는 생성(fork() 시스템 콜)과 소멸(exit() 시스템 콜)의 생명주기를 갖습니다. fork()를 통해 새로운 프로세스를 생성할 때, 호출한 프로세스는 부모 프로세스가 되고 새로 생성된 프로세스는 자식 프로세스가 됩니다. 이 부모-자식 관계는 프로세스 관리의 핵심적인 부분이며, 고아 프로세스와 좀비 프로세스는 이 관계가 비정상적으로 처리될 때 발생하는 특수한 상태입니다.
1.1. 프로세스의 정상적인 종료 절차
- 자식 프로세스는 실행을 완료하면
exit()시스템 콜을 호출하여 커널에 종료를 알립니다. - 커널은 자식 프로세스의 대부분 리소스(메모리, 파일 디스크립터 등)를 해제하지만, 프로세스 테이블에 최소한의 정보(PID, 종료 상태, CPU 사용 시간 등)는 남겨둡니다. 이 상태의 프로세스를 좀비(Zombie) 라고 합니다.
- 부모 프로세스는
wait()또는waitpid()시스템 콜을 호출하여 자식 프로세스의 종료 상태 정보를 수신합니다. - 커널은
wait()호출을 받으면 좀비 상태인 자식의 남은 정보(프로세스 테이블 엔트리)를 부모에게 전달하고, 테이블에서 해당 엔트리를 완전히 삭제합니다. 이 과정을 자식 프로세스를 거둔다(reaping)고 표현합니다. - 이로써 자식 프로세스의 모든 흔적이 시스템에서 사라지며 생명주기가 완전히 종료됩니다.
2. 좀비 프로세스 (Zombie Process)
2.1. 정의 및 발생 원인
좀비 프로세스는 실행을 종료했지만, 부모 프로세스가 아직 wait() 계열 시스템 콜을 호출하여 종료 상태를 거두어가지 않아 프로세스 테이블에 항목이 남아있는 프로세스를 의미합니다. 이미 실행이 종료되어 CPU를 사용하지 않으므로 '죽었지만' 완전히 사라지지는 않은, 말 그대로 '좀비' 같은 상태입니다.
주된 발생 원인은 부모 프로세스의 버그나 부주의한 설계 때문입니다. 자식 프로세스를 생성하고 난 후, 해당 자식의 종료를 적절히 처리(wait())하지 않으면 자식은 종료될 때마다 좀비로 남게 됩니다.
2.2. 내부 구조 및 커널에서의 처리
- 프로세스 상태: Linux/Unix 시스템에서
ps명령어를 사용하면 좀비 프로세스는 상태(STAT)가Z또는Z+로 표시됩니다. - 프로세스 테이블 엔트리: 자식 프로세스가
exit()를 호출하면, 커널은do_exit()함수를 실행합니다. 이 함수는 다음 작업을 수행합니다.- 프로세스가 사용하던 대부분의 메모리(코드, 데이터, 스택 영역)와 파일 디스크립터 등 리소스를 해제합니다.
- 하지만 프로세스 디스크립터(
task_structin Linux) 자체는 메모리에 유지합니다. 여기에는 다음 정보가 포함됩니다.- PID (Process ID)
- 종료 상태(Exit Status): 정상 종료인지, 특정 시그널에 의해 종료되었는지 등의 정보.
- 자원 사용량 통계: CPU 사용 시간, 메모리 사용량 등.
- 프로세스의 상태를
TASK_ZOMBIE로 변경합니다. - 부모 프로세스에게 자식이 종료되었음을 알리기 위해
SIGCHLD시그널을 보냅니다.
부모 프로세스가 wait()를 호출하면, 커널은 do_wait() 함수를 통해 좀비 상태인 자식을 찾아 그 프로세스 디스크립터에 저장된 정보를 부모에게 전달한 후, 비로소 task_struct를 메모리에서 해제하고 PID를 재사용 가능하게 만듭니다.
2.3. 문제점 및 해결 방안
- 문제점:
- PID 고갈: 좀비 프로세스 자체는 메모리를 거의 차지하지 않지만, 프로세스 테이블에서 하나의 항목을 계속 차지합니다. 시스템이 생성할 수 있는 PID의 수는 제한되어 있으므로, 좀비 프로세스가 계속 쌓이면 새로운 프로세스를 생성하지 못하는 심각한 문제가 발생할 수 있습니다.
- 리소스 누수: 아주 적은 양의 커널 메모리(프로세스 디스크립터)가 누수됩니다.
- 해결 방안:
- 명시적인
wait()호출: 부모 프로세스의 코드에서 자식 프로세스가 종료될 때까지 기다리거나(wait()), 비동기적으로 종료를 처리(waitpid()withWNOHANG옵션)하여 좀비를 즉시 정리해야 합니다. SIGCHLD시그널 핸들링:SIGCHLD시그널이 발생했을 때wait()를 호출하는 시그널 핸들러를 등록하여, 자식이 종료될 때마다 자동으로 정리되도록 구현하는 것이 가장 이상적인 방법입니다.- 부모 프로세스 종료: 좀비 프로세스는
kill명령어로 직접 제거할 수 없습니다(이미 죽어있기 때문). 좀비를 제거하는 유일한 방법은 부모 프로세스가wait()를 호출하게 하거나, 부모 프로세스를 종료시키는 것입니다. 부모가 종료되면 좀비는 고아 프로세스가 되어 아래에서 설명할init프로세스에 의해 처리됩니다.
- 명시적인
3. 고아 프로세스 (Orphan Process)
3.1. 정의 및 발생 원인
고아 프로세스는 부모 프로세스가 자식 프로세스보다 먼저 종료된 경우, 해당 자식 프로세스를 지칭합니다. 부모를 잃었기 때문에 '고아'라고 불립니다. 고아 프로세스는 좀비와 달리 여전히 실행 중인 정상적인 프로세스입니다.
3.2. 내부 구조 및 커널에서의 처리 (Reparenting)
부모-자식 관계는 시스템의 프로세스 계층 구조를 유지하는 데 중요합니다. 만약 어떤 프로세스의 부모가 사라진다면, 해당 프로세스는 종료되었을 때 아무도 거두어 갈 수 없는 상태가 됩니다. 이는 결국 영원히 좀비로 남게 되는 문제를 야기합니다.
이러한 문제를 방지하기 위해, 커널은 재입양(Reparenting) 메커니즘을 사용합니다.
- 프로세스가 종료될 때, 커널(
release_task()함수 내부)은 해당 프로세스에게 자식이 있는지 확인합니다. - 만약 자식 프로세스들이 있다면, 커널은 이 자식들의 부모를 새로운 프로세스로 지정합니다. 이 새로운 부모가 바로
init프로세스입니다.init프로세스는 시스템이 부팅될 때 가장 먼저 생성되는 프로세스로, PID 1번을 갖습니다. 모든 프로세스의 시조(始祖)이며, 시스템이 종료될 때까지 항상 실행됩니다.- 최신 Linux 시스템에서는
systemd가 PID 1번으로 동작하며init의 역할을 수행합니다.
init프로세스는 자신의 주된 역할 중 하나로, 주기적으로wait()를 호출하여 자신이 입양한 고아 프로세스들이 종료될 때마다 즉시 거두어주는(reaping) 역할을 수행합니다.- Linux 3.4부터는 서브리퍼(Subreaper) 개념이 도입되었습니다. 특정 프로세스가
prctl(PR_SET_CHILD_SUBREAPER, 1)을 통해 스스로를 서브리퍼로 지정할 수 있습니다. 이렇게 되면 해당 프로세스의 자손들 중에서 발생하는 고아 프로세스는init대신 이 서브리퍼에게 입양됩니다. 이는 데몬 관리나 컨테이너 환경에서 유용하게 사용됩니다.
따라서 고아 프로세스는 시스템에 의해 자동으로 관리되므로, 일반적으로는 문제가 되지 않습니다.
4. 고아 프로세스 vs 좀비 프로세스 비교
| 구분 | 고아 프로세스 (Orphan Process) | 좀비 프로세스 (Zombie Process) |
|---|---|---|
| 정의 | 부모가 먼저 종료되어 init에 입양된, 실행 중인 프로세스 |
실행은 종료되었으나 부모가 거두지 않아 프로세스 테이블에 남아있는 프로세스 |
| 상태 | 실행 중 (Running, Sleeping 등) | 종료됨 (Zombie, Z) |
| 부모 프로세스 | 원래 부모는 종료됨. init (PID 1) 또는 서브리퍼가 새 부모가 됨. |
원래 부모가 여전히 실행 중이며, wait()를 호출하지 않고 있음. |
| 시스템에 미치는 영향 | 거의 없음. init이 안정적으로 관리하므로 문제가 되지 않음. |
PID 고갈, 커널 메모리 누수 등 시스템에 문제를 일으킬 수 있음. |
| 해결 주체 | 운영체제 커널과 init 프로세스 |
부모 프로세스 프로그래머 |
| 강제 종료 가능 여부 | 가능 (kill 명령어로 종료 가능) |
불가능 (이미 종료된 상태이므로 kill 신호를 받지 않음) |
5. 동작 계층 구조
- 사용자 영역 (Application):
- 프로그램 A가
fork()를 호출하여 자식 B를 생성합니다. - 고아 발생: A가
wait()호출 없이 먼저exit()하고 종료됩니다. - 좀비 발생: B가 먼저
exit()하고 종료되지만, A는wait()를 호출하지 않고 다른 작업을 계속합니다.
- 프로그램 A가
- C 라이브러리 (glibc):
fork(),exit(),wait()등의 함수는 내부적으로 시스템 콜을 호출하는 래퍼(wrapper)입니다.
- 커널 영역 (Kernel - System Call Interface):
sys_fork():task_struct를 복제하여 부모-자식 관계를 설정합니다.sys_exit()(do_exit()):- 프로세스 리소스를 해제하고 상태를
TASK_ZOMBIE로 바꿉니다. - 부모에게
SIGCHLD시그널을 보냅니다. - 만약 현재 종료하는 프로세스(A)에게 자식(B)이 있다면,
forget_original_parent()->find_new_parent()를 통해 자식 B의 부모를init으로 재지정(reparenting)합니다.
- 프로세스 리소스를 해제하고 상태를
sys_wait()(do_wait()):- 부모 A가 호출하면, 커널은 A의 자식 중 좀비 상태인 B를 찾습니다.
- B의
task_struct에서 종료 상태와 자원 사용량 정보를 꺼내 A에게 전달합니다. - B의
task_struct를 완전히 해제하고 PID를 반환합니다.
- init 프로세스 (User Space, PID 1):
init프로세스는 메인 루프에서 항상 대기하며, 자신이 입양한 고아 프로세스가 종료되어 좀비가 되면wait()를 호출하여 즉시 정리해줍니다.
6. 요약
좀비 프로세스는 일을 모두 마친 자식 프로세스가 부모에게 결과를 보고하려고 기다리는데, 부모가 그 보고를 받지 않아서 프로세스 목록에 이름만 남아있는 상태를 말합니다. 자원은 거의 안 쓰지만, 목록을 차지해서 많아지면 시스템에 문제를 일으킬 수 있습니다. 프로그래머가 부모 프로세스에서 자식의 종료를 잘 처리해줘야 해결됩니다.
반면에 고아 프로세스는 자식이 아직 실행 중인데 부모 프로세스가 먼저 사라져버린 경우입니다. 이 때 운영체제는 이 고아 프로세스를 'init'이라는 시스템 최초의 프로세스에게 입양시킵니다. 그래서 고아 프로세스는 새로운 부모인 'init'의 보살핌을 받게 되므로, 실행이 끝나면 'init'이 깔끔하게 뒤처리를 해줍니다. 따라서 고아 프로세스는 보통 시스템에 문제를 일으키지 않습니다.
결론적으로, 좀비는 부모가 돌보지 않아 생기는 문제이고, 고아는 부모를 잃었지만 시스템이 새로운 부모를 찾아주어 문제가 해결되는 상황이라고 할 수 있습니다.
'Computer Science' 카테고리의 다른 글
| 단위 벡터 (0) | 2025.12.09 |
|---|---|
| Array of Structure (AoS) (0) | 2025.12.02 |
| Structure of Array (SoA) (0) | 2025.12.02 |
| 해시 충돌 Hash Collision (0) | 2025.11.25 |
| 뮤텍스 Mutex (1) | 2025.11.25 |
