Chapter 25 — Process Termination

Chapter 25 — Process Termination
Part 3 of 5  |  Exit Handlers — atexit()
atexit()
Focus of this part
3
Code Examples
6
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

What You Will Learn

Sometimes a library or application needs automatic cleanup to run when the process exits — regardless of where in the code exit() is called. Exit handlers registered with atexit() solve exactly this problem. This part covers: what exit handlers are, how to register them, the order in which they are called, their limitations, and how they interact with fork() and exec().

Keywords in this part:

exit handler atexit() registration order reverse call order SC_ATEXIT_MAX fork() + atexit exec() clears handlers _exit() skips handlers signal + exit handler SIGKILL limitation disable exit handler

§ A — What Is an Exit Handler?

An exit handler is a programmer-defined function that is registered at some point during the life of a process and is then automatically called during normal process termination via exit().

The classic use-case: A library that needs guaranteed cleanup:

Problem:
A library does not control when or how the main program exits. It cannot mandate the caller to run a cleanup function before calling exit().
Solution:
During library initialisation, register a cleanup function with atexit(). It will be called automatically when exit() runs — from anywhere in the program.
⚠ Important: Exit handlers are NOT called if:

  • The program calls _exit() directly (bypasses all handlers)
  • The process is terminated abnormally by a signal (e.g., SIGSEGV, SIGKILL)

This limits their utility for guaranteed cleanup, but they still cover the most common case of normal exit().

§ B — The atexit() API
Header & Signature:

#include <stdlib.h>

int atexit(void (*func)(void));

Returns: 0 on success, nonzero (not necessarily -1) on error.

The handler function must have this exact signature:

void cleanup_function(void)
{
    /* No parameters, no return value */
    /* Perform cleanup here */
}

Key rules for atexit():

  • The function takes no arguments and returns void.
  • Multiple handlers can be registered — even the same function multiple times.
  • Handlers are called in reverse order of registration (last registered = first called).
  • Each handler should not call exit() — SUSv3 says behaviour is undefined (on Linux, remaining handlers are still called, but don’t rely on this).
  • If a handler calls _exit() or is killed by a signal, remaining handlers are NOT called.
  • You cannot deregister a handler once registered with atexit(). Workaround: use a global flag the handler checks.

§ C — Calling Order: Reverse Registration Order

When exit() runs, registered handlers are called in reverse order of their registration. This is logical: handlers registered first typically set up more fundamental resources that should be cleaned up last.

Step Registration sequence Call sequence (when exit() runs) Reason
1st atexit(handlerA) Called LAST (3rd) Fundamental setup → cleaned up last
2nd atexit(handlerB) Called 2nd
3rd atexit(handlerC) Called FIRST (1st) Last registered → first cleaned up

Inside a handler, you can register additional exit handlers — they are placed at the head of the remaining list and will be called before the handlers already in the queue.

§ D — Maximum Number of Exit Handlers

SUSv3 requires an implementation to support at least 32 exit handlers. You can query the implementation-defined limit with:

long max_handlers = sysconf(_SC_ATEXIT_MAX);
printf("Max atexit handlers: %ld\n", max_handlers);

On Linux (glibc), sysconf(_SC_ATEXIT_MAX) returns 2,147,482,647 (the maximum signed 32-bit integer). This is because glibc stores registered handlers in a dynamically allocated linked list, so the limit is effectively infinite — you will run out of memory long before hitting it.

Note: There is no way to find out how many handlers have already been registered at runtime. If you need to know, maintain your own counter.

§ E — Two Key Limitations of atexit()

Handlers registered with atexit() have two important limitations:

Limitation Description Workaround
No exit status access The handler does not know what status value was passed to exit(). Cannot perform different cleanup based on success/failure. Use a global variable set before calling exit(), or use on_exit() (Part 4).
No argument passing The handler takes no arguments. Cannot register the same function multiple times with different contexts. Use a global context structure, or use on_exit() which supports a void* argument.

§ F — Signals, SIGKILL, and the Exit Handler Problem

Exit handlers are not called when a process is terminated by a signal. The recommended approach is:

  • Install signal handlers for signals that might arrive (SIGTERM, SIGHUP, SIGINT, etc.).
  • In the signal handler, set a global flag.
  • The main program loop checks the flag and calls exit() cleanly.
  • Because exit() is not async-signal-safe, calling it directly from a signal handler is technically unsafe.
SIGKILL cannot be caught, blocked, or ignored. There is no way to run exit handlers when a process is killed with SIGKILL. This is why you should always prefer sending SIGTERM (which can be caught) over SIGKILL to terminate a process gracefully.

