How to Use wait and waitpid in C to Reap Child Processes

When a child process terminates in a Unix-like operating system, it enters a "zombie" state until its parent process reads its exit status and releases its entry from the operating system's process table. This article explains how to reap terminated child processes in the C programming language using the wait and waitpid system calls, details the key differences between both functions, demonstrates how to inspect exit statuses using standard macros, and outlines best practices for handling asynchronous process termination.

Why Child Processes Must Be Reaped

When a child process created by fork() finishes executing via exit() or by returning from main(), the kernel deallocates its memory and closed file descriptors. However, the process table entry—which retains the Process ID (PID), termination status, and resource usage statistics—remains active. If the parent does not explicitly collect this information, the child becomes a zombie process (<defunct>). Accumulating too many zombies exhausts available PIDs, preventing the operating system from launching new processes.

Using the wait System Call

The wait() function suspends the calling parent process until any one of its child processes terminates.

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *wstatus);
  • Parameter: wstatus is a pointer to an integer where the kernel writes the child's status information. If this information is not needed, NULL can be passed.
  • Return Value: On success, it returns the PID of the terminated child. On failure (such as when the process has no children), it returns -1 and sets errno.

Basic wait Example

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();

    if (pid < 0) {
        perror("fork failed");
        return 1;
    }

    if (pid == 0) {
        // Child process
        printf("Child process (PID: %d) running...\n", getpid());
        sleep(2);
        exit(42);
    } else {
        // Parent process
        int status;
        pid_t child_pid = wait(&status);
        printf("Parent reaped child (PID: %d)\n", child_pid);

        if (WIFEXITED(status)) {
            printf("Child exited with code: %d\n", WEXITSTATUS(status));
        }
    }

    return 0;
}

Using the waitpid System Call

The waitpid() function provides fine-grained control over process reaping. Unlike wait(), it can wait for a specific process, operate non-blockingly, and monitor stopped or resumed children.

#include <sys/types.h>
#include <sys/wait.h>

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

Parameters

  • pid:
    • < -1: Wait for any child process whose process group ID equals the absolute value of pid.
    • -1: Wait for any child process (identical to wait()).
    • 0: Wait for any child process whose process group ID matches the parent.
    • > 0: Wait for the specific child process whose PID matches this value.
  • wstatus: Pointer to an integer where status information is stored.
  • options: Bitwise OR flags modifying behavior:
    • WNOHANG: Return immediately if no child has terminated (non-blocking).
    • WUNTRACED: Also return for children that have stopped (e.g., via SIGSTOP).
    • WCONTINUED: Also return for children resumed by SIGCONT.

Non-Blocking Reaping Example

Using WNOHANG allows the parent to execute other tasks while periodically polling for terminated children:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();

    if (pid == 0) {
        sleep(1);
        exit(0);
    }

    int status;
    pid_t result;

    while ((result = waitpid(pid, &status, WNOHANG)) == 0) {
        printf("Child still running. Parent performing work...\n");
        usleep(300000); // Sleep for 300ms
    }

    if (result > 0) {
        printf("Successfully reaped child PID: %d\n", result);
    } else {
        perror("waitpid error");
    }

    return 0;
}

Inspecting Child Termination Status

The integer populated in wstatus is an encoded bitfield. Standard POSIX macros are used to evaluate how the process terminated:

  • WIFEXITED(status): Returns true if the child terminated normally (via exit() or returning from main).
  • WEXITSTATUS(status): Evaluates to the exit code passed to exit() or returned from main (only valid if WIFEXITED is true).
  • WIFSIGNALED(status): Returns true if the child was terminated by an unhandled signal (such as SIGKILL or SIGSEGV).
  • WTERMSIG(status): Evaluates to the signal number that caused the termination (only valid if WIFSIGNALED is true).
  • WIFSTOPPED(status): Returns true if the child process was stopped by a signal.

Asynchronous Reaping with SIGCHLD

When a child terminates, the kernel sends a SIGCHLD signal to the parent. Handling this signal allows a parent to reap children asynchronously without blocking main execution threads.

Because standard Unix signals do not queue, multiple children terminating simultaneously may only trigger a single SIGCHLD. Therefore, the signal handler must invoke waitpid in a loop with WNOHANG:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

void sigchld_handler(int sig) {
    (void)sig;
    int status;
    pid_t pid;

    // Reap all terminated children currently available
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        printf("Reaped child PID: %d\n", pid);
    }
}

int main() {
    struct sigaction sa;
    sa.sa_handler = sigchld_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;

    if (sigaction(SIGCHLD, &sa, NULL) == -1) {
        perror("sigaction failed");
        exit(1);
    }

    // Fork child processes
    for (int i = 0; i < 3; i++) {
        if (fork() == 0) {
            sleep(1 + i);
            exit(0);
        }
    }

    // Simulate main process work
    while (1) {
        sleep(5);
    }

    return 0;
}

This pattern ensures that every terminating child is promptly reaped without stalling parent execution or leaking zombie entries in the system process table.