Chapter 25 — Process Termination

Chapter 25 — Process Termination
Part 4 of 5  |  Exit Handlers — on_exit() and the Full Example Program
on_exit()
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

atexit() has two limitations: handlers cannot know the exit status and cannot receive arguments. GNU libc provides on_exit() to address both. This part covers the on_exit() API in full, the exact output of the textbook example program (Listing 25-1), how mixed atexit()/on_exit() registrations are ordered, and portability considerations.

Keywords in this part:

on_exit() exit status in handler void* arg in handler _BSD_SOURCE _SVID_SOURCE mixed atexit + on_exit shared handler list portability glibc extension non-standard

§ A — The on_exit() API

on_exit() is a glibc extension (not in any POSIX standard) that addresses both limitations of atexit() by passing the exit status and a custom argument to the handler.

Required feature test macro + header:

#define _BSD_SOURCE    /* or: #define _SVID_SOURCE */
#include <stdlib.h>

Signature:

int on_exit(void (*func)(int, void *), void *arg);

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

The handler function must have this signature:

void my_handler(int exit_status, void *arg)
{
    /*
     * exit_status : the value passed to exit()
     * arg         : the void* passed to on_exit() at registration time
     */
}

The arg parameter is typed as void* but is open to programmer interpretation. Through judicious casting it can carry an integer, a pointer to a structure, or any other scalar value.

§ B — atexit() vs on_exit() — Complete Comparison
Feature atexit() on_exit()
Standard ISO C, SUSv3, POSIX — fully portable glibc only — NOT in any standard
Header <stdlib.h> <stdlib.h> + _BSD_SOURCE or _SVID_SOURCE
Handler signature void f(void) void f(int status, void *arg)
Exit status available ✗ No ✓ Yes — passed as first argument
Custom argument ✗ No ✓ Yes — void* passed at registration
Multiple registrations ✓ Yes — shared list with on_exit ✓ Yes — shared list with atexit
Return value 0 on success, nonzero on error 0 on success, nonzero on error
Portability recommendation Use for all portable programs Avoid in portable code — Linux/glibc only

§ C — Mixed Registration: atexit() and on_exit() Share One List

Functions registered using atexit() and on_exit() are placed on the same internal list. When exit() runs, all registered handlers (regardless of which function registered them) are called in reverse order of their registration.

Example registration sequence and call order:

Registration order Function call Call order when exit() runs
1st on_exit(onexitFunc, (void*)10) Called 4th (last)
2nd atexit(atexitFunc1) Called 3rd
3rd atexit(atexitFunc2) Called 2nd
4th on_exit(onexitFunc, (void*)20) Called 1st (first)

This matches the exact output from Listing 25-1 in the textbook (see Example 1 below).

§ D — Portability Warning for on_exit()

on_exit() is not covered by any standard (not POSIX, not SUSv3, not ISO C) and is available on very few UNIX implementations beyond Linux/glibc.

Recommendation: Avoid on_exit() in programs intended to be portable across UNIX systems (macOS, FreeBSD, Solaris, etc.). Use a global variable to pass context to an atexit() handler instead. Reserve on_exit() for Linux-only programs where you genuinely need the exit status or argument in the handler.

Coding Examples

Example 1 — Textbook Listing 25-1: Mixed atexit() and on_exit() Handlers

This is the complete example from the textbook (Listing 25-1, file procexec/exit_handlers.c). Study the registration order and match it against the output.

/* exit_handlers.c  (Listing 25-1 from TLPI)
 * Compile : gcc -Wall -D_BSD_SOURCE -o exit_handlers exit_handlers.c
 * Run     : ./exit_handlers
 *
 * Expected output:
 *   on_exit function called: status=2, arg=20
 *   atexit function 2 called
 *   atexit function 1 called
 *   on_exit function called: status=2, arg=10
 */
#define _BSD_SOURCE          /* Required to expose on_exit() declaration */
#include <stdlib.h>
#include <stdio.h>

/* ---- atexit handlers (no arguments, no exit status) ---- */
static void atexitFunc1(void)
{
    printf("atexit function 1 called\n");
}

static void atexitFunc2(void)
{
    printf("atexit function 2 called\n");
}

/* ---- on_exit handler (receives exit status AND custom arg) ---- */
static void onexitFunc(int exitStatus, void *arg)
{
    printf("on_exit function called: status=%d, arg=%ld\n",
           exitStatus, (long)arg);
}