§ G — How fork() and exec() Interact with Exit Handlers
Operation Effect on registered atexit handlers Reason
fork() Child inherits a copy of parent’s handler registrations fork() duplicates the entire process, including the atexit list
exec() All registrations are removed exec() replaces process image — the old handler code no longer exists in memory
Practical warning: After fork(), if both parent and child call exit(), both will run the atexit handlers. This can cause double cleanup (e.g., deleting a temp file twice, writing to a log file twice). Best practice: child processes should call _exit() instead of exit() to avoid running the parent’s atexit handlers.

§ H — How to Disable a Registered Exit Handler

atexit() provides no deregistration mechanism. The standard workaround is to use a global flag:

static int cleanup_enabled = 1;   /* global flag */

void my_cleanup(void)
{
    if (!cleanup_enabled)
        return;                   /* disabled — do nothing */
    /* ... actual cleanup ... */
}

/* To disable: */
cleanup_enabled = 0;

This pattern lets you turn off specific exit handlers at runtime without removing them.

Coding Examples

Example 1 — Reverse Call Order of Multiple atexit() Handlers

Register four handlers in order A→B→C→D, then call exit(). Watch them execute in reverse: D→C→B→A.

/* demo_atexit_order.c
 * Compile : gcc -Wall -o demo_atexit_order demo_atexit_order.c
 * Run     : ./demo_atexit_order
 */
#include <stdio.h>
#include <stdlib.h>

static void handlerA(void) { printf("handlerA called (registered 1st, called LAST)\n"); }
static void handlerB(void) { printf("handlerB called (registered 2nd)\n"); }
static void handlerC(void) { printf("handlerC called (registered 3rd)\n"); }
static void handlerD(void) { printf("handlerD called (registered 4th, called FIRST)\n"); }

int main(void)
{
    printf("--- Registering handlers A, B, C, D ---\n");

    if (atexit(handlerA) != 0) { perror("atexit A"); exit(1); }
    if (atexit(handlerB) != 0) { perror("atexit B"); exit(1); }
    if (atexit(handlerC) != 0) { perror("atexit C"); exit(1); }
    if (atexit(handlerD) != 0) { perror("atexit D"); exit(1); }

    printf("--- Calling exit(0) now ---\n");
    exit(0);

    /* unreachable */
    return 0;
}
Expected output:
--- Registering handlers A, B, C, D ---
--- Calling exit(0) now ---
handlerD called (registered 4th, called FIRST)
handlerC called (registered 3rd)
handlerB called (registered 2nd)
handlerA called (registered 1st, called LAST)

Example 2 — Exit Handler for Temp File Cleanup with Signal Handling

Real-world pattern: atexit handler deletes a temp file. Signal handler sets a flag to trigger clean exit() so the handler still runs on SIGTERM/SIGINT.

/* demo_atexit_tempfile.c
 * Compile : gcc -Wall -o demo_atexit_tempfile demo_atexit_tempfile.c
 * Run     : ./demo_atexit_tempfile
 *           Then in another terminal: kill -SIGTERM <pid>
 *           The temp file is cleaned up automatically.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>

#define TMPFILE "/tmp/ep_demo_temp.txt"

static volatile sig_atomic_t terminate = 0;

/* ---- Exit handler: always clean up temp file ---- */
static void remove_tmpfile(void)
{
    if (unlink(TMPFILE) == 0)
        printf("  [atexit] Removed temp file: %s\n", TMPFILE);
    else
        /* File may not exist if exit is called before file creation */
        perror("  [atexit] unlink");
}

/* ---- Signal handler: set flag, let main() call exit() ---- */
static void sig_handler(int sig)
{
    (void)sig;
    terminate = 1;
    /* Do NOT call exit() here — it is not async-signal-safe */
}

int main(void)
{
    int fd;
    struct sigaction sa;

    /* Register signal handlers for clean termination */
    sa.sa_handler = sig_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGTERM, &sa, NULL);
    sigaction(SIGINT,  &sa, NULL);

    /* Register atexit handler BEFORE creating the resource */
    if (atexit(remove_tmpfile) != 0) {
        perror("atexit");
        exit(EXIT_FAILURE);
    }

    /* Create temp file */
    fd = open(TMPFILE, O_WRONLY | O_CREAT | O_TRUNC, 0600);
    if (fd == -1) { perror("open"); exit(EXIT_FAILURE); }
    write(fd, "temporary data\n", 15);
    close(fd);
    printf("Created temp file: %s\n", TMPFILE);
    printf("PID = %d  — send SIGTERM to test cleanup.\n", getpid());
    printf("Sleeping 30 seconds (Ctrl-C also works) ...\n");

    /* Main loop — exit cleanly on signal so atexit handler runs */
    while (!terminate)
        sleep(1);

    printf("Signal received — calling exit() ...\n");
    exit(EXIT_SUCCESS);   /* ← atexit handler will be called here */
}
Key point: The signal handler only sets a flag. The main loop calls exit(), which triggers the atexit handler. If the process were killed with SIGKILL, the handler would NOT run and the temp file would remain.

