Chapter 58: Process Termination: exit()
, _exit()
, and Exit Status
Chapter Objectives
Upon completing this chapter, you will be able to:
- Understand the key differences between normal and abnormal process termination in a Linux environment.
- Implement controlled process shutdown using the
exit()
and_exit()
system calls. - Configure cleanup handlers that execute upon normal process termination using the
atexit()
function. - Debug parent-child relationships by retrieving and interpreting child process exit statuses using the
wait()
family of calls. - Analyze and prevent common issues like zombie and orphan processes in multitasking applications.
- Apply these concepts to build robust, predictable applications on an embedded platform like the Raspberry Pi 5.
Introduction
In any operating system, processes are transient; they are created, they execute, and eventually, they terminate. In the world of embedded Linux, where systems are expected to run reliably for extended periods, often without direct human supervision, the manner in which a process terminates is not a trivial detail—it is a cornerstone of system stability and robustness. A process that ends abruptly without proper cleanup can leave the system in an inconsistent state, leaking resources like file descriptors, shared memory segments, or semaphores. An unhandled error condition that causes a crash can have cascading effects, bringing down critical services.
This chapter delves into the mechanisms that govern the end of a process’s life cycle. We will explore the fundamental distinction between normal termination, a controlled and orderly shutdown, and abnormal termination, which typically results from an unrecoverable error or an external signal. You will learn how a process communicates its outcome—success or a specific failure—to its parent process through an exit status. This simple integer value is a vital communication tool in the shell’s command-line scripting and in complex, multi-process applications where the fate of one process dictates the actions of another.
We will dissect the standard library function exit()
and the underlying system call _exit()
, revealing the critical differences in their behavior regarding I/O buffer flushing and cleanup handlers. Understanding when to use each is crucial for predictable program behavior, especially in resource-constrained embedded systems. By the end of this chapter, you will not only grasp the theory but will have implemented these concepts directly on your Raspberry Pi 5, writing C programs that manage process lifecycle, handle termination gracefully, and communicate status effectively, thereby creating more resilient and debuggable embedded applications.
graph TD A[Process Execution] --> B{Termination Point Reached}; B --> C[Normal Termination]; B --> D[Abnormal Termination]; subgraph "Controlled Shutdown" C --> C1("Call exit() or<br>return from main()"); C1 --> C2("Cleanup: atexit() handlers,<br>stdio buffer flush"); C2 --> C3("Kernel Call: _exit()"); end subgraph "Forced Shutdown" D --> D1("Caused by an<br>unhandled signal"); D1 --> D2("e.g., SIGSEGV, SIGILL, SIGKILL"); D2 --> D3("Immediate Kernel-level<br>cleanup, no user cleanup"); end C3 --> E[Parent Notified via SIGCHLD]; D3 --> E; E --> F[Process becomes Zombie]; F --> G["Parent calls wait()"]; G --> H[Process Reaped]; classDef primary fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff; classDef process fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff; classDef decision fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff; classDef check fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff; classDef system fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff; classDef success fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff; class A primary; class B decision; class C,D,C1,C2,C3,D1,D2,D3 process; class E,F,G system; class H success;
Technical Background
The termination of a process is the final phase of its lifecycle, a sequence of events managed by the Linux kernel in coordination with the C standard library to ensure a structured and clean shutdown. This process involves releasing system resources, notifying the parent process of the termination, and communicating a final status code. The distinction between different termination paths—normal versus abnormal, and the subtle but critical differences between exit()
and _exit()
—is fundamental to writing reliable system software.
The Concept of Normal Termination
A process is said to terminate normally when it concludes its execution as intended by its programmer. This is not necessarily synonymous with success, but rather indicates that the program reached a designated end-point and initiated its own shutdown in a controlled manner. The most common way for a C program to terminate normally is by returning from the main()
function. The value returned by main()
is implicitly passed to the exit()
function and becomes the process’s exit status. For example, return 0;
at the end of main()
is functionally equivalent to calling exit(0);
.
Alternatively, a program can explicitly call the exit()
function from any point in its code. This function, defined in <stdlib.h>
, initiates the shutdown sequence. The single integer argument passed to exit()
is the exit status, a value between 0 and 255 that the parent process can retrieve to learn about the child’s outcome. By convention, an exit status of 0 signifies successful completion. A non-zero value indicates that an error or some specific condition occurred. This convention is the backbone of shell scripting, where the success or failure of one command determines whether the next command in a sequence is executed.
When exit()
is called, it does not immediately cede control to the kernel. Instead, it performs a series of crucial cleanup actions within the user space, managed by the C standard library. First, it calls any functions that have been registered via the atexit()
function. These are cleanup handlers that programmers can define to perform tasks like closing files, releasing memory, or logging a shutdown message. The handlers are called in the reverse order of their registration, providing a last-in, first-out (LIFO) stack-like mechanism for cleanup.
After executing the atexit()
handlers, the exit()
function proceeds to flush all open standard I/O streams. This is a critical step. Many I/O operations, particularly writing to files or the console, are buffered for efficiency. Data written with functions like printf()
or fwrite()
may reside in a user-space buffer and not yet be written to the underlying file or device. The exit()
function ensures that these buffers are flushed, meaning their contents are written out to their final destinations. This prevents data loss and ensures that log messages or output files are complete at the time of termination.
Only after these user-space cleanup activities are complete does the exit()
function invoke the _exit()
system call (often aliased as _Exit
), which transfers control to the kernel to perform the final steps of termination.
graph TD subgraph "C Standard Library (User Space)" A["Call to exit(status)"] --> B{"Call atexit() Handlers"}; B -- "LIFO Order" --> C{Flush and Close All stdio Streams}; C -- "e.g., printf buffers" --> D["Invoke _exit(status) system call"]; end subgraph "Linux Kernel (Kernel Space)" D -- "Context Switch" --> E{Kernel Termination Process}; E --> F[Close open file descriptors]; F --> G[Release Memory]; G --> H[Notify Parent via SIGCHLD]; H --> I[Become Zombie Process]; end I -- "Waits for parent" --> J["Parent calls wait()"]; J --> K[Process Reaped and Removed]; classDef primary fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff; classDef process fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff; classDef check fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff; classDef system fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff; classDef success fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff; class A primary; class B,C,D process; class E,F,G,H,I system; class J check; class K success;
The _exit()
System Call: The Point of No Return
The _exit()
system call, which can be found in <unistd.h>
, is the kernel’s direct entry point for process termination. Unlike exit()
, _exit()
terminates the process immediately. It does not call any atexit()
handlers and does not flush any standard I/O buffers. It is an abrupt, though still “normal,” termination from the kernel’s perspective. The kernel’s job is to reclaim the resources owned by the process.
Upon receiving an _exit()
call, the kernel performs the following essential actions:
- Closes all open file descriptors associated with the process. This includes files, sockets, pipes, and any other kernel-managed I/O objects.
- Releases memory allocated to the process, including its stack, heap, and code segments. The virtual memory mappings are torn down, and the physical memory pages are returned to the system’s free pool.
- Detaches from any shared memory segments.
- Decrements the link count on its current working directory.
- Sends the
SIGHUP
signal to any child processes if the terminating process is a session leader, effectively orphaning them. - Changes the process state to
EXIT_ZOMBIE
and stores the exit status provided to_exit()
. - Notifies the parent process by sending it the
SIGCHLD
signal. This signal informs the parent that one of its children has changed state and should be “reaped.”
The key takeaway is the immediacy and rawness of _exit()
. Its use is generally reserved for specific scenarios. For instance, in a child process created via fork()
, it is common to call _exit()
after the child has completed its task, especially after an exec()
call fails. Using exit()
in the child could be problematic because it would flush I/O buffers that were duplicated from the parent, potentially leading to duplicated output. It would also call atexit()
handlers registered by the parent, which is almost certainly not the desired behavior. Therefore, the established pattern is: fork()
, the child does its work (often an exec()
call), and if it needs to terminate, it uses _exit()
.
Parent-Child Synchronization: wait()
and Zombie Processes
A process does not simply vanish from the system upon termination. As mentioned, the kernel transitions it into a special state known as EXIT_ZOMBIE
. In this state, the process is effectively dead—it consumes no CPU resources—but its entry in the process table is preserved. This entry holds valuable information: the process ID (PID) and, most importantly, its exit status. The process remains a zombie until its parent explicitly acknowledges its termination by calling one of the functions from the wait()
family (e.g., wait()
, waitpid()
).
This mechanism serves as a crucial synchronization point. The parent process, upon receiving the SIGCHLD
signal, can call wait()
or waitpid()
to retrieve the child’s exit status and allow the kernel to finally remove the zombie process from the process table. This act of collecting the exit status is known as reaping the child.
If a parent process terminates before its children, the children become orphan processes. In a UNIX-like system, orphans are not left to wander aimlessly. They are immediately adopted by a special system process, typically the init
process (PID 1) or a modern equivalent like systemd
. This “grandparent” process has a built-in routine to periodically call wait()
, ensuring that any of its adopted children are properly reaped when they terminate, thus preventing them from becoming zombies.
However, if a parent process fails to reap its child, the zombie will persist until the parent itself terminates. While a few zombies are harmless, a system with a buggy parent process that continuously forks children without reaping them can accumulate a large number of zombies. Since each zombie consumes an entry in the finite process table, this can eventually exhaust the available PIDs, preventing new processes from being created and leading to system failure. This is a classic resource leak bug in system programming.
flowchart TB Start([Start]) --> Fork["fork()"] Fork --> Parent[Parent Process] Fork --> Child[Child Process] Child --> Exit["calls exit()/_exit()"] Exit --> Zombie[Zombie State<br/>EXIT_ZOMBIE<br/>- Process is dead<br/>- Process table entry kept<br/>- Holds exit status] Parent --> Wait["calls wait()/waitpid()"] Wait --> Zombie Zombie --> End([Process Removed]) style Zombie fill:#ffcccc style Start fill:#ccffcc style End fill:#ccffcc
Abnormal Termination
Not all processes terminate by their own volition. Abnormal termination occurs when a process is forced to end due to an unhandled signal. Signals are a form of inter-process communication used by the kernel and other processes to notify a process of an event. Many signals, if not caught and handled by the process, will result in its immediate termination.
Common signals that cause abnormal termination include:
SIGSEGV
(Segmentation Fault): This occurs when a process attempts to access a memory location that it is not allowed to access (e.g., dereferencing aNULL
pointer or writing to a read-only memory segment).SIGILL
(Illegal Instruction): This signal is sent when the CPU encounters an invalid or privileged machine instruction in the process’s code.SIGFPE
(Floating-Point Exception): This indicates an error in a floating-point arithmetic operation, such as division by zero.SIGTERM
: This is a generic termination signal that requests a process to shut down. It is the default signal sent by thekill
command. A well-behaved process will catch this signal and perform a graceful shutdown (similar to callingexit()
). If not caught, it terminates the process.SIGKILL
: This is the ultimate termination signal. It cannot be caught, blocked, or ignored. It instructs the kernel to terminate the process immediately, without any opportunity for cleanup. From the kernel’s perspective, the termination process is similar to_exit()
, but it is initiated externally and cannot be prevented.