int main(int argc, char *argv[])
{
    (void)argc; (void)argv;

    /*
     * Registration order:
     *   1. on_exit(onexitFunc, 10)   ← registered first → called LAST
     *   2. atexit(atexitFunc1)
     *   3. atexit(atexitFunc2)
     *   4. on_exit(onexitFunc, 20)   ← registered last  → called FIRST
     *
     * All four are on the SAME internal list.
     * Calling order = reverse of registration order.
     */

    if (on_exit(onexitFunc, (void *)10) != 0) {
        fprintf(stderr, "on_exit 1 failed\n"); exit(EXIT_FAILURE);
    }
    if (atexit(atexitFunc1) != 0) {
        fprintf(stderr, "atexit 1 failed\n"); exit(EXIT_FAILURE);
    }
    if (atexit(atexitFunc2) != 0) {
        fprintf(stderr, "atexit 2 failed\n"); exit(EXIT_FAILURE);
    }
    if (on_exit(onexitFunc, (void *)20) != 0) {
        fprintf(stderr, "on_exit 2 failed\n"); exit(EXIT_FAILURE);
    }

    exit(2);   /* This value (2) appears in both on_exit handler calls */
}
Exact expected output:
on_exit function called: status=2, arg=20
atexit function 2 called
atexit function 1 called
on_exit function called: status=2, arg=10

Why? Registration was: on_exit(10) → atexit1 → atexit2 → on_exit(20).
Reverse of that is: on_exit(20) → atexit2 → atexit1 → on_exit(10).

Example 2 — Using on_exit() to Perform Conditional Cleanup Based on Exit Status

This is a practical use of on_exit()‘s unique ability: performing different cleanup actions depending on whether the program exited successfully or with an error.

/* demo_on_exit_status.c
 * Compile : gcc -Wall -D_BSD_SOURCE -o demo_on_exit_status demo_on_exit_status.c
 * Run     : ./demo_on_exit_status 0   → exits with EXIT_SUCCESS
 *           ./demo_on_exit_status 1   → exits with EXIT_FAILURE
 */
#define _BSD_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Context structure to pass via void* arg */
typedef struct {
    const char *logfile;
    const char *tempfile;
} CleanupCtx;

static void cleanup_handler(int status, void *arg)
{
    CleanupCtx *ctx = (CleanupCtx *)arg;

    printf("\n[on_exit handler] exit status = %d\n", status);

    if (status == EXIT_SUCCESS) {
        printf("  Success path: removing temp file '%s'\n", ctx->tempfile);
        /* unlink(ctx->tempfile); */   /* would actually delete it */
        printf("  Success path: writing 'OK' to log '%s'\n", ctx->logfile);
    } else {
        printf("  Failure path: keeping temp file '%s' for debugging\n",
               ctx->tempfile);
        printf("  Failure path: writing 'FAILED (status=%d)' to log '%s'\n",
               status, ctx->logfile);
    }

    free(ctx);   /* free the heap-allocated context */
}

int main(int argc, char *argv[])
{
    int simulate_failure = (argc > 1 && argv[1][0] == '1');

    /* Allocate context on the heap (not stack — handler runs after main returns) */
    CleanupCtx *ctx = malloc(sizeof(CleanupCtx));
    if (!ctx) { perror("malloc"); exit(EXIT_FAILURE); }
    ctx->logfile  = "/var/log/myapp.log";
    ctx->tempfile = "/tmp/myapp_work.tmp";

    /* Register handler with context pointer */
    if (on_exit(cleanup_handler, ctx) != 0) {
        perror("on_exit");
        free(ctx);
        exit(EXIT_FAILURE);
    }

    printf("Program running (PID %d)...\n", getpid());

    if (simulate_failure) {
        fprintf(stderr, "Simulated error — exiting with EXIT_FAILURE\n");
        exit(EXIT_FAILURE);
    }

    printf("Work complete — exiting with EXIT_SUCCESS\n");
    exit(EXIT_SUCCESS);
}
Key points demonstrated: Heap-allocated context (safe because it persists after main() returns); different cleanup paths based on status argument; free(ctx) done inside the handler to avoid memory leaks.

Example 3 — Using void* arg as Integer (No struct needed)

The arg parameter of on_exit() is void* but can be used to carry an integer directly via casting — just as the textbook example does with (void*)10 and (void*)20.

