← System Programming DUJINLABS.COM

Process · Linux userspace / kernel ABI

waitpid, zombie, pidfd

child 종료 상태가 zombie로 보존되는 이유와 PID 재사용 경쟁을 pidfd가 어떻게 줄이는지 설명합니다.

Series
08 / 37
Build
cc -std=c17 -Wall -Wextra -O2 pidfd_wait.c -o pidfd_wait
Run
./pidfd_wait
Kernel
Linux 6.18.37 LTS

child가 종료됐는데도 parent가 wait해야 하는 이유는 무엇인가?

child가 exit하면 실행 자원 대부분은 정리되지만 parent가 읽어야 할 exit status와 resource usage는 task의 최소 정보로 남는다. parent가 wait 계열 호출로 이를 소비해야 PID와 task slot이 완전히 회수된다.

숫자 PID는 시간이 지나면 재사용된다. pidfd는 특정 process instance를 가리키는 file descriptor라 poll 가능한 종료 알림과 signal 전송에서 PID lookup 경쟁을 줄인다.

구조 그림

그림 1. parent가 보유하는 child 종료 정보와 pidfd 참조
Parent taskchild list · wait queue · pidfd slot

Running child

  • task_struct
  • mm/files
  • PID 5301

Zombie record

  • EXIT_ZOMBIE
  • exit_code=42
  • rusage 보존

pidfd

  • struct pid ref
  • poll readable
  • waitid(P_PIDFD)

After wait

  • status 소비
  • release_task
  • PID 재사용 가능

child 실행이 끝나도 exit status는 parent가 wait로 소비할 때까지 남는다. pidfd는 숫자 PID가 재사용돼도 같은 process instance를 가리킨다.

호출 흐름

그림 2. 사용자 코드에서 관찰 가능한 결과까지
fork child PID/pidfd 확보
child exit exit_code 기록
zombie parent 통지
waitid status 소비
release_task 최종 회수

종료와 회수는 같은 사건이 아니다. process가 더 이상 실행되지 않는 시점과 parent가 종료 정보를 소비하는 시점을 따로 기록한다.

그림 3. 커널 내부에서 지나가는 주요 지점
do_exit resource 정리
exit_notify parent/SIGCHLD
EXIT_ZOMBIE status 보존
do_wait matching child
release_task pid/task 해제

함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.

Linux 6.18.37 LTS 소스 위치

glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.

파일함수·구조체여기서 볼 것
kernel/exit.c do_exit(), exit_notify() 종료 자원 정리와 zombie 상태 설정
kernel/exit.c do_wait(), wait_task_zombie() parent가 종료 정보를 소비하는 경로
kernel/pid.c pidfd_open(), pidfd_send_signal() PID 대신 stable handle을 얻고 사용하는 경로

실행 예제 원본

아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.

빌드cc -std=c17 -Wall -Wextra -O2 pidfd_wait.c -o pidfd_wait
01#define _GNU_SOURCE
02#include <poll.h>
03#include <stdio.h>
04#include <stdlib.h>
05#include <sys/syscall.h>
06#include <sys/wait.h>
07#include <unistd.h>
08
09int main(void)
10{
11    pid_t pid = fork();
12    if (pid < 0)
13        return 1;
14    if (pid == 0)
15        _exit(42);
16
17    int pfd = (int)syscall(SYS_pidfd_open, pid, 0);
18    if (pfd < 0) {
19        perror("pidfd_open");
20        return 1;
21    }
22
23    struct pollfd event = { .fd = pfd, .events = POLLIN };
24    if (poll(&event, 1, -1) < 0)
25        return 1;
26
27    siginfo_t info;
28    if (waitid(P_PIDFD, (id_t)pfd, &info, WEXITED) < 0)
29        return 1;
30    printf("pid=%ld status=%d\n", (long)info.si_pid, info.si_status);
31    close(pfd);
32    return 0;
33}

코드 조각별 설명

실제 코드 15행_exit(42)

child의 exit status 하위 8 bit에 42를 기록한다. stdio 상태를 상속받은 child라서 exit() 대신 _exit()를 쓴다.

실제 코드 17행SYS_pidfd_open

glibc wrapper 가용성과 무관하게 raw syscall을 호출하는 예다. 성공한 fd는 이 process instance를 참조한다.

실제 코드 23행.events = POLLIN

pidfd는 process가 종료되면 readable event를 제공해 epoll 기반 supervisor에도 합칠 수 있다.

실제 코드 28행waitid(P_PIDFD

숫자 PID를 다시 lookup하지 않고 같은 pidfd가 가리키는 child의 상태를 회수한다.

실제 코드 31행close(pfd)

wait와 pidfd close는 별개다. wait가 child status를 회수하고 close가 pid object에 대한 fd 참조를 놓는다.

세부 동작

01

SIGCHLD는 알림이고 wait는 회수다

SIGCHLD handler가 실행됐다고 child가 자동 회수되는 것은 아니다. handler에서는 waitpid(-1, ..., WNOHANG)를 더 이상 대상이 없을 때까지 반복하거나 signalfd/event loop에서 같은 작업을 한다.

SIGCHLD를 SIG_IGN으로 명시하거나 SA_NOCLDWAIT를 사용하면 Linux는 zombie를 남기지 않는 정책을 적용할 수 있다.

02

PID lookup에는 시간 차이가 있다

kill(pid), stat(/proc/pid), waitpid 사이에 target이 종료되고 같은 숫자가 다른 process에 배정될 수 있다. namespace가 끼면 보이는 PID도 context마다 다르다.

pidfd_open을 process 발견 직후 수행하거나 clone3의 CLONE_PIDFD로 생성과 handle 획득을 원자적으로 묶는다.

03

subreaper와 init의 역할

parent가 먼저 종료한 orphan은 가장 가까운 child subreaper 또는 PID namespace init으로 reparent된다. supervisor는 PR_SET_CHILD_SUBREAPER로 손자 process의 종료도 회수할 수 있다.

container의 PID 1이 wait loop를 구현하지 않으면 zombie가 누적되는 이유다.

객체와 수명

대상언제 생기고 없어지는가확인할 값
exit statusdo_exit에서 기록되고 wait가 소비할 때까지 보존된다exit_code, si_code, rusage
struct pidPID 번호와 task 참조를 연결하며 pidfd가 수명을 연장한다namespace별 upid, refcount
pidfdpidfd_open/clone3에서 생기고 close 때 fd 참조가 사라진다poll readiness, target identity

실패 조건과 오해하기 쉬운 부분

겉으로 보이는 현상실제 원인 후보확인 방법
zombie 누적parent가 wait하지 않음ps state Z, /proc/PID/status
kill이 엉뚱한 process에 전달될 위험PID 재사용 사이의 lookuppidfd_send_signal 사용 검토
ECHILD호출 process의 child가 아니거나 이미 회수parent 관계와 wait option 확인

직접 확인

  1. child exit 뒤 wait 전에 sleep을 넣고 ps -o pid,ppid,state,cmd로 Z 상태를 확인한다.
  2. waitid에 WNOWAIT를 추가해 상태를 읽되 회수하지 않고 두 번째 wait에서 다시 확인한다.
  3. 여러 child pidfd를 epoll에 등록해 종료 순서와 생성 순서가 다를 때 처리 방식을 확인한다.
실행./pidfd_wait
추적strace -f -e trace=clone,pidfd_open,poll,waitid,exit_group ./pidfd_wait

원문