When working with processes and signals in Unix-like environments, understanding how system calls like wait() and waitpid() interact with signals is critical. A common scenario developers run into is whether wait() can return while a child process is still running, and why status macros like WIFEXITED appear to report that a process terminated when it clearly hasn't.

The Short Answer

Yes, wait() can return while the child is still alive. If a signal is caught by the parent process before the child exits and the signal handler does not set the SA_RESTART flag, wait() will fail, return -1, and set errno to EINTR (Interrupted system call).

However, when wait() fails with -1, the status integer is left untouched. Checking WIFEXITED(status) on an interrupted wait() inspects uninitialized memory (garbage data), which often falsely evaluates to true.

Breakdown of the Issues

1. Uninitialized status Variable

In POSIX, when a system call fails (returning -1), it does not populate its output parameters. In code like this:

int status;
pid_t pid = wait(&status);
if (pid < 0) {
    if (errno == EINTR) {
        // BUG: status was never modified by wait()!
        printf("Exited? %d\n", WIFEXITED(status));
    }
}

Because status was declared on the stack without initialization, whatever random bits were in that memory location are passed to the WIFEXITED macro. WIFEXITED(status) merely checks the lower 8 bits of status (typically (status & 0x7f) == 0). If those bits happen to be zero, it returns 1, leading to the illusion that the child exited normally.

Rule of thumb: Never inspect the status variable unless wait() returns a PID greater than 0.

2. Uninitialized struct sigaction (Inconsistent Interruption)

Another subtle issue occurs when registering signal handlers without zeroing the struct sigaction structure:

struct sigaction act;
act.sa_handler = &handler;
act.sa_mask = blockedset;
// Notice: act.sa_flags was never initialized!

Because act is allocated on the stack, act.sa_flags contains indeterminate stack memory. If that garbage value happens to have the SA_RESTART bit set, the system call will automatically restart after the signal handler finishes. If the bit is clear, wait() returns -1 with EINTR. This explains why wait() behaves inconsistently across different runs.

Always initialize signal structures to zero:

struct sigaction act = {0};
// or
memset(&act, 0, sizeof(act));

3. Unsafe Functions in Signal Handlers

Standard I/O functions like printf() are not async-signal-safe. Calling printf() inside a signal handler can cause deadlocks if a signal arrives while another thread or the main routine is inside a buffered I/O lock. Use write() to STDERR_FILENO instead for safe debugging output.

The Refactored, Correct Code

Here is an updated and robust version illustrating how to handle EINTR correctly without relying on garbage status values:

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

void handler(int signum) {
    const char msg[] = "Parent caught SIGINT!\n";
    write(STDERR_FILENO, msg, sizeof(msg) - 1);
}

void child_code() {
    printf("Child (%d) running...\n", getpid());
    // Emulate heavy work
    for (volatile unsigned long long i = 0; i < 500000000ULL; i++);
    printf("Child (%d) finished normally.\n", getpid());
}

int main(void) {
    pid_t newpid = fork();

    if (newpid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (newpid == 0) {
        // Ignore SIGINT in child so Ctrl+C only triggers parent handler
        struct sigaction sa_child = {0};
        sa_child.sa_handler = SIG_IGN;
        sigaction(SIGINT, &sa_child, NULL);

        child_code();
        _exit(EXIT_SUCCESS);
    }

    // Parent process
    struct sigaction act = {0};
    act.sa_handler = handler;
    // Explicitly omit SA_RESTART to ensure wait() fails with EINTR
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);

    if (sigaction(SIGINT, &act, NULL) < 0) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    int status = 0;
    pid_t returned_pid;

    // Keep waiting until the child terminates or an unhandled error occurs
    while ((returned_pid = wait(&status)) < 0) {
        if (errno == EINTR) {
            printf("wait() interrupted by signal (EINTR). Child is still alive.\n");
            continue; // Retry wait()
        }
        perror("wait");
        exit(EXIT_FAILURE);
    }

    if (WIFEXITED(status)) {
        printf("Child %d terminated with exit status: %d\n",
               returned_pid, WEXITSTATUS(status));
    } else if (WIFSIGNALED(status)) {
        printf("Child %d killed by signal: %d\n",
               returned_pid, WTERMSIG(status));
    }

    return 0;
}

Key Takeaways

  • wait() on interrupted calls: If interrupted by a signal caught without SA_RESTART, wait() returns -1 with errno == EINTR, leaving the child running.
  • Never inspect status on error: Only check WIFEXITED and related macros when the returned PID is > 0.
  • Zero your structs: Always clear struct sigaction with {0} or memset() to prevent garbage flags from subtly altering program execution.