Process · Linux userspace / kernel ABI

waitpid, zombie, pidfd

Explains why a child's termination status is retained as a zombie and how pidfds reduce PID-reuse races.

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

Why must a parent still wait after its child has exited?

When a child exits, most execution resources are released, but the exit status and resource usage that the parent must read remain as minimal task information. The PID and task slot are fully reclaimed only after the parent consumes them with a wait-family call.

A numeric PID can be reused over time. A pidfd is a file descriptor that refers to one process instance, reducing PID-lookup races for pollable termination notifications and signal delivery.

Structure diagram

Figure 1. Child termination information and pidfd references held by the parent
Parent taskchild list · wait queue · pidfd slot

Running child

  • task_struct
  • mm/files
  • PID 5301

Zombie record

  • EXIT_ZOMBIE
  • exit_code=42
  • retain rusage

pidfd

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

After wait

  • consume status
  • release_task
  • PID can be reused

Even after the child stops executing, exit status remains until the parent consumes it with wait. A pidfd refers to the same process instance even if its numeric PID is reused.

Call path

Figure 2. From userspace code to observable results
fork obtain child PID/pidfd
child exit record exit_code
zombie notify parent
waitid consume status
release_task final reclamation

Termination and reaping are not the same event. Record separately when a process stops executing and when the parent consumes its termination information.

Figure 3. Major points along the kernel-internal path
do_exit release resources
exit_notify parent/SIGCHLD
EXIT_ZOMBIE retain status
do_wait matching child
release_task release pid/task

This diagram is not for memorizing function names. Follow which return value, file descriptor, memory mapping, or wait queue is passed to the next stage.

Linux 6.18.37 LTS source locations

Go beyond the glibc function to the files where the syscall implementation meets kernel objects. Each link points to the original file at the same tag.

FileFunction / structureWhat to inspect
kernel/exit.c do_exit(), exit_notify() Release termination resources and set zombie state
kernel/exit.c do_wait(), wait_task_zombie() The path through which the parent consumes termination information
kernel/pid.c pidfd_open(), pidfd_send_signal() The path that obtains and uses a stable handle instead of a PID

Complete runnable example

The code below is not pseudocode with explanatory lines omitted. It is a minimal example that can be built and run as a file.

Buildcc -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}

Code notes

Source line 15_exit(42)

Records 42 in the low 8 bits of the child's exit status. Because the child inherited stdio state, it uses _exit() rather than exit().

Source line 17SYS_pidfd_open

This example invokes the raw syscall regardless of glibc-wrapper availability. A successful fd refers to this process instance.

Source line 23.events = POLLIN

A pidfd becomes readable when the process exits, so the notification can be integrated into an epoll-based supervisor.

Source line 28waitid(P_PIDFD

Collects the state of the child referred to by the same pidfd without looking up the numeric PID again.

Source line 31close(pfd)

Waiting and closing the pidfd are separate. wait collects child status; close drops the fd reference to the pid object.

Detailed behavior

01

SIGCHLD is notification; wait is reclamation

Running a SIGCHLD handler does not automatically reap the child. The handler must repeat waitpid(-1, ..., WNOHANG) until no targets remain, or an event loop must perform the same operation through signalfd.

If SIGCHLD is explicitly set to SIG_IGN or SA_NOCLDWAIT is used, Linux can apply a policy that leaves no zombie.

02

PID lookup has a time gap

Between kill(pid), stat(/proc/pid), and waitpid, the target can exit and the same number can be assigned to another process. When namespaces are involved, the visible PID also differs by context.

Call pidfd_open immediately after discovering a process, or use CLONE_PIDFD with clone3 to make process creation and handle acquisition atomic.

03

The roles of subreapers and init

An orphan whose parent exits first is reparented to the nearest child subreaper or to the PID namespace's init. A supervisor can use PR_SET_CHILD_SUBREAPER to reap descendant processes as well.

This is why zombies accumulate when PID 1 in a container does not implement a wait loop.

Objects and lifetimes

ObjectCreation and releaseValues to inspect
exit statusRecorded by do_exit and retained until wait consumes itexit_code, si_code, rusage
struct pidConnects a PID number to a task reference; a pidfd extends its lifetimePer-namespace upid and reference count
pidfdCreated by pidfd_open/clone3; its fd reference disappears on closepoll readiness, target identity

Failure conditions and common misconceptions

Observed symptomLikely causesHow to verify
Zombies accumulateThe parent does not waitps state Z, /proc/PID/status
kill may be delivered to the wrong processA lookup occurs across PID reuseConsider pidfd_send_signal
ECHILDThe target is not a child of the caller, or it has already been reapedCheck the parent relationship and wait options

Verify it yourself

  1. Sleep after child exit but before wait, then use ps -o pid,ppid,state,cmd to observe state Z.
  2. Add WNOWAIT to waitid so the status is read but not reaped, then inspect it again with a second wait.
  3. Register several child pidfds with epoll and observe how handling works when termination order differs from creation order.
Run./pidfd_wait
Tracestrace -f -e trace=clone,pidfd_open,poll,waitid,exit_group ./pidfd_wait

Primary sources