Chapter 25 — Process Termination

Chapter 25 — Process Termination
Part 1 of 5  |  Terminating a Process: _exit() and exit()
5
Parts in Series
3
Code Examples
7
Interview Q&As

Series Navigation: Part 1: _exit() & exit()| Part 2: Kernel Cleanup Details| Part 3: Exit Handlers – atexit()| Part 4: Exit Handlers – on_exit()| Part 5: fork() + stdio Buffers

Keywords in this part:

_exit() exit() termination status EXIT_SUCCESS EXIT_FAILURE stdio buffer flush atexit handlers return from main() C89 vs C99 abnormal termination normal termination WEXITSTATUS()

§ A — Two Ways a Process Can Terminate

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().

§ B — The _exit() System Call

_exit() is the raw Linux system call for process termination. It goes directly into the kernel — no userspace cleanup happens.

Header & Signature:

#include <unistd.h>

void _exit(int status);

Key facts you must know:

  • _exit() never returns — the process always terminates successfully.
  • The status argument is the termination status passed to the parent process.
  • Although status is declared as int, only the bottom 8 bits (0–255) are actually made available to the parent via wait().
  • By convention: 0 = success, non-zero = failure.
  • Values > 128 should be avoided — the shell uses 128 + signal_number to 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()):

✗ Does NOT call atexit() handlers
✗ Does NOT flush stdio buffers
✓ Kernel closes file descriptors
✓ Kernel performs all cleanup

§ C — The exit() C Library Function

exit() is a C standard library function that wraps _exit() with extra userspace cleanup steps. Most programs call exit(), not _exit() directly.

Header & Signature:

#include <stdlib.h>

void exit(int status);

Exact steps performed by exit():

1
Call all exit handlers registered with atexit() and on_exit() — in reverse order of registration
2
Flush all open stdio stream buffers (e.g., whatever is still buffered in stdout, stderr)
3
Call _exit(status) — process terminates, kernel takes over
Key difference from _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>.

§ D — Termination Status & the 8-Bit Rule

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 !
⚠ Important: Never rely on values > 255 or negative values being passed correctly. Always use values in range 0–125 for clean portable behaviour. Shell scripts use 126 (command not executable), 127 (command not found), and 128+ (signals).

§ E — EXIT_SUCCESS and EXIT_FAILURE Constants

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.

§ F — Returning from 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:

⚠ Undefined Behaviour Warning: If any steps performed during 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

Example 1 — Seeing the Difference: exit() flushes, _exit() does not

This 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);
    }
}
Expected output with exit() (./demo_exit_flush 0 > out.txt):
[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)

Example 2 — Parent reads child’s exit status with 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;
}
Expected output:
Child PID ...: calling exit(-1)
Parent: WEXITSTATUS = 255

Child PID ...: calling exit(256)
Parent: WEXITSTATUS = 0  ← looks like success, but child passed 256!

Example 3 — C89 vs C99: falling off end of 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 */
}
Best practice rule: Always add 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

Q1. What is the difference between _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.

Q2. How many bits of exit status does the parent process receive? What happens with 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.

Q3. Why should exit status values greater than 128 be avoided?

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.

Q4. Is 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.

Q5. Does _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.

Q6. Where is _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.

Q7. What is the behaviour when 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.

Go to Part 2 → EmbeddedPathashala Home

Leave a Reply

Your email address will not be published. Required fields are marked *