Example 3 — fork() Inherits Handlers: Use _exit() in Child

Shows the double-cleanup problem when child calls exit() and the fix using _exit() in the child.

/* demo_atexit_fork.c
 * Compile : gcc -Wall -o demo_atexit_fork demo_atexit_fork.c
 * Run     : ./demo_atexit_fork 0   → child uses exit()  → double handler call
 *           ./demo_atexit_fork 1   → child uses _exit() → handler called once
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

static int call_count = 0;

static void cleanup_handler(void)
{
    call_count++;
    printf("  cleanup_handler called (PID %d, call_count=%d)\n",
           getpid(), call_count);
}

int main(int argc, char *argv[])
{
    int child_use_exit = (argc > 1 && argv[1][0] == '0') ? 1 : 0;
    pid_t pid;

    /* Register handler before fork() */
    if (atexit(cleanup_handler) != 0) { perror("atexit"); exit(1); }

    pid = fork();
    if (pid == -1) { perror("fork"); exit(1); }

    if (pid == 0) {
        /* CHILD */
        printf("Child (PID %d): using %s\n", getpid(),
               child_use_exit ? "exit() — handler will run (double cleanup!)"
                              : "_exit() — handler skipped (correct!)");

        if (child_use_exit)
            exit(EXIT_SUCCESS);    /* triggers inherited atexit handler! */
        else
            _exit(EXIT_SUCCESS);   /* skips atexit handlers — correct for child */
    }

    /* PARENT */
    waitpid(pid, NULL, 0);
    printf("Parent (PID %d): calling exit()\n", getpid());
    exit(EXIT_SUCCESS);   /* parent's atexit handler runs once */
}
With ./demo_atexit_fork 0 (child uses exit): handler called twice — once by child, once by parent.
With ./demo_atexit_fork 1 (child uses _exit): handler called once — only by parent. This is the correct pattern.

Interview Questions & Answers

Q1. What is an exit handler and when is it called?

Answer: An exit handler is a programmer-defined function registered via atexit() or on_exit() that is automatically called during normal process termination — i.e., when exit() is called or when main() returns. Exit handlers are not called when the process calls _exit() directly, or when the process is terminated abnormally by a signal (including SIGKILL).

Q2. In what order are multiple atexit() handlers called?

Answer: In reverse order of registration. The last function registered with atexit() is called first when exit() runs. This ensures that handlers performing higher-level cleanup (registered later) run before handlers performing lower-level cleanup (registered earlier). If the same function is registered multiple times, it is called multiple times.

Q3. How does fork() affect registered atexit() handlers?

Answer: A child created by fork() inherits a copy of its parent’s atexit handler registrations. If both parent and child call exit(), both will execute the registered handlers, which can cause double cleanup bugs. The standard solution is to call _exit() in the child process instead of exit(), so only the parent runs the atexit handlers.

Q4. What happens to atexit() handlers when exec() is called?

Answer: All atexit handler registrations are removed when a process calls exec(). This is necessarily so because exec() replaces the process’s entire code segment — the handler functions no longer exist in memory. The new program loaded by exec() starts with no atexit handlers registered.

Q5. Why can’t you call exit() from inside an atexit handler? What should you do instead?

Answer: SUSv3 states that calling exit() from within an atexit handler causes undefined behaviour (on Linux, remaining handlers are still called, but this is non-portable). If an atexit handler needs to abort the exit process, it should call _exit() directly (which will stop all remaining handlers) or use raise() to send a signal. Portable applications should simply avoid calling exit() inside atexit handlers.

Q6. How do you disable an atexit handler that has already been registered?

Answer: atexit() provides no deregistration mechanism. The standard workaround is to protect the handler body with a global flag: set the flag to 1 when the cleanup should run; the handler checks the flag and returns immediately if it is 0. To disable the handler, set the flag to 0 before calling exit(). The handler function is still called, but performs no action.

Next: Part 4 — Exit Handlers with on_exit()

on_exit() solves atexit()’s two main limitations — it passes the exit status AND a custom argument to the handler. Full comparison, example program walkthrough, and portability notes.

Go to Part 4 → EmbeddedPathashala Home

Leave a Reply

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