Linux System Programming · intermediate · ~10 min

wait and waitpid

Collect a child's exit status.

Overview

wait(&status) and waitpid(pid, &status, opts) pause the parent process until one of its child processes finishes. They return the child's PID and its exit status.

If the parent never calls wait, finished children stick around as zombies. A zombie is an entry in the kernel's process table that holds no memory of its own. It keeps only the child's exit code, waiting for the parent to collect it.

Why it matters

In a program that uses fork, the parent must collect each child's exit code. If it does not, zombies build up.

This matters most for long-running servers. Imagine a server that spawns hundreds of subprocesses an hour. Each missed wait leaves one more zombie behind.

Over time these entries fill the kernel's process table. Once it is full, the system can no longer create new processes, and fork starts to fail.

Core concepts

Status macros

The status value packs several pieces of information into one integer. You read it with macros:

  • WIFEXITED(status) is true if the child exited normally.
  • WEXITSTATUS(status) extracts the exit code (only valid after WIFEXITED).
  • WIFSIGNALED(status) is true if a signal killed the child.
  • WTERMSIG(status) gives the signal number that killed it.

waitpid flags

  • WNOHANG makes the call non-blocking. If no child has exited yet, waitpid returns 0 immediately instead of waiting.

SIGCHLD

When a child exits, the kernel sends the parent a SIGCHLD signal. You can install a handler for it that calls waitpid in a loop, reaping every child that has finished.

Syntax notes

int status;
pid_t kid = waitpid(child, &status, 0);
if (WIFEXITED(status)) printf("exited with %d\n", WEXITSTATUS(status));
else if (WIFSIGNALED(status)) printf("killed by signal %d\n", WTERMSIG(status));

Lesson

When a child exits, the kernel keeps a small record of it (the zombie) until the parent calls wait or waitpid.

The signature is:

int waitpid(pid_t pid, int *status, int options);

It blocks until the chosen child terminates, or polls without blocking when you pass WNOHANG.

Decode the returned status like this:

  • Normal exit: WIFEXITED then WEXITSTATUS.
  • Killed by a signal: WIFSIGNALED then WTERMSIG.

Code examples

int st;
waitpid(pid, &st, 0);
if (WIFEXITED(st))   printf("exit %d\n", WEXITSTATUS(st));
if (WIFSIGNALED(st)) printf("killed by signal %d\n", WTERMSIG(st));

Common mistakes

  • Calling WEXITSTATUS without checking WIFEXITED first. The exit code lives in a specific bit field of status, so the value is only meaningful once WIFEXITED confirms a normal exit.

Debugging tips

Run ps -ef | grep defunct to list zombies. Each one is a child you forgot to wait on.

Modern Linux also offers prctl(PR_SET_CHILD_SUBREAPER, 1). This lets a parent claim its grandchildren, which is useful in service managers.

Memory safety

wait is about resource hygiene, not memory.

The kernel keeps a small struct for each child that has not yet been reaped. Long-running servers must reap these children to avoid piling them up.

Two common approaches:

  • Call wait / waitpid directly.
  • Install a SIGCHLD handler that calls waitpid in a loop.

Real-world uses

  • Every shell, after every command it runs.
  • Every web server that uses a child process per connection.
  • cron.
  • init / systemd, which act as the system's process reaper.

Practice tasks

  1. Fork a child that prints a message and calls _exit(7). Have the parent print the exit code using WEXITSTATUS.
  2. Fork 5 children and call waitpid for each one.
  3. Install a SIGCHLD handler that calls waitpid(-1, &st, WNOHANG) in a loop.

Summary

  • wait and waitpid collect a finished child's exit status and clear it from the process table.
  • Pair every fork with a wait (or a SIGCHLD handler) to avoid zombies.
  • Always check WIFEXITED before reading WEXITSTATUS.
  • The fork / exec / wait triad is the heart of Unix process management.

Practice with these exercises