Every Linux process eventually ends. There are exactly two broad categories of termination: abnormal and normal.
| Type | How it happens | Core dump? | Common examples |
|---|---|---|---|
| Abnormal | A signal is delivered whose default action is to terminate the process | Sometimes (SIGSEGV, SIGABRT) | SIGSEGV, SIGKILL, SIGABRT, SIGFPE |
| Normal | Process calls _exit() directly, or calls exit(), or returns from main() |
No | exit(0), return 0; in main |
This part focuses on normal termination and specifically the difference between the raw system call _exit() and the C library wrapper exit().
_exit() System Call_exit() is the raw Linux system call for process termination. It goes directly into the kernel — no userspace cleanup happens.
#include <unistd.h>
void _exit(int status);
Key facts you must know:
_exit()never returns — the process always terminates successfully.- The
statusargument is the termination status passed to the parent process. - Although
statusis declared asint, only the bottom 8 bits (0–255) are actually made available to the parent viawait(). - By convention: 0 = success, non-zero = failure.
- Values > 128 should be avoided — the shell uses
128 + signal_numberto indicate signal-caused termination, and you can get confusion. - Does NOT flush stdio buffers.
- Does NOT call
atexit()handlers. _exit()is UNIX/POSIX specific — declared in<unistd.h>.
What _exit() skips (compared to exit()):
exit() C Library Functionexit() is a C standard library function that wraps _exit() with extra userspace cleanup steps. Most programs call exit(), not _exit() directly.
#include <stdlib.h>
void exit(int status);
Exact steps performed by exit():
atexit() and on_exit() — in reverse order of registration_exit(status) — process terminates, kernel takes over_exit(): Unlike _exit() which is UNIX-specific, exit() is part of the ISO C standard and available on every C platform (Linux, Windows, macOS, embedded). Always include <stdlib.h>.When a child process exits, its parent can retrieve the termination status using wait() or waitpid() combined with the macro WEXITSTATUS().
How the status bits work:
| Bits 31–8 (ignored) | Bits 7–0 (sent to parent) | = 0 to 255 |
| exit() call | int value | Bits 7–0 (hex) | Parent sees via WEXITSTATUS() |
|---|---|---|---|
exit(0) |
0 | 0x00 | 0 (success) |
exit(1) |
1 | 0x01 | 1 (failure) |
exit(42) |
42 | 0x2A | 42 |
exit(256) |
256 | 0x00 (overflow) | 0 — looks like success! |
exit(-1) |
-1 | 0xFF (two’s complement) | 255 — not -1 ! |
SUSv3 (POSIX) defines two portable constants so your code communicates intent clearly. Both are defined in <stdlib.h>.
| Constant | Typical value | Use case | Shell $? |
|---|---|---|---|
EXIT_SUCCESS |
0 | Process completed its job without errors | 0 |
EXIT_FAILURE |
1 | Process encountered an error | 1 |
Using these constants instead of raw 0 and 1 makes code self-documenting and portable across all C platforms.
main() vs Calling exit()Doing return n; from main() is generally equivalent to calling exit(n) — the C runtime startup code that called main() uses the return value in a call to exit().
However there is one critical exception:
exit() processing access local variables of main() (e.g., you called setvbuf() or setbuf() with a pointer to a local array), then returning from main() causes undefined behaviour because those stack variables are destroyed on return. In this case you must call exit() explicitly rather than returning.Behaviour when falling off end of main() or returning without a value:
| C Standard / Compile flag | Behaviour | Exit status seen by parent |
|---|---|---|
| C89 (default gcc on Linux) | Undefined behaviour — exit status taken from stack/register garbage | Random / unpredictable |
C99+ (gcc -std=c99 or newer) |
Equivalent to calling exit(0) |
0 (success) |
Best practice: Always write an explicit return 0; (or return EXIT_SUCCESS;) at the end of main(), and always compile with at least -std=c99.
Coding Examples
exit() flushes, _exit() does notThis program writes to stdout without a newline (so data stays in the stdio buffer), then calls either exit() or _exit() depending on a command-line argument. Redirect output to a file to clearly see the buffer flushing difference.
/* demo_exit_flush.c
* Compile : gcc -Wall -o demo_exit_flush demo_exit_flush.c
* Run : ./demo_exit_flush 0 > out.txt && cat out.txt → data appears
* ./demo_exit_flush 1 > out.txt && cat out.txt → data missing!
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
/* No '\n' — data stays in stdio buffer, NOT yet written to kernel */
printf("stdio buffered text (no newline)");
/* write() goes straight to kernel buffer — always visible */
write(STDOUT_FILENO, "\n[write() output is always immediate]\n", 38);
if (argc > 1 && argv[1][0] == '1') {
/*
* _exit() terminates immediately.
* stdio buffer is NEVER flushed.
* The printf text above is LOST when output goes to a file.
*/
_exit(EXIT_SUCCESS);
} else {
/*
* exit() calls fflush() on all open streams before _exit().
* The printf text WILL appear even without a newline.
*/
exit(EXIT_SUCCESS);
}
}
[write() output is always immediate]stdio buffered text (no newline)
Expected output with _exit() (./demo_exit_flush 1 > out.txt):
[write() output is always immediate]
(printf text is missing — buffer was never flushed)
wait()Demonstrates the 8-bit truncation rule. The child calls exit(-1) but the parent sees 255 — also shows EXIT_SUCCESS / EXIT_FAILURE usage.
/* demo_exit_status.c
* Compile : gcc -Wall -o demo_exit_status demo_exit_status.c
* Run : ./demo_exit_status
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
static void fork_and_check(int exit_val)
{
pid_t pid;
int wstatus;
pid = fork();
if (pid == -1) { perror("fork"); exit(EXIT_FAILURE); }
if (pid == 0) {
/* CHILD */
printf(" Child PID %d: calling exit(%d)\n", getpid(), exit_val);
exit(exit_val); /* only bottom 8 bits reach parent */
}
/* PARENT */
if (waitpid(pid, &wstatus, 0) == -1) {
perror("waitpid");
exit(EXIT_FAILURE);
}
if (WIFEXITED(wstatus))
printf(" Parent: WEXITSTATUS = %d\n\n", WEXITSTATUS(wstatus));
}
int main(void)
{
printf("--- exit(0) → parent sees: ---\n");
fork_and_check(0); /* parent sees 0 */
printf("--- exit(42) → parent sees: ---\n");
fork_and_check(42); /* parent sees 42 */
printf("--- exit(-1) → parent sees: ---\n");
fork_and_check(-1); /* parent sees 255 ! (-1 & 0xFF = 0xFF = 255) */
printf("--- exit(256) → parent sees: ---\n");
fork_and_check(256); /* parent sees 0 ! (256 & 0xFF = 0) */
return EXIT_SUCCESS;
}
Child PID ...: calling exit(-1)Parent: WEXITSTATUS = 255
Child PID ...: calling exit(256)
Parent: WEXITSTATUS = 0 ← looks like success, but child passed 256!
main()Shows how the exit status differs when main() has no explicit return, compiled under C89 vs C99.
/* demo_no_return.c
* Compile C89 : gcc -std=c89 -Wall -o demo89 demo_no_return.c
* ./demo89 ; echo "Exit status: $?" → random (may not be 0)
*
* Compile C99 : gcc -std=c99 -Wall -o demo99 demo_no_return.c
* ./demo99 ; echo "Exit status: $?" → always 0
*/
#include <stdio.h>
int main(void)
{
printf("Running with no explicit return or exit()\n");
printf("Under C89: exit status is UNDEFINED (stack junk)\n");
printf("Under C99: equivalent to return 0 (exit status = 0)\n");
/* No return statement — intentionally omitted for demonstration */
}
return EXIT_SUCCESS; at the end of main(). Do not rely on implicit return behaviour. Use -std=c99 or newer in all your projects.Interview Questions & Answers
_exit() and exit() in Linux?Answer: _exit() is a direct Linux system call (declared in <unistd.h>) that immediately terminates the process. It does not flush stdio buffers and does not call atexit handlers. exit() is a C library function (declared in <stdlib.h>) that first calls all atexit/on_exit handlers in reverse order, then flushes all open stdio streams, and finally calls _exit(). Use _exit() in child processes after fork() to avoid double-flushing parent’s stdio buffers.
exit(-1)?Answer: Only the bottom 8 bits (bits 0–7) of the status argument are made available to the parent via WEXITSTATUS(). When a child calls exit(-1), the value -1 in binary is 0xFFFFFFFF (32-bit two’s complement). Only the bottom 8 bits (0xFF = 255) are passed, so the parent sees 255, not -1. This is why exercise 25-1 in the book asks exactly this question.
Answer: The shell uses the convention that when a process is killed by a signal, $? is set to 128 + signal_number. For example, SIGINT (signal 2) gives $? = 130. If a process explicitly calls exit(130), the shell cannot distinguish whether the process was killed by SIGINT or terminated voluntarily with status 130. This causes shell script logic errors.
return 0; in main() always identical to exit(0);?Answer: Usually yes, but not always. If an atexit handler or code run during exit() processing accesses local variables of main() (e.g., a local buffer passed to setvbuf()), then returning from main() causes undefined behaviour because those stack variables are destroyed. Calling exit() explicitly is safe because the local variables are still in scope.
_exit() close open file descriptors?Answer: Yes. File descriptors are closed by the kernel as part of process termination, regardless of whether _exit() or exit() is called. What _exit() skips is the userspace stdio buffer flush. Data still in the userspace stdio buffer (not yet handed to the kernel) will be lost; data already in the kernel buffer cache is safe and will be written to disk.
_exit() declared vs exit()? Why does it matter?Answer: _exit() → #include <unistd.h> (POSIX, UNIX-specific). exit() → #include <stdlib.h> (ISO C standard, cross-platform). Including the wrong header causes implicit declaration warnings or errors. On 64-bit systems an implicit function declaration can lead to incorrect pointer-size assumptions and stack corruption.
main() falls off the end without a return value in C89 vs C99?Answer: In C89, this is undefined behaviour — the exit status is whatever value happens to be in a particular CPU register or stack location at that point. In C99 and later, falling off the end of main() is equivalent to return 0;, so the process exits with status 0. Always compile with -std=c99 or later and add an explicit return EXIT_SUCCESS; for clarity.
Next: Part 2 — Details of Process Termination
What does the kernel actually do when a process exits? File descriptors, file locks, shared memory, semaphores, SIGHUP, memory mappings — all 10 kernel cleanup actions explained with code.
