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.
on_exit() APIon_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.
#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.
| 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 |
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).
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.
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
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 */
}
on_exit function called: status=2, arg=20atexit function 2 calledatexit function 1 calledon_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).
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);
}
status argument; free(ctx) done inside the handler to avoid memory leaks.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);
}
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
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.
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).
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
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.
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.
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.
