좀비 사냥 (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()라는 확인서를 받아야만 이름표를 정리할 수 있어요. 이름표가 너무 많이 쌓이면 새로운 프로세스를 만들 수 없게 되는 큰 문제가 생겨요.