/* demo_on_exit_intarg.c
 * Compile : gcc -Wall -D_BSD_SOURCE -o demo_on_exit_intarg demo_on_exit_intarg.c
 * Run     : ./demo_on_exit_intarg
 *
 * Registers the same handler function three times with different integer
 * arguments. Shows how one handler function can serve multiple contexts.
 */
#define _BSD_SOURCE
#include <stdio.h>
#include <stdlib.h>

/*
 * Generic cleanup handler.
 * 'arg' is actually an int disguised as void* — we cast it back.
 */
static void generic_cleanup(int exit_status, void *arg)
{
    int resource_id = (int)(long)arg;   /* safe cast: void* → long → int */
    printf("  Cleaning up resource #%d (exit_status=%d)\n",
           resource_id, exit_status);
    /* In reality: release resource_id (fd, shmid, semid ...) */
}

int main(void)
{
    /* Register same handler for three different "resources" */
    if (on_exit(generic_cleanup, (void *)1L) != 0) goto fail;
    if (on_exit(generic_cleanup, (void *)2L) != 0) goto fail;
    if (on_exit(generic_cleanup, (void *)3L) != 0) goto fail;

    printf("Registered 3 handlers for resource IDs 1, 2, 3\n");
    printf("Calling exit(0) ...\n");
    exit(0);

fail:
    perror("on_exit");
    exit(EXIT_FAILURE);
}
Expected output:
Cleaning up resource #3 (exit_status=0)
Cleaning up resource #2 (exit_status=0)
Cleaning up resource #1 (exit_status=0)

Note the cast chain: (void*)1L — use long as intermediate type to avoid truncation on 64-bit systems where sizeof(long)==sizeof(void*). Never cast directly through int if sizeof(int) < sizeof(void*).

Interview Questions & Answers

Q1. What are the two limitations of atexit() that on_exit() solves?

Answer: (1) No exit status: atexit() handlers have no way to know what value was passed to exit(). on_exit() passes the exit status as the first argument to the handler. (2) No argument: atexit() handlers take no parameters, making it impossible to register the same function with different contexts. on_exit() passes a void* argument at registration time which is forwarded to the handler on each call.

Q2. Are atexit() and on_exit() handlers on separate lists?

Answer: No. Functions registered with both atexit() and on_exit() are placed on the same internal list. When exit() runs, it processes this single list in reverse registration order, calling each entry appropriately (atexit-style entries with no args, on_exit-style with status and arg).

Q3. What is the exact output of the textbook Listing 25-1 example and why?

Answer: Registration order is: on_exit(onexitFunc,10) → atexit(func1) → atexit(func2) → on_exit(onexitFunc,20), then exit(2) is called. Reverse order gives: on_exit(20) first, then func2, then func1, then on_exit(10) last. So output is:
on_exit function called: status=2, arg=20
atexit function 2 called
atexit function 1 called
on_exit function called: status=2, arg=10

Q4. Why should context passed to on_exit() be heap-allocated, not stack-allocated?

Answer: Exit handlers are called after exit() is invoked and potentially after main() has returned. If the arg pointer points to a local variable in main() (or any other stack frame that has been destroyed), the handler will access deallocated stack memory — causing undefined behaviour and likely a crash. Always use heap allocation (malloc()) for context structures passed to on_exit(), and free them inside the handler.

Q5. What is the correct way to cast an integer as the void* arg in on_exit()?

Answer: Use long as an intermediate type: register as (void *)(long)my_int and retrieve as (int)(long)arg. On 64-bit systems sizeof(void*) == 8 but sizeof(int) == 4, so casting directly (void*)(int) or back (int)(void*) can cause compiler warnings or truncation. Using long as an intermediary is safe because sizeof(long) == sizeof(void*) on all standard 64-bit Linux platforms.

Q6. Is on_exit() available on macOS, FreeBSD, or other UNIX systems?

Answer: on_exit() is a glibc-specific extension — it is not covered by any POSIX standard and is available on very few UNIX implementations outside of Linux. macOS (darwin libc), FreeBSD libc, and other UNIX systems generally do not provide it. For portable code, use atexit() with a global context variable. Use on_exit() only for Linux-specific programs.

Next: Part 5 — fork(), stdio Buffers, and _exit()

The famous “why does printf appear twice?” puzzle — how fork() duplicates userspace stdio buffers, why write() output is different, and all solutions to prevent duplicate output.

Go to Part 5 → EmbeddedPathashala Home

Leave a Reply

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