Loading content...
When a process terminates, its last act is to communicate its fate to anyone listening. This exit status is the process's final message—a compact encoding of how and why it ended. Did it complete successfully? Did it encounter an error? Was it killed by a signal? Was a core dump generated?
For shell scripts, build systems, process supervisors, and virtually any program that manages children, understanding how to collect and interpret exit statuses is essential. A missing or misinterpreted exit status can mean the difference between a resilient system and one that silently ignores failures.
This page explores the complete mechanics of exit status collection: how statuses are encoded in the kernel, how to extract them via wait() and waitpid(), and how to interpret every possible termination scenario.
By the end of this page, you will understand the exit status encoding, master the wait() and waitpid() function signatures, know how to use status parameter to retrieve termination information, and understand the complete range of termination scenarios your code must handle.
The wait() system call is the simplest mechanism for a parent to synchronize with a terminated child and collect its exit status.
Function Signature:
#include <sys/wait.h>
pid_t wait(int *status);
Parameters:
status: Pointer to an integer where the kernel will store the child's termination status. Can be NULL if the status is not needed.Return Value:
errnoPossible Errors:
| errno | Meaning |
|---|---|
ECHILD | No child processes exist (or no unwaited children) |
EINTR | Interrupted by a signal before any child terminated |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h> /** * Basic wait() usage demonstration * * The parent forks a child, the child exits with a specific status, * and the parent collects that status using wait(). */int main() { pid_t child_pid = fork(); if (child_pid < 0) { perror("fork failed"); exit(EXIT_FAILURE); } if (child_pid == 0) { // Child process printf("Child (PID %d): Starting work...\n", getpid()); sleep(1); // Simulate some work printf("Child: Exiting with status 7\n"); exit(7); // Exit with status 7 (arbitrary choice) } // Parent process printf("Parent (PID %d): Created child %d\n", getpid(), child_pid); int status; // Will receive termination status pid_t terminated_pid = wait(&status); if (terminated_pid < 0) { perror("wait failed"); exit(EXIT_FAILURE); } printf("Parent: Child %d terminated\n", terminated_pid); printf("Parent: Raw status value = 0x%04X (%d)\n", status, status); // Note: The raw status value is NOT the exit code! // We'll learn to extract it properly in the next section. return 0;} /* * Expected Output: * Parent (PID 1000): Created child 1001 * Child (PID 1001): Starting work... * Child: Exiting with status 7 * Parent: Child 1001 terminated * Parent: Raw status value = 0x0700 (1792) * * Notice: The exit code 7 is NOT directly in the status value. * It's encoded—0x0700 contains 7 in the high byte. */Critical Insight: The Status Value is Encoded
Notice in the example that the child exited with status 7, but the raw status value the parent receives is 1792 (or 0x0700 in hexadecimal). This is not a bug—the status integer is encoded to carry multiple pieces of information:
Extracting this information requires status macros, which we'll cover shortly. Never interpret the raw status value directly!
The integer stored in the status variable by wait() is NOT the exit code. It's a bitfield encoding multiple pieces of information. Always use the WIFEXITED, WEXITSTATUS, WIFSIGNALED, and other macros to extract meaningful data. Treating the raw value as an exit code is a common bug.
wait() is simple but limited—it waits for any child and always blocks. The waitpid() system call provides fine-grained control over which child to wait for and whether to block.
Function Signature:
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
Parameters:
pid — Specifies which children to wait for:
| pid Value | Meaning | Use Case |
|---|---|---|
> 0 | Wait for the specific child with this PID | Wait for a known child process |
-1 | Wait for any child (equivalent to wait()) | Reap any terminated child |
0 | Wait for any child in the same process group as parent | Job control in shells |
< -1 | Wait for any child in process group abs(pid) | Wait for specific process group |
status — Pointer to receive termination status (same as wait()).
options — Bitwise OR of flags controlling behavior:
| Flag | Effect | When to Use |
|---|---|---|
0 | Block until child terminates (default behavior) | Simple synchronous waiting |
WNOHANG | Return immediately if no child has terminated | Polling without blocking |
WUNTRACED | Also return if a child has stopped (not terminated) | Job control (Ctrl+Z handling) |
WCONTINUED | Also return if a stopped child has resumed | Advanced job control |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h> /** * Demonstrates various waitpid() usage patterns */ // Example 1: Wait for a specific childvoid wait_for_specific_child() { pid_t child1 = fork(); if (child1 == 0) { sleep(2); exit(1); } pid_t child2 = fork(); if (child2 == 0) { sleep(1); exit(2); } // Wait specifically for child1, ignoring child2 for now int status; pid_t result = waitpid(child1, &status, 0); printf("Waited for child1 (%d), got %d\n", child1, result); // Now wait for child2 result = waitpid(child2, &status, 0); printf("Waited for child2 (%d), got %d\n", child2, result);} // Example 2: Wait for any child (equivalent to wait())void wait_for_any_child() { for (int i = 0; i < 3; i++) { pid_t pid = fork(); if (pid == 0) { sleep(i + 1); exit(i * 10); } printf("Created child %d\n", pid); } // Wait for all children in termination order int status; pid_t result; while ((result = waitpid(-1, &status, 0)) > 0) { printf("Child %d terminated with status %d\n", result, WEXITSTATUS(status)); }} // Example 3: Non-blocking wait with WNOHANGvoid non_blocking_wait() { pid_t child = fork(); if (child == 0) { sleep(3); exit(0); } int status; pid_t result; int iterations = 0; // Poll until child terminates while ((result = waitpid(child, &status, WNOHANG)) == 0) { printf("Child still running... (iteration %d)\n", ++iterations); usleep(500000); // Sleep 0.5 seconds // Could do other work here } if (result > 0) { printf("Child terminated after %d iterations\n", iterations); } else { perror("waitpid"); }} int main(int argc, char *argv[]) { printf("=== Wait for Specific Child ===\n"); wait_for_specific_child(); printf("\n=== Wait for Any Child ===\n"); wait_for_any_child(); printf("\n=== Non-blocking Wait ===\n"); non_blocking_wait(); return 0;}Return Value Interpretation for waitpid():
| Return Value | Meaning |
|---|---|
> 0 | PID of the child whose state changed |
0 | WNOHANG was specified and no children have changed state |
-1 | Error occurred; check errno |
The WNOHANG return value of 0 is crucial—it means "I checked, and no children have terminated yet." This allows polling without blocking.
Relationship between wait() and waitpid():
The wait() call is actually a simplified wrapper:
// These two calls are equivalent:
wait(&status);
waitpid(-1, &status, 0);
In many codebases, waitpid() is preferred even for simple cases because it makes the behavior explicit and allows easy future extension.
While wait() is simpler, waitpid() is more explicit about what's being waited for. When reviewing code, waitpid(-1, &status, 0) clearly shows 'wait for any child, blocking.' This explicitness helps prevent bugs when the code evolves.
The status value returned by wait() and waitpid() is not a simple number—it's a carefully designed bitfield that encodes multiple pieces of information in a single integer. Understanding this encoding is essential for correct interpretation.
Status Layout (Linux/POSIX):
The 32-bit status integer uses the following layout for normal termination:
Bits: 31-16 15-8 7 6-0
unused exit code 0x00 0x00
For signal termination:
Bits: 31-16 15-8 7 6-0
unused 0x00 core dump signal number
Key Points:
Why This Encoding?
This design serves several purposes:
Manual Decoding (Don't Do This):
You could decode manually:
// DON'T DO THIS - use macros instead
int signal_num = status & 0x7f; // Bits 0-6
int core_dumped = (status >> 7) & 1; // Bit 7
int exit_code = (status >> 8) & 0xff; // Bits 8-15
if (signal_num == 0) {
// Normal exit, exit_code is valid
} else {
// Killed by signal, signal_num is valid
}
However, this is wrong for several reasons:
ALWAYS use the provided status macros instead.
While understanding the bit layout helps conceptually, never write code that directly manipulates status bits. The status macros (WIFEXITED, WEXITSTATUS, etc.) are portable, correct, and clearly express intent. Manual bit manipulation is a source of bugs.
Exit codes are the primary way processes communicate their outcome. While technically any value from 0-255 is valid, strong conventions govern their use.
The Universal Convention:
| Exit Code | Meaning | Example |
|---|---|---|
0 | Success - operation completed as expected | return 0; |
1 | General failure - something went wrong | exit(1); |
2 | Misuse - incorrect command-line usage | grep with bad syntax |
126 | Permission problem - cannot execute | Command not executable |
127 | Command not found - file doesn't exist | $ nonexistent-command |
128+N | Killed by signal N | 128 + 9 = 137 means killed by SIGKILL |
Shell Conventions (Bash, POSIX):
Shells often use the formula 128 + signal_number when a command is killed by a signal:
$ sleep 100 &
[1] 12345
$ kill -9 12345
$ echo $?
137 # 128 + 9 (SIGKILL)
This convention allows scripts to distinguish "exited with error" from "killed by signal" using only the numeric code.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h>#include <string.h>#include <errno.h> /** * Standard exit codes following common conventions */#define EXIT_SUCCESS_CODE 0#define EXIT_GENERAL_ERROR 1#define EXIT_MISUSE 2#define EXIT_CANNOT_EXECUTE 126#define EXIT_NOT_FOUND 127 // Custom application-specific codes (should be documented)#define EXIT_CONFIG_ERROR 10#define EXIT_NETWORK_ERROR 11#define EXIT_DATABASE_ERROR 12 /** * Process that follows exit code conventions */int run_task(const char *config_file, const char *database, const char *server) { // Check configuration if (config_file == NULL || strlen(config_file) == 0) { fprintf(stderr, "Error: Configuration file not specified\n"); return EXIT_MISUSE; } // Try to open config file FILE *fp = fopen(config_file, "r"); if (fp == NULL) { if (errno == ENOENT) { fprintf(stderr, "Error: Config file not found: %s\n", config_file); return EXIT_NOT_FOUND; } if (errno == EACCES) { fprintf(stderr, "Error: Cannot read config: %s\n", config_file); return EXIT_CANNOT_EXECUTE; } fprintf(stderr, "Error: Config error: %s\n", strerror(errno)); return EXIT_CONFIG_ERROR; } fclose(fp); // Simulate database connection check if (database == NULL) { fprintf(stderr, "Error: Database connection failed\n"); return EXIT_DATABASE_ERROR; } // Simulate network check if (server == NULL) { fprintf(stderr, "Error: Cannot reach server\n"); return EXIT_NETWORK_ERROR; } // All checks passed printf("Task completed successfully\n"); return EXIT_SUCCESS_CODE;} int main(int argc, char *argv[]) { // Demonstrate proper exit code usage int result = run_task( argc > 1 ? argv[1] : NULL, argc > 2 ? argv[2] : NULL, argc > 3 ? argv[3] : NULL ); // Exit with meaningful code return result;} /* * Usage examples: * $ ./program # Exits 2 (misuse) * $ ./program missing.conf # Exits 127 (not found) * $ ./program /etc/shadow db srv # Exits 126 (permission denied) * $ ./program config.conf db srv # Exits 0 (success) */Important Considerations:
1. Exit code truncation:
Exit codes are limited to 8 bits (0-255). If a program returns a value outside this range:
exit(256); // Actually becomes 0 (256 & 0xFF)
exit(257); // Actually becomes 1 (257 & 0xFF)
exit(-1); // Actually becomes 255 (0xFF)
2. Return vs. exit() vs. _exit():
| Method | Behavior |
|---|---|
return N; from main() | Calls exit(N), runs cleanup |
exit(N); | Flushes buffers, runs atexit() handlers, then terminates |
_exit(N); or _Exit(N); | Immediate termination, no cleanup |
After fork(), children typically use _exit() to avoid double-flushing parent's buffers and running cleanup handlers twice.
3. Signal information encoding:
When a process is killed by a signal, some environments encode this as 128 + signal_number. However, this is a convention, not a kernel feature. The kernel separately reports the signal via the status bits—the 128+N encoding is something shells do for uniformity.
For non-trivial programs, document all possible exit codes in your man page, README, or header files. Scripts and automation tools depend on these codes being stable and meaningful. Changing exit codes is a breaking API change.
Let's put together a comprehensive example that demonstrates proper exit status collection with full error handling. This pattern serves as a template for production code.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h>#include <string.h>#include <errno.h>#include <signal.h> /** * Complete example: Proper exit status collection * * Demonstrates: * - Forking with error handling * - waitpid() with EINTR handling * - Status extraction using all relevant macros * - Meaningful reporting of child outcomes */ /** * Returns a human-readable description of a signal */const char* signal_description(int sig) { switch (sig) { case SIGTERM: return "Termination request (SIGTERM)"; case SIGKILL: return "Forced kill (SIGKILL)"; case SIGSEGV: return "Segmentation fault (SIGSEGV)"; case SIGABRT: return "Abort (SIGABRT)"; case SIGFPE: return "Floating point exception (SIGFPE)"; case SIGBUS: return "Bus error (SIGBUS)"; case SIGINT: return "Interrupt (SIGINT)"; default: return "Unknown signal"; }} /** * Waits for a specific child and reports its termination status * * @param child_pid The PID of the child to wait for * @param child_name Descriptive name for logging * @return The exit code if exited normally, or 128+signal if killed */int wait_and_report(pid_t child_pid, const char *child_name) { int status; pid_t result; // Retry waitpid if interrupted by a signal do { result = waitpid(child_pid, &status, 0); } while (result < 0 && errno == EINTR); if (result < 0) { fprintf(stderr, "waitpid() failed for %s (PID %d): %s\n", child_name, child_pid, strerror(errno)); return -1; // Internal error } // Verify we got the expected child if (result != child_pid) { fprintf(stderr, "Warning: Expected PID %d, got %d\n", child_pid, result); } printf("\n=== Status Report for %s (PID %d) ===\n", child_name, child_pid); if (WIFEXITED(status)) { // Child called exit() or returned from main() int exit_code = WEXITSTATUS(status); printf("Termination: Normal exit\n"); printf("Exit code: %d\n", exit_code); if (exit_code == 0) { printf("Meaning: Success\n"); } else { printf("Meaning: Failure (code %d)\n", exit_code); } return exit_code; } else if (WIFSIGNALED(status)) { // Child was killed by a signal int signal_num = WTERMSIG(status); int core_dumped = 0; #ifdef WCOREDUMP // WCOREDUMP is not POSIX, but common core_dumped = WCOREDUMP(status); #endif printf("Termination: Killed by signal\n"); printf("Signal: %d (%s)\n", signal_num, signal_description(signal_num)); printf("Core dumped: %s\n", core_dumped ? "Yes" : "No"); // Shell convention: 128 + signal_number return 128 + signal_num; } else if (WIFSTOPPED(status)) { // Child was stopped (not terminated) int stop_signal = WSTOPSIG(status); printf("State: Stopped (not terminated)\n"); printf("Stop signal: %d\n", stop_signal); return -2; // Indicate stopped, not terminated } else if (WIFCONTINUED(status)) { // Child was resumed (only with WCONTINUED flag) printf("State: Continued\n"); return -3; // Indicate continued, not terminated } else { // Should never happen printf("Unknown status: 0x%04X\n", status); return -4; }} /** * Different child behaviors for testing */void child_exit_success() { printf("Child: Exiting successfully\n"); _exit(0);} void child_exit_failure() { printf("Child: Exiting with error code 42\n"); _exit(42);} void child_crash_segfault() { printf("Child: About to crash (segfault)...\n"); int *p = NULL; *p = 42; // Null pointer dereference} void child_abort() { printf("Child: Calling abort()\n"); abort();} int main() { pid_t pids[4]; // Child 1: Normal exit with success pids[0] = fork(); if (pids[0] == 0) { child_exit_success(); } // Child 2: Normal exit with failure pids[1] = fork(); if (pids[1] == 0) { child_exit_failure(); } // Child 3: Crash via segfault pids[2] = fork(); if (pids[2] == 0) { child_crash_segfault(); } // Child 4: Abort pids[3] = fork(); if (pids[3] == 0) { child_abort(); } // Parent: Wait for all children and report sleep(1); // Give children time to do their thing wait_and_report(pids[0], "Success Child"); wait_and_report(pids[1], "Failure Child"); wait_and_report(pids[2], "Segfault Child"); wait_and_report(pids[3], "Abort Child"); printf("\nAll children processed.\n"); return 0;}Expected Output:
Child: Exiting successfully
Child: Exiting with error code 42
Child: About to crash (segfault)...
Child: Calling abort()
=== Status Report for Success Child (PID 12345) ===
Termination: Normal exit
Exit code: 0
Meaning: Success
=== Status Report for Failure Child (PID 12346) ===
Termination: Normal exit
Exit code: 42
Meaning: Failure (code 42)
=== Status Report for Segfault Child (PID 12347) ===
Termination: Killed by signal
Signal: 11 (Segmentation fault (SIGSEGV))
Core dumped: No
=== Status Report for Abort Child (PID 12348) ===
Termination: Killed by signal
Signal: 6 (Abort (SIGABRT))
Core dumped: Yes
All children processed.
This output demonstrates all the key termination scenarios you'll encounter in practice.
Let's examine common patterns for collecting exit statuses in real applications:
Pattern 1: Shell-Style Command Execution
Run a command and return its exit code, translating signals to 128+N:
1234567891011121314151617181920212223242526272829303132333435363738
/** * Execute a command and return its exit status * (shell-style: signals become 128 + signal_number) */int run_command(const char *program, char *const argv[]) { pid_t pid = fork(); if (pid < 0) { return -1; // fork failed } if (pid == 0) { // Child: exec the command execvp(program, argv); // If exec returns, it failed _exit(errno == ENOENT ? 127 : 126); // Not found or can't execute } // Parent: wait and return status int status; while (waitpid(pid, &status, 0) < 0) { if (errno != EINTR) { return -1; // waitpid failed } } if (WIFEXITED(status)) { return WEXITSTATUS(status); } else if (WIFSIGNALED(status)) { return 128 + WTERMSIG(status); } return -1; // Unknown status} // Usage:// char *args[] = {"ls", "-la", NULL};// int result = run_command("ls", args);Pattern 2: Build System Task Runner
Run multiple tasks, abort on first failure:
1234567891011121314151617181920212223242526272829303132333435363738394041
/** * Run tasks sequentially, abort on first failure */int run_tasks_sequential(const char **tasks, int num_tasks) { for (int i = 0; i < num_tasks; i++) { printf("Running task %d: %s\n", i + 1, tasks[i]); pid_t pid = fork(); if (pid < 0) { perror("fork"); return -1; } if (pid == 0) { // Child: execute task (using shell for simplicity) execlp("sh", "sh", "-c", tasks[i], NULL); _exit(127); } // Parent: wait for this task int status; waitpid(pid, &status, 0); if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { printf("Task %d FAILED\n", i + 1); if (WIFSIGNALED(status)) { printf(" Killed by signal %d\n", WTERMSIG(status)); } else { printf(" Exit code: %d\n", WEXITSTATUS(status)); } return -1; // Abort remaining tasks } printf("Task %d completed successfully\n", i + 1); } printf("All %d tasks completed successfully\n", num_tasks); return 0;}This page has provided comprehensive coverage of exit status collection—one of the most important aspects of process management.
What's Next:
We've learned how to wait for children and collect their exit statuses. But wait() is a blocking call—the parent is suspended until a child terminates. What if the parent needs to remain responsive? The next page explores blocking vs. non-blocking waits, introducing the WNOHANG flag and strategies for concurrent wait handling.
You now understand how to collect and interpret child process exit statuses using wait() and waitpid(). You know the status encoding, the extraction macros, and common patterns for production code. Next, we'll explore blocking vs. non-blocking wait strategies.