좀비 사냥 (Reaping Zombies)
Ⅰ. 개요
1. 정의
**좀비 프로세스(Zombie Process)**는 자식 프로세스가 종료되었으나 부모 프로세스가 아직 wait() 계열 시스템 콜로 종료 상태를 수집하지 않아, 프로세스 테이블 엔트리(PID 및 종료 정보)가 해제되지 않고 남아있는 상태이다. 좀비 사냥(Reaping)은 이 좀비 프로세스를 정리하는 과정이다.
좀비 프로세스 발생 과정
┌─────────────────────────────────────────────────────┐
│ │
│ 1. 자식 프로세스 실행 중 │
│ ┌──────────┐ 생성 ┌──────────┐ │
│ │ 부모(PID │ ──────→ │ 자식(PID │ │
│ │ =100) │ │ =101) │ │
│ └──────────┘ └──────────┘ │
│ │
│ 2. 자식 종료 (exit(0)) │
│ ┌──────────┐ ┌──────────┐ │
│ │ 부모 │ │ 좀비 │ │
│ │ PID=100 │ │ PID=101 │ │
│ │ │ │ Z 상태 │ │
│ │ │ │ 메모리 X │ ← 메모리는 반납│
│ │ │ │ PID만 점유│ ← PID는 남아있음│
│ └──────────┘ └──────────┘ │
│ │ │
│ │ 부모가 wait() 호출 안 함! │
│ │ │
│ 3. 부모가 wait() 호출 → 좀비 수거 (Reap) │
│ ┌──────────┐ ┌──────────┐ │
│ │ 부모 │ │ (해제됨) │ │
│ │ PID=100 │ │ PID=101 │ │
│ │ │ │ 재사용 │ │
│ └──────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────┘
비유: 좀비 프로세스는 "집에서 죽은 사람의 주민등록증"과 같다. 사람은 이미 떠났지만(메모리 반납), 등록증(PID)은 여전히 책상 위에 남아 자리를 차지하고 있다. 부모가 사망 확인서(wait)를 받아야만 등록증을 폐기할 수 있다.
Ⅱ. 좀비 프로세스 수거 방법
1. wait() 시스템 콜
pid_t wait(int *status);
// 임의의 자식이 종료될 때까지 블로킹
pid_t waitpid(pid_t pid, int *status, int options);
// 특정 자식을 기다리거나 비블로킹 모드 가능
wait() 동작
┌─────────────────────────────────────────────────────┐
│ │
│ 부모 프로세스 │
│ │ │
│ ├── fork() ──→ 자식 생성 (PID=101) │
│ │ │
│ ▼ │
│ wait(&status); ←── 블로킹 (자식이 종료될 때까지) │
│ │ │
│ │ ... 자식이 exit() 호출 ... │
│ │ │
│ ▼ │
│ wait() 반환 ──→ status에 종료 정보 저장 │
│ PID=101 좀비 해제 │
│ │
│ ★ 블로킹이므로 부모가 다른 작업 불가 │
└─────────────────────────────────────────────────────┘
2. waitpid()의 옵션
// 비블로킹으로 자식 상태 확인
waitpid(-1, &status, WNOHANG);
// 특정 자식만 대기
waitpid(child_pid, &status, 0);
// 모든 자식 중 임의의 것 대기
waitpid(-1, &status, WNOHANG);
// stopped 자식도 수거
waitpid(-1, &status, WUNTRACED | WCONTINUED);
3. SIGCHLD 시그널 핸들러
SIGCHLD 기반 비동기 수거
┌─────────────────────────────────────────────────────┐
│ │
│ 부모 프로세스: │
│ ┌─────────────────────────────────────┐ │
│ │ signal(SIGCHLD, sigchld_handler); │ │
│ │ │ │
│ │ while (1) { │ │
│ │ // 부모의 주 작업 계속... │ │
│ │ } │ │
│ └─────────────────────────────────────┘ │
│ │
│ 자식이 exit() 시: │
│ ┌─────────────────────────────────────┐ │
│ │ sigchld_handler() { │ │
│ │ while (waitpid(-1, &status, │ │
│ │ WNOHANG) > 0) { │ │
│ │ // 좀비 수거 완료 │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────┘ │
│ │
│ ★ 비블로킹: 부모는 주 작업을 계속하면서 │
│ 시그널 수신 시 좀비를 수거 │
│ ★ WNOHANG 루프: 여러 자식이 동시에 종료된 경우 │
│ 모두 수거 보장 │
└─────────────────────────────────────────────────────┘
Ⅲ. 고급 좀비 방지 기법
1. 더블 포크 (Double Fork) 기법
더블 포크로 좀비 완전 방지
┌─────────────────────────────────────────────────────┐
│ │
│ A: 부모(고조부) PID=100 │
│ │ │
│ ├── fork() ──→ B: 중간 자식 PID=101 │
│ │ │ │
│ │ ├── fork() ──→ C: 실제 일꾼 │
│ │ │ PID=102 │
│ │ │ │
│ ▼ ▼ │
│ A: wait(B) B: exit(0) │
│ → B 수거 완료 → B는 좀비가 되지 않음 │
│ │
│ C: PPID=1 (init/systemd) │
│ → C가 종료되면 init가 자동 수거! │
│ → 고조부 A는 좀비를 볼 일이 없음 │
│ │
│ 적용 예: │
│ - 네트워크 서버의 자식 프로세스 생성 │
│ - 데몬에서 일회성 작업 프로세스 실행 │
│ - cron 작업에서 백그라운드 프로세스 실행 │
└─────────────────────────────────────────────────────┘
2. PR_SET_CHILD_SUBREAPER
subreaper를 이용한 좀비 수거 위임
┌─────────────────────────────────────────────────────┐
│ │
│ prctl(PR_SET_CHILD_SUBREAPER, 1); │
│ │
│ 설정 전: │
│ A ──→ B ──→ C (C 종료 시 → init이 수거) │
│ │
│ 설정 후 (B에 subreaper 설정): │
│ A ──→ B (subreaper) ──→ C │
│ (C 종료 시 → B가 수거, init 아님) │
│ │
│ 장점: │
│ - init/systemd에 의존하지 않고 │
│ 특정 프로세스가 모든 후손의 좀비를 수거 │
│ - 컨테이너 환경에서 PID=1이 아닌 │
│ 프로세스가 좀비 수거 담당 가능 │
│ │
│ Docker의 --init 옵션이 이 기능을 사용 │
└─────────────────────────────────────────────────────┘
3. SIG_IGN으로 좀비 방지
// 자식 종료 시 시그널 무시 (자동 수거)
signal(SIGCHLD, SIG_IGN);
// 결과: 자식이 종료되면 커널이 즉시 좀비를 해제
// 주의: 종료 상태(status)를 알 수 없음
Ⅳ. 좀비 프로세스의 위험성
1. PID 고갈 문제
PID 고갈 시나리오
┌─────────────────────────────────────────────────────┐
│ │
│ PID 공간: 1 ~ 32768 (기본, pid_max 설정 가능) │
│ │
│ 좀비가 계속 쌓이면: │
│ ┌─────────────────────────────────────────┐ │
│ │ PID 1000: Running │ │
│ │ PID 1001: Zombie ← 사용 불가 │ │
│ │ PID 1002: Zombie ← 사용 불가 │ │
│ │ ... │ │
│ │ PID 32768: Zombie ← 사용 불가 │ │
│ │ │ │
│ │ ★ 새 프로세스 생성 불가! (EAGAIN) │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 확인: ps aux | grep Z │
│ 해결: 부모 프로세스 재시작 또는 kill -9 부모 │
│ (부모가 죽으면 init가 고아 + 좀비 수거) │
└─────────────────────────────────────────────────────┘
2. 프로세스 상태별 비교
┌──────────────────┬──────────────┬──────────────┬──────────┐
│ 상태 │ 좀비 (Z) │ 고아 (O) │ 정상 (R) │
├──────────────────┼──────────────┼──────────────┼──────────┤
│ 메모리 점유 │ X │ O │ O │
│ CPU 점유 │ X │ O │ O │
│ PID 테이블 점유 │ O │ O │ O │
│ 부모 │ 존재 │ init/systemd │ 존재 │
│ 해결 방법 │ wait() 호출 │ 자동 정상 │ - │
│ 위험도 │ 높음 (PID) │ 낮음 │ - │
└──────────────────┴──────────────┴──────────────┴──────────┘
Ⅴ. 지식 그래프 및 요약
1. 지식 그래프
[좀비 사냥 Reaping Zombies]
├── [좀비 프로세스]
│ ├── 발생 원인 ─── 자식 종료 후 부모가 wait() 미호출
│ ├── 특징 ──────── 메모리는 반납, PID만 점유
│ └── 위험성 ────── PID 고갈 → 프로세스 생성 불가
├── [수거 방법]
│ ├── wait() ────── 블로킹, 임의 자식 대기
│ ├── waitpid() ─── 비블로킹(WNOHANG), 특정 자식 지정
│ ├── SIGCHLD ────── 비동기 시그널 기반 수거
│ └── SIG_IGN ────── 커널 자동 수거 (상태 불가)
├── [방지 기법]
│ ├── Double Fork ─── init가 좀비 수거 위임
│ ├── PR_SET_CHILD_SUBREAPER ── 특정 프로세스가 수거
│ └── 부모 올바른 종료 핸들링
├── [진단]
│ ├── ps aux | grep Z
│ ├── ps -eo pid,ppid,stat,cmd | grep Z
│ └── top / htop 상태 확인
└── [응급 조치]
├── kill -9 부모_PID (부모 강제 종료)
└── 부모 재시작 → init가 고아 좀비 자동 수거
2. 준말
- PPID: Parent Process ID (부모 프로세스 식별자)
- WNOHANG: Wait No Hang (비블로킹 대기 옵션)
3. 어린이를 위한 3줄 설명
좀비 프로세스는 일을 마치고 나간 자식의 "이름표(PID)"가 남아서 자리만 차지하고 있는 상태예요. 부모가 wait()라는 확인서를 받아야만 이름표를 정리할 수 있어요. 이름표가 너무 많이 쌓이면 새로운 프로세스를 만들 수 없게 되는 큰 문제가 생겨요.