zombie와 orphan 프로세스 완전 정복
리눅스 zombie 프로세스와 orphan 프로세스의 생성 원인, 차이점, zombie 제거 방법, SIGCHLD 핸들러와 double fork 패턴을 설명합니다.
지난 글에서 fork(), exec(), wait() 시스템 콜을 다뤘습니다. 부모가 wait()를 호출하지 않으면 자식이 zombie가 된다고 잠깐 언급했는데, 이번엔 zombie와 orphan을 각각 깊이 파헤칩니다. ps aux에서 Z 상태가 쌓이기 시작하면 이 글이 필요한 순간입니다.
Zombie 프로세스란?
자식 프로세스가 exit()를 호출하면 커널은 해당 프로세스의 종료 코드를 프로세스 테이블에 보관합니다. 부모가 wait()로 이 코드를 수거하기 전까지 프로세스는 이미 실행이 멈췄지만 테이블 엔트리는 살아 있습니다. 이 상태가 zombie(defunct) 입니다.
# zombie 확인: STAT 컬럼이 Z 또는 Z+
ps aux | awk '$8 ~ /^Z/ {print $1,$2,$8,$11}'
# pstree로 zombie 탐색
pstree -p | grep defunct
Zombie의 영향
zombie 자체는 CPU나 메모리를 소비하지 않습니다. 하지만 PID를 점유하므로, 장기 실행 서버에서 zombie가 계속 쌓이면 /proc/sys/kernel/pid_max(기본 32768) 한도에 달해 새 프로세스를 생성할 수 없게 됩니다.
# 현재 zombie 수 확인
ps aux | grep -c ' Z '
# PID 최대값 확인
cat /proc/sys/kernel/pid_max
Orphan 프로세스란?
부모가 자식보다 먼저 종료되면 자식은 orphan(고아) 이 됩니다. 커널은 orphan을 자동으로 init(PID 1, systemd) 에 입양시킵니다. init은 항상 wait()를 수행하므로 orphan이 zombie가 되는 일은 없습니다.
# orphan 확인: PPID가 1인 프로세스
ps -o pid,ppid,comm ax | awk '$2==1 && $3!="init" && $3!="systemd"'
Zombie 제거하기
방법 1: 부모에 SIGCHLD 보내기
부모가 살아 있고 SIGCHLD 핸들러가 없거나 잘못 구현된 경우, 시그널을 수동으로 보내면 핸들러가 트리거됩니다.
# zombie의 부모 PID 찾기
PARENT=$(ps -o ppid= -p ZOMBIE_PID | tr -d ' ')
# SIGCHLD 전송
kill -SIGCHLD $PARENT
방법 2: 부모 재시작 또는 종료
부모가 wait()를 전혀 구현하지 않았다면, 부모를 종료하는 것이 가장 확실합니다. 부모가 죽으면 zombie도 init에 입양되어 즉시 회수됩니다.
kill -9 $PARENT
방법 3: SIG_IGN 설정 (코드 수정)
C 코드에서 SIGCHLD를 SIG_IGN으로 설정하면 커널이 자식을 자동으로 즉시 소멸시킵니다. POSIX가 보장하는 동작입니다.
signal(SIGCHLD, SIG_IGN);
/* 이후 fork()된 자식은 즉시 회수됨 */
Zombie 방지 패턴
SIGCHLD 핸들러 (권장)
#include <signal.h>
#include <sys/wait.h>
void sigchld_handler(int sig) {
(void)sig;
/* -1: 모든 자식, WNOHANG: 비블로킹 */
while (waitpid(-1, NULL, WNOHANG) > 0);
}
int main(void) {
struct sigaction sa = {.sa_handler = sigchld_handler,
.sa_flags = SA_RESTART | SA_NOCLDSTOP};
sigaction(SIGCHLD, &sa, NULL);
/* ... */
}
SA_NOCLDSTOP 플래그를 지정하면 자식이 멈출 때(SIGSTOP)는 핸들러가 호출되지 않고, 실제 종료 시에만 호출됩니다.
Double Fork (데몬 프로세스)
데몬처럼 부모와 완전히 분리된 프로세스가 필요할 때 사용합니다. 중간 부모가 즉시 종료하면 손자가 init에게 입양되어 zombie가 될 수 없습니다.
# bash에서 double fork 흉내
(setsid bash -c 'sleep 100' &)
# 외부 쉘이 중간 부모로 즉시 종료
# sleep 100은 init 자식이 됨
실전 진단 예시
# 다음 명령으로 zombie 모니터링
watch -n 2 'ps aux | awk "NR==1 || \$8~/^Z/"'
# /proc으로 상세 정보
cat /proc/ZOMBIE_PID/status | grep -E 'State|PPid|Threads'
정리
zombie는 부모가 wait()를 호출하지 않아 발생하며, PID를 소모하지만 자원을 쓰지는 않습니다. orphan은 부모가 먼저 죽어 init에 입양된 자식으로, 문제가 없습니다. zombie를 근본적으로 막으려면 SIGCHLD 핸들러 안에서 waitpid(-1, NULL, WNOHANG)을 루프로 돌리거나, SIGCHLD를 SIG_IGN으로 설정하면 됩니다. 다음 글에서는 프로세스 자원 제한의 핵심 기술인 cgroups를 살펴봅니다.
지난 글: fork / exec / wait 완전 이해
다음 글: cgroups 완전 개요 — 자원 격리의 뼈대
읽어주셔서 감사합니다. 😊