When a process is terminated by a signal, its exit status is not a simple integer. The parent process, using wait()
or waitpid()
, can determine not only that the child terminated abnormally but also which signal caused the termination. The status integer returned by wait()
is a bitmask, and a set of macros (WIFEXITED
, WEXITSTATUS
, WIFSIGNALED
, WTERMSIG
, etc.) must be used to decode it properly. This provides a rich diagnostic framework for a parent process to understand exactly why its child has disappeared.
Practical Examples
Theory comes to life when applied. In this section, we will use the Raspberry Pi 5 as our embedded development platform to explore process termination concepts through hands-on C programming and shell interaction. Ensure you have a Raspberry Pi 5 running a standard Raspberry Pi OS (or a similar Debian-based distribution) and have the GCC compiler installed (sudo apt-get install build-essential
).
Example 1: exit()
vs. _exit()
and I/O Buffering
This example clearly demonstrates the most important difference between exit()
and _exit()
: the flushing of standard I/O buffers.
Code Snippet
Create a file named exit_demo.c
with the following content. The program writes a message to the standard output, but without a newline character. A newline typically forces a flush of the line-buffered console stream. We will see how exit()
and _exit()
behave differently in this context.
// exit_demo.c: Demonstrates the difference between exit() and _exit()
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
// Check for correct command-line arguments
if (argc != 2) {
fprintf(stderr, "Usage: %s <exit | _exit>\n", argv[0]);
exit(EXIT_FAILURE); // Use exit() for usage errors
}
// Print a message to standard output.
// IMPORTANT: No newline character is used. stdout is typically line-buffered,
// so this text will be held in a buffer and not immediately displayed.
printf("This message is buffered");
// Choose termination method based on the command-line argument
if (strcmp(argv[1], "exit") == 0) {
printf("\nTerminating with exit()... Buffers will be flushed.\n");
exit(EXIT_SUCCESS);
} else if (strcmp(argv[1], "_exit") == 0) {
// We print a newline here to ensure the message above this one is visible
// before we call _exit(), which will NOT flush the first printf.
printf("\nTerminating with _exit()... Buffers will NOT be flushed.\n");
// The following call terminates the process immediately.
// The "This message is buffered" string will likely be lost.
_exit(EXIT_SUCCESS);
} else {
fprintf(stderr, "Invalid argument. Use 'exit' or '_exit'.\n");
exit(EXIT_FAILURE);
}
// This line should never be reached
return 0;
}
Build and Execution Steps
1. Compile the code: Open a terminal on your Raspberry Pi and compile the program using GCC.
gcc -o exit_demo exit_demo.c
2. Run with exit()
: Execute the program with the argument exit
.
gcc -o exit_demo exit_demo.c
Expected Output:
This message is buffered
Terminating with exit()... Buffers will be flushed.
Explanation: The exit()
function flushes the standard output buffer before terminating. As a result, the initial string “This message is buffered” is written to the terminal, followed by the explicit message before the call.
3. Run with _exit()
: Now, execute the program with the argument _exit
.
./exit_demo _exit
Expected Output:
Terminating with _exit()... Buffers will NOT be flushed.
Explanation: The _exit()
system call terminates the process immediately. The standard output buffer containing “This message is buffered” is never flushed and is discarded by the kernel along with the rest of the process’s memory. The only output you see is the one explicitly flushed with a newline character before _exit()
was called. This starkly illustrates the danger of using _exit()
when pending I/O is important.
Example 2: Using atexit()
for Graceful Shutdown
This example shows how to register cleanup handlers that are automatically called by exit()
. This is a powerful technique for ensuring resources are released correctly.
sequenceDiagram participant Main as "main() Execution" participant Atexit as "atexit() Handler Stack" Main->>Atexit: atexit(cleanup_handler_1) note right of Atexit: handler_1 is pushed Main->>Atexit: atexit(cleanup_handler_2) note right of Atexit: handler_2 is pushed on top Main-->>Main: Program continues... Main-->>Atexit: exit() is called note over Atexit: Executing handlers in LIFO order Atexit-->>Main: 1. Run cleanup_handler_2 note right of Atexit: handler_2 is popped Atexit-->>Main: 2. Run cleanup_handler_1 note right of Atexit: handler_1 is popped
Code Snippet
Create a file named atexit_demo.c
. This program registers two cleanup functions and then terminates.
// atexit_demo.c: Demonstrates registration of exit handlers.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// First cleanup handler to be registered
void cleanup_handler_1() {
printf("Cleanup handler 1: Releasing primary resources...\n");
}
// Second cleanup handler to be registered
void cleanup_handler_2() {
printf("Cleanup handler 2: Closing log files...\n");
}
int main() {
printf("Program starting. Registering exit handlers.\n");
// Register the first handler.
if (atexit(cleanup_handler_1) != 0) {
fprintf(stderr, "Failed to register cleanup_handler_1\n");
exit(EXIT_FAILURE);
}
// Register the second handler.
if (atexit(cleanup_handler_2) != 0) {
fprintf(stderr, "Failed to register cleanup_handler_2\n");
exit(EXIT_FAILURE);
}
printf("Handlers registered. Program will now exit normally.\n");
// When exit() is called (implicitly by returning from main or explicitly),
// the registered functions will be called in LIFO order.
exit(EXIT_SUCCESS);
}
Build and Execution Steps
1. Compile the code:
gcc -o atexit_demo atexit_demo.c
2. Run the program:
./atexit_demo
Expected Output:
Program starting. Registering exit handlers.
Handlers registered. Program will now exit normally.
Cleanup handler 2: Closing log files...
Cleanup handler 1: Releasing primary resources...
Explanation: Notice the order of the cleanup messages. cleanup_handler_2
was registered after cleanup_handler_1
, but it was executed first. This confirms that atexit()
handlers are executed in a last-in, first-out (LIFO) sequence, which is crucial for correctly de-allocating resources that have dependencies on each other.
Example 3: Parent Reaping a Child and Checking Exit Status
This is the quintessential system programming example. A parent process forks a child. The child does some “work” and exits with a specific status. The parent waits for the child to terminate and then decodes its exit status.
Code Snippet
Create a file named wait_demo.c
.
// wait_demo.c: Parent process waits for child and checks its exit status.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <exit_code>\n", argv[0]);
exit(EXIT_FAILURE);
}
int child_exit_code = atoi(argv[1]);
if (child_exit_code < 0 || child_exit_code > 255) {
fprintf(stderr, "Exit code must be between 0 and 255.\n");
exit(EXIT_FAILURE);
}
pid_t pid = fork();
if (pid < 0) {
// Fork failed
perror("fork failed");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// --- Child Process ---
printf("CHILD: I am the child process, PID = %d.\n", getpid());
printf("CHILD: I will do my 'work' and then exit with status %d.\n", child_exit_code);
// In a real application, work would be done here.
// We use _exit() in the child to avoid flushing parent's stdio buffers.
_exit(child_exit_code);
} else {
// --- Parent Process ---
printf("PARENT: I am the parent, PID = %d. My child's PID is %d.\n", getpid(), pid);
printf("PARENT: Waiting for my child to terminate...\n");
int status;
pid_t child_pid = wait(&status); // wait() blocks until a child terminates.
if (child_pid < 0) {
perror("wait failed");
exit(EXIT_FAILURE);
}
printf("PARENT: Child process %d has terminated.\n", child_pid);
// Decode the status integer
if (WIFEXITED(status)) {
// Child terminated normally
int exit_status = WEXITSTATUS(status);
printf("PARENT: Child exited normally with status code: %d\n", exit_status);
} else if (WIFSIGNALED(status)) {
// Child was terminated by a signal
int term_signal = WTERMSIG(status);
printf("PARENT: Child was terminated by signal: %d (%s)\n", term_signal, strsignal(term_signal));
} else {
printf("PARENT: Child terminated in an unknown way.\n");
}
}
return EXIT_SUCCESS;
}
Build and Execution Steps
1. Compile the code:
gcc -o wait_demo wait_demo.c
2. Run with a normal exit code (e.g., 42):
./wait_demo 42
Expected Output:
PARENT: I am the parent, PID = <parent_pid>. My child's PID is <child_pid>.
PARENT: Waiting for my child to terminate...
CHILD: I am the child process, PID = <child_pid>.
CHILD: I will do my 'work' and then exit with status 42.
PARENT: Child process <child_pid> has terminated.
PARENT: Child exited normally with status code: 42
Explanation: The parent successfully forks a child. The wait()
call in the parent blocks until the child calls _exit(42)
. Once the child terminates, wait()
returns. The parent then uses the WIFEXITED
macro, which returns true, and WEXITSTATUS
to extract the exact exit code (42) that the child provided.
Demonstrate abnormal termination: We can test the signal-handling path by running the child and killing it from another terminal.
- Terminal 1: Run the program with a long sleep in the child (modify the child’s code to
sleep(30); _exit(0);
and recompile).
./wait_demo 0
- Terminal 2: Find the child’s PID from the output in Terminal 1 and send it a
SIGKILL
signal.
kill -9 <child_pid>
- Return to Terminal 1.Expected Output in Terminal 1:
...
PARENT: Child process <child_pid> has terminated.
PARENT: Child was terminated by signal: 9 (Killed)
Explanation: This time, WIFEXITED
returns false, but WIFSIGNALED
returns true. The parent uses WTERMSIG
to determine that the terminating signal was 9, which corresponds to SIGKILL
. This demonstrates how a parent can diagnose the exact cause of its child’s demise.
Common Mistakes & Troubleshooting
Navigating process termination can be tricky, and several common pitfalls can lead to bugs that are difficult to diagnose. Awareness of these issues is the first step toward avoiding them.
Exercises
These exercises are designed to be completed on your Raspberry Pi 5. They will reinforce the concepts of process termination, cleanup, and status checking.
- Exit Code Reporter
- Objective: Write a program that takes an integer as a command-line argument, creates a child process, and has the child exit with that integer as its status code. The parent must wait for the child and report the exact exit code back to the user.
- Steps:
- Create a C file
reporter.c
. - The
main
function should checkargc
to ensure one command-line argument is provided. Convert this argument to an integer. - Fork the process.
- The child process should print its PID and the exit code it is about to use, then call
_exit()
with that code. - The parent process should call
waitpid()
for the specific child PID, decode the status using theWIFEXITED
andWEXITSTATUS
macros, and print a confirmation message like “Parent: Child <PID> exited with status <code>.”
- Create a C file
- Verification: Run your program with different values (e.g.,
./reporter 0
,./reporter 1
,./reporter 123
) and confirm the parent reports the correct code each time.
- Multi-Handler Cleanup
- Objective: Explore the LIFO execution order of
atexit()
handlers in more detail. - Steps:
- Create a C file
multihandler.c
. - Write three distinct functions:
handler_A
,handler_B
, andhandler_C
. Each function should simply print its name (e.g.,printf("Executing handler A\n");
). - In
main()
, register the handlers in the order A, then B, then C. - After registration, print a message “Exiting main…” and return 0.
- Create a C file
- Verification: Compile and run the program. The output should show the handler messages in the reverse order of registration: C, then B, then A.
- Objective: Explore the LIFO execution order of
- Zombie Maker and Killer
- Objective: Intentionally create a zombie process and observe it using system tools.
- Steps:
- Create a C file
zombie_maker.c
. - In
main()
, fork a child process. - The child process should print its PID and then immediately call
_exit(0)
. - The parent process should print its PID and the child’s PID, and then enter a long sleep (e.g.,
sleep(30);
). The parent should not callwait()
. - Compile and run
./zombie_maker
. - While it’s running, open a second terminal and use the command
ps aux | grep zombie_maker
.
- Create a C file
- Verification: In the
ps
output, you should see the parent process in a sleeping state (S
) and the child process in a zombie state (Z
orZ+
). The child process will be marked with<defunct>
. After 30 seconds, the parent will exit, and the zombie will be reaped byinit
/systemd
and disappear from the process list.
- Signal Interruption
- Objective: Write a parent program that can differentiate between a child’s normal exit and termination by a signal.
- Steps:
- Modify the
wait_demo.c
program from the examples. In the child process block, instead of exiting immediately, make it enter an infinite loop (while(1) { sleep(1); }
). - Compile and run the program. It will print the parent and child PIDs, and the parent will block at the
wait()
call. - From a second terminal, send a signal to the child process PID. First try with
kill -TERM <child_pid>
(orkill -15
). Then run it again and try withkill -INT <child_pid>
(orkill -2
).
- Modify the
- Verification: Observe the parent’s output. It should correctly identify that the child was terminated by a signal and print the correct signal number (15 for
SIGTERM
, 2 forSIGINT
).
Summary
- Process Termination: A process can terminate normally by calling
exit()
,_exit()
, or returning frommain()
, or abnormally when terminated by an unhandled signal. exit()
vs._exit()
: The standard library functionexit()
performs cleanup (callsatexit
handlers, flushesstdio
buffers) before calling the kernel’s_exit()
system call._exit()
terminates the process immediately without user-space cleanup.- Exit Status: A process returns an integer value (0-255) to its parent to indicate its outcome. By convention, 0 means success, and non-zero means failure.
atexit()
: This function registers cleanup handlers that are executed in LIFO order upon normal termination viaexit()
.- Zombies and Orphans: A terminated process becomes a zombie until its parent reaps it by calling
wait()
orwaitpid()
. A process whose parent dies becomes an orphan and is adopted by theinit
process (PID 1). wait()
and Status Decoding: Thewait()
family of functions allows a parent to synchronize with a child’s termination and retrieve its status. This status is a bitmask that must be decoded with macros likeWIFEXITED()
,WEXITSTATUS()
, andWIFSIGNALED()
to be understood correctly.- Robust Programming: Proper termination handling is critical for resource management and building stable, multi-process applications in embedded Linux.
Further Reading
- Linux
man
pages: The definitive source for system call and library function details.man 3 exit
man 2 _exit
man 3 atexit
man 2 wait
- The Single UNIX Specification (POSIX.1-2017): The official standard defining the behavior of these functions.
- “The Linux Programming Interface” by Michael Kerrisk: An exhaustive and highly respected guide to the Linux and UNIX system APIs. Chapters 24-26 cover process creation, termination, and monitoring in great detail.
- “Advanced Programming in the UNIX Environment” by W. Richard Stevens and Stephen A. Rago: The classic text on UNIX system programming. Chapter 8 focuses on process control.
- Raspberry Pi Foundation Documentation: Official hardware and software documentation for the Raspberry Pi platform.
- Robert Love’s “Linux System Programming”: A concise and modern book on Linux system programming, offering clear explanations of core concepts.