Loading learning content...
When a process terminates, it doesn't simply vanish. It leaves behind a return status—a concise message to its parent describing how it ended. Did it complete successfully? Did it encounter an error? Was it killed by a signal? Did it produce a core dump?
This seemingly simple integer encodes critical information that enables:
The return status is the last act of a process—its final communication to the operating system and its parent. Understanding this mechanism thoroughly is essential for writing correct multi-process programs and diagnosing system issues.
By the end of this page, you will understand: the structure of process exit status, how exit codes differ from termination status, the wait status macros (WIFEXITED, WIFSIGNALED, etc.), how signals and core dumps are encoded, shell conventions for exit codes, and practical patterns for interpreting termination status in your programs.
Before diving into the details, we must clarify two related but distinct concepts:
Exit Status (Exit Code)
The value passed to exit() or returned from main(). This is an integer between 0 and 255 (the low 8 bits are used).
exit(42); // Exit status is 42
return 0; // Exit status is 0
Termination Status (Wait Status)
The complete status retrieved by wait() or waitpid(). This is a 16-bit value that encodes:
The relationship: exit status is a subset of termination status. When a process exits normally, its exit status is embedded in the termination status. When killed by a signal, there's no exit status—only signal information.
The 8-Bit Limitation
Although exit() accepts an int, only the low 8 bits (0-255) are preserved in the termination status:
exit(256); // Truncated! Exit status is 0
exit(257); // Truncated! Exit status is 1
exit(-1); // Interpreted as 255
exit(1000); // Exit status is 1000 & 0xFF = 232
This is because the exit code must fit within the wait status encoding, which reserves only 8 bits for the exit code.
While exit() accepts any int, values above 255 are truncated to their low 8 bits. This can cause exit(256) to appear as success (0)! Always use exit codes between 0 and 255 for reliable behavior.
The termination status retrieved by wait() and waitpid() is encoded in a specific bit format. While the exact layout is implementation-defined, the following is typical on Linux and most Unix systems:
Normal Exit Status Layout:
15 8 7 0
+----------+---------+
| exit_code| 0x00 |
+----------+---------+
Bits 8-15 contain the exit code; bits 0-7 are zero.
Killed by Signal Layout:
15 8 7 6 0
+----------+--+------+
| unused |c | sig |
+----------+--+------+
Bits 0-6 contain the signal number; bit 7 is the core dump flag.
Stopped by Signal Layout:
15 8 7 0
+----------+---------+
| signal | 0x7F |
+----------+---------+
Bits 8-15 contain the signal that caused the stop; 0x7F in low bits indicates stopped.
Rather than parsing these bits manually, POSIX provides status macros that portably extract this information.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/wait.h>#include <unistd.h>#include <string.h>#include <signal.h> // Pretty-print wait status using POSIX macrosvoid print_wait_status(int status) { printf("Raw status value: 0x%04x (%d)\n", status, status); if (WIFEXITED(status)) { // Process called exit() or returned from main() int exit_code = WEXITSTATUS(status); printf(" Normal termination\n"); printf(" Exit code: %d\n", exit_code); } else if (WIFSIGNALED(status)) { // Process was killed by a signal int signal_num = WTERMSIG(status); printf(" Killed by signal: %d (%s)\n", signal_num, strsignal(signal_num)); #ifdef WCOREDUMP // Not all systems support this if (WCOREDUMP(status)) { printf(" Core dump was produced\n"); } else { printf(" No core dump\n"); }#endif } else if (WIFSTOPPED(status)) { // Process was stopped (e.g., by SIGSTOP or debugger) int stop_sig = WSTOPSIG(status); printf(" Stopped by signal: %d (%s)\n", stop_sig, strsignal(stop_sig)); }#ifdef WIFCONTINUED // POSIX.1-2001 else if (WIFCONTINUED(status)) { printf(" Continued (SIGCONT)\n"); }#endif else { printf(" Unknown status format\n"); }} void test_normal_exit(int exit_code) { pid_t pid = fork(); if (pid == 0) { // Child: exit with specified code exit(exit_code); } // Parent: wait and analyze int status; waitpid(pid, &status, 0); printf("\nChild exit(%d):\n", exit_code); print_wait_status(status);} void test_signal_death(int signal_num) { pid_t pid = fork(); if (pid == 0) { // Child: kill self with signal raise(signal_num); _exit(99); // Shouldn't reach here } // Parent: wait and analyze int status; waitpid(pid, &status, 0); printf("\nChild killed by signal %d (%s):\n", signal_num, strsignal(signal_num)); print_wait_status(status);} int main() { printf("Wait Status Analysis Demo\n"); printf("=========================\n"); // Test normal exits test_normal_exit(0); test_normal_exit(1); test_normal_exit(42); test_normal_exit(255); test_normal_exit(256); // Truncated to 0! // Test signal deaths test_signal_death(SIGTERM); // 15 test_signal_death(SIGKILL); // 9 test_signal_death(SIGSEGV); // 11 - may produce core test_signal_death(SIGABRT); // 6 - usually produces core return 0;}| Macro | Purpose | Returns |
|---|---|---|
| WIFEXITED(status) | True if child called exit() normally | Boolean (nonzero/zero) |
| WEXITSTATUS(status) | Get exit code (only if WIFEXITED is true) | 0-255 |
| WIFSIGNALED(status) | True if child was killed by a signal | Boolean |
| WTERMSIG(status) | Get signal number (only if WIFSIGNALED is true) | Signal number |
| WCOREDUMP(status) | True if core dump was produced | Boolean (not all systems) |
| WIFSTOPPED(status) | True if child is currently stopped | Boolean |
| WSTOPSIG(status) | Get signal causing stop (if WIFSTOPPED) | Signal number |
| WIFCONTINUED(status) | True if child was resumed by SIGCONT | Boolean (POSIX.1-2001) |
A common mistake is treating the raw wait status as the exit code. This fails for signal-terminated processes and is generally incorrect. Here's the proper pattern:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
#include <stdio.h>#include <stdlib.h>#include <sys/wait.h>#include <unistd.h>#include <errno.h>#include <string.h> // WRONG: Common mistake - using raw statusint wrong_way_to_check(int status) { // This is WRONG! status is not the exit code! return status; // Raw status includes encoding bits} // CORRECT: Proper status interpretationint get_exit_code(int status, int* was_signaled) { *was_signaled = 0; if (WIFEXITED(status)) { return WEXITSTATUS(status); } else if (WIFSIGNALED(status)) { *was_signaled = 1; // Convention: 128 + signal for signal deaths return 128 + WTERMSIG(status); } // Stopped or continued - shouldn't happen with wait() return -1;} // Complete status analysis functiontypedef struct { int exited_normally; int exit_code; int was_signaled; int signal_number; int core_dumped; const char* description;} ProcessExitInfo; ProcessExitInfo analyze_status(int status) { ProcessExitInfo info = {0}; static char desc_buffer[256]; if (WIFEXITED(status)) { info.exited_normally = 1; info.exit_code = WEXITSTATUS(status); snprintf(desc_buffer, sizeof(desc_buffer), "Exited normally with code %d%s", info.exit_code, info.exit_code == 0 ? " (success)" : " (failure)"); } else if (WIFSIGNALED(status)) { info.was_signaled = 1; info.signal_number = WTERMSIG(status);#ifdef WCOREDUMP info.core_dumped = WCOREDUMP(status);#endif snprintf(desc_buffer, sizeof(desc_buffer), "Killed by %s (signal %d)%s", strsignal(info.signal_number), info.signal_number, info.core_dumped ? ", core dumped" : ""); } else { snprintf(desc_buffer, sizeof(desc_buffer), "Unknown termination status: 0x%x", status); } info.description = desc_buffer; return info;} // Practical example: run a command and interpret its exitint run_and_check(const char* program, char* const argv[]) { pid_t pid = fork(); if (pid < 0) { perror("fork"); return -1; } if (pid == 0) { // Child: execute program execvp(program, argv); // If we get here, exec failed perror("exec"); _exit(127); // Convention for "command not found" } // Parent: wait for child int status; if (waitpid(pid, &status, 0) < 0) { perror("waitpid"); return -1; } ProcessExitInfo info = analyze_status(status); printf("Command '%s' completed:\n", program); printf(" %s\n", info.description); if (info.exited_normally) { return info.exit_code; } else { return 128 + info.signal_number; }} int main() { printf("Exit Status Interpretation Demo\n"); printf("================================\n\n"); // Test: successful command char* args1[] = {"true", NULL}; int result1 = run_and_check("true", args1); printf(" Result: %d\n\n", result1); // Test: failing command char* args2[] = {"false", NULL}; int result2 = run_and_check("false", args2); printf(" Result: %d\n\n", result2); // Test: command that doesn't exist char* args3[] = {"nonexistent_command", NULL}; int result3 = run_and_check("nonexistent_command", args3); printf(" Result: %d\n\n", result3); return 0;}Many programs use exit code 1 for any error, losing diagnostic information. Better practice: use distinct codes for different error types (configuration error=2, file not found=3, etc.), and document them. The sysexits.h header provides standard values.
Shells have established conventions for interpreting exit codes that go beyond the basic 0=success, non-zero=failure rule. Understanding these is essential for shell scripting.
| Exit Code | Meaning | Source |
|---|---|---|
| 0 | Success | Command completed successfully |
| 1 | General error | Catch-all for miscellaneous errors |
| 2 | Misuse of command | Invalid arguments or options (shell builtins) |
| 126 | Cannot execute | Permission denied or not executable |
| 127 | Command not found | Command doesn't exist in PATH |
| 128 | Invalid exit code | exit() called with invalid argument |
| 128+N | Killed by signal N | e.g., 130 = 128+2 = SIGINT (Ctrl+C) |
| 255 | Exit status out of range | exit(value > 255) or exit(-1) |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
#!/bin/bash# Understanding shell exit codes # The special variable $? holds the last exit codetrueecho "After 'true': $?" # 0 falseecho "After 'false': $?" # 1 # Command not foundnonexistent_command 2>/dev/nullecho "After nonexistent: $?" # 127 # Permission denied (create unexecutable file)echo "echo test" > /tmp/notexec.shchmod -x /tmp/notexec.sh/tmp/notexec.sh 2>/dev/nullecho "After non-executable: $?" # 126 # Killed by SIGINT (Ctrl+C)# Run: sleep 100 & then kill -INT $!sleep 0.1 &SLEEP_PID=$!kill -INT $SLEEP_PID 2>/dev/nullwait $SLEEP_PID 2>/dev/nullecho "After SIGINT: $?" # 130 = 128 + 2 # Killed by SIGTERMsleep 0.1 &SLEEP_PID=$!kill -TERM $SLEEP_PID 2>/dev/nullwait $SLEEP_PID 2>/dev/nullecho "After SIGTERM: $?" # 143 = 128 + 15 # Killed by SIGKILLsleep 0.1 &SLEEP_PID=$!kill -KILL $SLEEP_PID 2>/dev/nullwait $SLEEP_PID 2>/dev/nullecho "After SIGKILL: $?" # 137 = 128 + 9 # Checking for signal death./my_commandEXIT_CODE=$?if [ $EXIT_CODE -gt 128 ]; then SIGNAL=$((EXIT_CODE - 128)) echo "Command killed by signal $SIGNAL"fi # Common pattern: exit on errorset -e # Exit script if any command fails # Cleanuprm -f /tmp/notexec.shBash's PIPESTATUS array captures exit codes from all commands in a pipeline. By default, a pipeline's exit code is that of the last command. Use 'set -o pipefail' to make a pipeline fail if any command fails. Example: cmd1 | cmd2 | cmd3 # Only $? is cmd3's exit code
Signal to Exit Code Mapping:
| Signal | Number | Exit Code (128+N) | Common Cause |
|---|---|---|---|
| SIGHUP | 1 | 129 | Terminal closed |
| SIGINT | 2 | 130 | Ctrl+C |
| SIGQUIT | 3 | 131 | Ctrl+\ |
| SIGABRT | 6 | 134 | abort() called |
| SIGKILL | 9 | 137 | kill -9 |
| SIGSEGV | 11 | 139 | Segmentation fault |
| SIGPIPE | 13 | 141 | Broken pipe |
| SIGTERM | 15 | 143 | kill (default) |
Let's examine real-world patterns for working with exit status in production code.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h>#include <sys/types.h>#include <errno.h>#include <string.h>#include <signal.h> // Define application-specific exit codes#define EXIT_SUCCESS_CODE 0#define EXIT_GENERAL_ERROR 1#define EXIT_MISUSE 2#define EXIT_CONFIG_ERROR 3#define EXIT_IO_ERROR 4#define EXIT_NETWORK_ERROR 5#define EXIT_TIMEOUT 6 // Pattern 1: Process Manager with Restart Logictypedef struct { pid_t pid; const char* name; int restart_count; int max_restarts;} ManagedProcess; int should_restart(int status, ManagedProcess* proc) { // Don't restart if intentionally stopped if (WIFEXITED(status)) { int code = WEXITSTATUS(status); // Exit 0 = clean shutdown, don't restart if (code == 0) { printf("[%s] Clean exit, not restarting\n", proc->name); return 0; } // Exit 1-2 = expected errors, check restart limit if (code == 1 || code == 2) { printf("[%s] Expected error (code %d)\n", proc->name, code); return (proc->restart_count < proc->max_restarts); } // Other codes = unexpected, always try restart printf("[%s] Unexpected exit code %d\n", proc->name, code); return 1; } if (WIFSIGNALED(status)) { int sig = WTERMSIG(status); // SIGTERM/SIGINT = intentional stop if (sig == SIGTERM || sig == SIGINT) { printf("[%s] Intentionally terminated\n", proc->name); return 0; } // SIGSEGV/SIGABRT = crash, should restart printf("[%s] Crashed (signal %d), will restart\n", proc->name, sig); return 1; } return 0;} // Pattern 2: Pipeline Status Collectiontypedef struct { int count; int codes[16]; int signals[16]; int success_count; int failure_count;} PipelineStatus; void record_child_status(PipelineStatus* ps, int status) { if (ps->count >= 16) return; if (WIFEXITED(status)) { ps->codes[ps->count] = WEXITSTATUS(status); ps->signals[ps->count] = 0; if (WEXITSTATUS(status) == 0) { ps->success_count++; } else { ps->failure_count++; } } else if (WIFSIGNALED(status)) { ps->codes[ps->count] = -1; ps->signals[ps->count] = WTERMSIG(status); ps->failure_count++; } ps->count++;} int pipeline_succeeded(PipelineStatus* ps) { return ps->failure_count == 0;} void print_pipeline_status(PipelineStatus* ps) { printf("Pipeline status: %d commands, %d succeeded, %d failed\n", ps->count, ps->success_count, ps->failure_count); for (int i = 0; i < ps->count; i++) { if (ps->signals[i]) { printf(" [%d] Killed by signal %d\n", i, ps->signals[i]); } else { printf(" [%d] Exit code %d%s\n", i, ps->codes[i], ps->codes[i] == 0 ? " (OK)" : " (FAIL)"); } }} // Pattern 3: Timeout with Status Preservationint run_with_timeout(char* const argv[], int timeout_secs) { pid_t pid = fork(); if (pid == 0) { // Child: exec the command execvp(argv[0], argv); _exit(127); // exec failed } // Parent: wait with timeout int status; int elapsed = 0; while (elapsed < timeout_secs) { pid_t result = waitpid(pid, &status, WNOHANG); if (result > 0) { // Child exited if (WIFEXITED(status)) { return WEXITSTATUS(status); } else { return 128 + WTERMSIG(status); } } else if (result < 0) { perror("waitpid"); return -1; } // Still running, wait a bit sleep(1); elapsed++; } // Timeout: kill the child printf("Timeout after %d seconds, terminating...\n", timeout_secs); kill(pid, SIGTERM); // Give it a moment to clean up sleep(1); if (waitpid(pid, &status, WNOHANG) == 0) { // Still not dead, force kill printf("Force killing...\n"); kill(pid, SIGKILL); waitpid(pid, &status, 0); } return EXIT_TIMEOUT;} int main() { printf("Exit Status Patterns Demo\n"); printf("=========================\n\n"); // Demo: Pipeline status tracking PipelineStatus ps = {0}; // Simulate some processes for (int i = 0; i < 3; i++) { pid_t pid = fork(); if (pid == 0) { // Simulate various exits if (i == 0) exit(0); // Success if (i == 1) exit(1); // Failure if (i == 2) raise(SIGSEGV); // Crash _exit(99); } } // Collect all statuses for (int i = 0; i < 3; i++) { int status; wait(&status); record_child_status(&ps, status); } print_pipeline_status(&ps); return pipeline_succeeded(&ps) ? 0 : 1;}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
#!/bin/bash# Proper status preservation in wrapper scripts # WRONG: Status is lostrun_something() { some_command "$@" # Implicit: return 0 (BAD!)} # CORRECT: Preserve the exit statusrun_something_v2() { some_command "$@" return $?} # CORRECT: Simplest form (implicit return of last status)run_something_v3() { some_command "$@" # Return status is this command's status} # CORRECT: Explicit status handling with cleanuprun_with_cleanup() { local status some_command "$@" status=$? # Cleanup regardless of status rm -f /tmp/tempfile # Preserve original status return $status} # CORRECT: Complex wrapper with loggingrun_with_logging() { local start_time=$(date +%s) local status echo "Starting: $@" "$@" status=$? local end_time=$(date +%s) local duration=$((end_time - start_time)) if [ $status -eq 0 ]; then echo "Success (${duration}s)" elif [ $status -gt 128 ]; then local signal=$((status - 128)) echo "Killed by signal $signal (${duration}s)" else echo "Failed with code $status (${duration}s)" fi return $status} # Usagerun_with_logging my_command --option value|| echo "Command failed, status was $?"Always declare main() as returning int, not void. C99 requires int main(). Without a return statement, C99 implicitly returns 0, but explicit returns are clearer. C++ requires main() to return int; void main() is non-standard.
When processes exit unexpectedly, debugging the exit status is often the first step in diagnosis.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
#!/bin/bash# Techniques for debugging exit statuses # 1. Basic status capture./myprogramSTATUS=$?echo "Exit status: $STATUS" # 2. Decode signal deathsdecode_status() { local status=$1 if [ $status -eq 0 ]; then echo "Success" elif [ $status -lt 128 ]; then echo "Error: exit code $status" else local signal=$((status - 128)) echo "Killed by signal $signal ($(kill -l $signal 2>/dev/null || echo 'unknown'))" fi} ./myprogramdecode_status $? # 3. Trace process exit in real-timestrace -f -e trace=exit_group ./myprogram 2>&1 | grep exit_group# Output: exit_group(42) = ? # 4. Use timeout to catch hangstimeout 30 ./myprogramSTATUS=$?if [ $STATUS -eq 124 ]; then echo "Command timed out"fi # 5. Debug with gdb for crashesgdb -ex run -ex "bt" -ex quit --args ./myprogram # 6. Set up process accounting (requires root)# lastcomm shows exit statuseslastcomm | head # 7. Use auditd to track exits# auditctl -a always,exit -F arch=b64 -S exit_group # 8. Check dmesg for OOM killsdmesg | grep -i "killed process" # 9. Examine core dumpsif [ -f core ]; then gdb ./myprogram core -ex "bt" -ex quitfi # 10. Pipeline debuggingset -o pipefail # Fail on any pipe component failure cmd1 | cmd2 | cmd3echo "Pipeline status: $?"echo "Individual statuses: ${PIPESTATUS[@]}"strace shows the exact exit_group() call: 'strace -e trace=exit_group ./program' reveals 'exit_group(42) = ?' for a program that called exit(42). This confirms the exit code even when the shell reports something different.
We've explored the complete system for process exit status—from the raw bits encoded in the wait status to practical patterns for real-world applications.
What's Next:
Now that we understand how processes communicate their termination status, we need to examine what happens to the resources they held. When a process dies, its memory, file descriptors, locks, and other resources must be reclaimed. The next page explores resource cleanup—the kernel's mechanisms for ensuring that terminating processes don't leave the system in an inconsistent state.
You now have a thorough understanding of process return status: how it's encoded, how to decode it correctly, shell conventions, and practical patterns for production code. This knowledge is fundamental for shell scripting, process orchestration, and debugging multi-process systems.