QUESTION
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
Structure diagram
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
Call path
Termination and reaping are not the same event. Record separately when a process stops executing and when the parent consumes its termination information.
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.
SOURCE COORDINATES
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.
| File | Function / structure | What 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 PROGRAM
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.
cc -std=c17 -Wall -Wextra -O2 pidfd_wait.c -o pidfd_wait01#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
Code notes
_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().
SYS_pidfd_openThis example invokes the raw syscall regardless of glibc-wrapper availability. A successful fd refers to this process instance.
.events = POLLINA pidfd becomes readable when the process exits, so the notification can be integrated into an epoll-based supervisor.
waitid(P_PIDFDCollects the state of the child referred to by the same pidfd without looking up the numeric PID again.
close(pfd)Waiting and closing the pidfd are separate. wait collects child status; close drops the fd reference to the pid object.
DETAILS
Detailed behavior
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.
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.
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
Objects and lifetimes
| Object | Creation and release | Values to inspect |
|---|---|---|
exit status | Recorded by do_exit and retained until wait consumes it | exit_code, si_code, rusage |
struct pid | Connects a PID number to a task reference; a pidfd extends its lifetime | Per-namespace upid and reference count |
pidfd | Created by pidfd_open/clone3; its fd reference disappears on close | poll readiness, target identity |
FAILURE PATH
Failure conditions and common misconceptions
| Observed symptom | Likely causes | How to verify |
|---|---|---|
| Zombies accumulate | The parent does not wait | ps state Z, /proc/PID/status |
| kill may be delivered to the wrong process | A lookup occurs across PID reuse | Consider pidfd_send_signal |
| ECHILD | The target is not a child of the caller, or it has already been reaped | Check the parent relationship and wait options |
LAB
Verify it yourself
- Sleep after child exit but before wait, then use ps -o pid,ppid,state,cmd to observe state Z.
- Add WNOWAIT to waitid so the status is read but not reaped, then inspect it again with a second wait.
- Register several child pidfds with epoll and observe how handling works when termination order differs from creation order.
./pidfd_waitstrace -f -e trace=clone,pidfd_open,poll,waitid,exit_group ./pidfd_waitPRIMARY REFERENCES