Loading content...
Imagine a function executing normally when suddenly—through no deliberate recursion—it's interrupted and called again. This can happen when a signal handler calls the same function that the main code was executing, or when a callback triggers during a function's own execution. If the function can handle this gracefully, surviving its own re-entry, it's called reentrant.
Reentrancy is a subtle but crucial property in systems programming. It's related to but distinct from thread safety, and understanding the difference is essential for writing correct code, especially for signal handlers, callback mechanisms, and any situation where execution can be interrupted and resume in the same code.
By the end of this page, you will understand: (1) the precise definition of reentrancy, (2) how reentrancy differs from thread safety, (3) why reentrancy matters for signal handlers and interrupts, (4) common patterns that break reentrancy, (5) techniques for writing reentrant code, and (6) how to identify and document reentrancy properties.
A function is reentrant if it can be safely interrupted at any point during its execution and called again (re-entered) before the first invocation completes, with both invocations eventually completing correctly.
The Formal Definition:
A function is reentrant if it:
- Does not hold static or global data between invocations
- Does not return a pointer to static data
- Works only on data provided by the caller
- Does not call non-reentrant functions
- Does not use shared resources without proper handling
The Key Scenario:
Consider what happens when:
foo(), currently at line 10foo() (another invocation starts)foo() runs to completion in the signal handler contextfoo() resumes at line 10If foo() works correctly in this scenario, it's reentrant.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
#include <stdio.h>#include <stdlib.h>#include <string.h> // NON-REENTRANT: Uses static bufferchar *uppercase_nonreentrant(const char *s) { static char buffer[1024]; // PROBLEM: Static storage size_t i; for (i = 0; s[i] && i < sizeof(buffer) - 1; i++) { buffer[i] = toupper((unsigned char)s[i]); } buffer[i] = '\0'; return buffer;} // What happens on reentry:// 1. First call: buffer = "HELLO"// 2. Signal interrupts, handler calls uppercase_nonreentrant("world")// 3. buffer = "WORLD" (overwrites!)// 4. Handler returns// 5. First caller's pointer now points to "WORLD", not "HELLO"! // REENTRANT: Caller provides buffer (no static data)char *uppercase_reentrant(const char *s, char *buffer, size_t bufsize) { size_t i; for (i = 0; s[i] && i < bufsize - 1; i++) { buffer[i] = toupper((unsigned char)s[i]); } buffer[i] = '\0'; return buffer;} // Why this is reentrant:// 1. First call uses caller-provided buffer1// 2. Second (reentrant) call uses its own caller-provided buffer2// 3. No shared state between invocations// 4. Each invocation works on independent data // NON-REENTRANT: Maintains state between callschar *strtok_nonreentrant(char *str, const char *delim) { static char *saved; // PROBLEM: Static state if (str) saved = str; // ... tokenizing logic using 'saved' ... return result;} // REENTRANT: State passed by callerchar *strtok_r(char *str, const char *delim, char **saveptr) { if (str) *saveptr = str; // ... tokenizing logic using *saveptr ... return result;}Reentrancy and thread safety are related but distinct concepts. A common misconception is that they're the same—they are not. Understanding the difference is crucial.
Key Distinction:
The Relationship:
| Reentrant | Not Reentrant | |
|---|---|---|
| Thread-Safe | Pure functions, no static/global data, uses recursive locks or lock-free | Uses non-recursive mutex (deadlocks on reentry from signal handler) |
| Not Thread-Safe | Stack-only data but no protection against concurrent access | Static data with no protection (e.g., original strtok) |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
// THREAD-SAFE but NOT REENTRANT// Uses a non-recursive mutex for protection static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;static int shared_counter = 0; void threadsafe_not_reentrant() { pthread_mutex_lock(&mutex); shared_counter++; // If a signal arrives here and handler calls this function... // DEADLOCK! We try to lock an already-locked non-recursive mutex pthread_mutex_unlock(&mutex);} // What happens on reentry:// 1. Thread acquires mutex// 2. Signal interrupts the thread// 3. Handler calls threadsafe_not_reentrant()// 4. Thread (in handler context) tries to acquire mutex// 5. DEADLOCK: mutex is locked by the same thread, but it's non-recursive // REENTRANT but NOT THREAD-SAFE// No global/static data, but no synchronization either void reentrant_not_threadsafe(int *counter) { // Only uses caller-provided data // Reentrant: each call works on its own data // NOT thread-safe: increment isn't atomic (*counter)++;} // Why it's reentrant:// - No static/global data// - If interrupted and called again with different pointer, no conflict// // Why it's not thread-safe:// - If two threads call with same pointer, ++(*counter) is a data race // BOTH REENTRANT AND THREAD-SAFE// Uses atomic operations, no locks, no static data void reentrant_and_threadsafe(atomic_int *counter) { atomic_fetch_add(counter, 1);} // Why it's reentrant:// - No static/global data// - Atomic operation completes even if interrupted//// Why it's thread-safe:// - Atomic operation is safe for concurrent access // Using recursive mutex makes thread-safe code reentrantstatic pthread_mutex_t recursive_mutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;static int protected_value = 0; void recursive_safe() { pthread_mutex_lock(&recursive_mutex); protected_value++; // If reentered, same thread can re-acquire recursive mutex // Still deadlock-free! pthread_mutex_unlock(&recursive_mutex);}Many functions are thread-safe (use locks) but not reentrant (locks cause deadlock on reentry). Signal handlers that call such functions will deadlock because the thread already holds the lock. This is why POSIX specifies a limited set of 'async-signal-safe' functions—these are the functions guaranteed to be both reentrant and safe to call from signal handlers.
Reentrancy is critical in several scenarios where execution can be unexpectedly interrupted and code re-entered:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
#include <signal.h>#include <stdio.h>#include <stdlib.h>#include <unistd.h> // This demonstrates why reentrancy matters for signal handlers volatile sig_atomic_t got_signal = 0; // WRONG: Non-reentrant function called from signal handlervoid signal_handler_WRONG(int sig) { // printf() is NOT async-signal-safe / NOT reentrant! // If main() was in the middle of printf() when signal arrived... // CORRUPTION or DEADLOCK printf("Got signal %d\n", sig); // WRONG! // malloc() is NOT reentrant either // If signal interrupted malloc(), internal heap state is inconsistent char *p = malloc(100); // WRONG! Heap corruption possible free(p);} // CORRECT: Only async-signal-safe operationsvoid signal_handler_CORRECT(int sig) { // sig_atomic_t is specifically designed for this got_signal = 1; // write() is async-signal-safe / reentrant const char msg[] = "Signal received\n"; write(STDOUT_FILENO, msg, sizeof(msg) - 1); // OK} int main() { struct sigaction sa; sa.sa_handler = signal_handler_CORRECT; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGUSR1, &sa, NULL); // Main loop while (1) { // If signal arrives during printf, printf is reentered // (if handler called printf) printf("Working...\n"); if (got_signal) { printf("Main: handling signal flag\n"); got_signal = 0; } sleep(1); } return 0;}The POSIX term 'async-signal-safe' is stronger than just 'reentrant'. An async-signal-safe function is reentrant AND safe to call from signal handlers (no lock acquisition that could deadlock, no access to shared state that could be inconsistent). The list of async-signal-safe functions is explicitly specified by POSIX and is quite limited (about 70 functions). Always consult this list when writing signal handlers.
Certain coding patterns inherently break reentrancy. Recognizing these patterns helps you avoid them and write reentrant code:
static char buffer[100];ctime(), non-reentrant gethostbyname().123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
// Pattern 1: Static local variableint generate_sequence_BAD() { static int counter = 0; // Breaks reentrancy return counter++; // If interrupted and called again, counter++ // On return, original caller sees skipped value} // Pattern 2: Returning pointer to static datachar *format_error_BAD(int errnum) { static char buf[256]; // Breaks reentrancy snprintf(buf, sizeof(buf), "Error %d", errnum); return buf; // If interrupted, buf is overwritten // Original caller's pointer now points to different content} // Pattern 3: Global state modificationstatic struct timeval last_call_time; void record_time_BAD() { gettimeofday(&last_call_time, NULL); // Process last_call_time... // If interrupted after gettimeofday but before processing, // reentrant call overwrites last_call_time // Original processing uses wrong time} // Pattern 4: Non-recursive mutexstatic pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; void protected_operation_BAD() { pthread_mutex_lock(&mutex); // If signal handler calls this function... // Deadlock: trying to lock already-held mutex do_work(); pthread_mutex_unlock(&mutex);} // Pattern 5: Calling non-reentrant functionsvoid wrapper_BAD() { // strtok is not reentrant char *token = strtok(some_string, " "); // If interrupted and handler calls wrapper_BAD()... // strtok's internal state is corrupted process(token);} // Table of common non-reentrant C library functions:/* * Non-reentrant Reentrant Alternative * ------------- --------------------- * strtok() strtok_r() * ctime() ctime_r() * localtime() localtime_r() * gmtime() gmtime_r() * asctime() asctime_r() * getpwnam() getpwnam_r() * getpwuid() getpwuid_r() * gethostbyname() getaddrinfo() * rand() rand_r() or better RNGs * readdir() readdir_r() [deprecated] or use other strategies */POSIX uses the '_r' suffix to denote reentrant versions of traditionally non-reentrant functions. strtok_r(), localtime_r(), gethostbyname_r(), etc. These functions take caller-provided buffers or state pointers instead of using static storage. Always prefer _r versions in signal-safe or threaded code.
Writing reentrant code requires discipline and awareness. Here are the fundamental techniques:
char **saveptr in strtok_r()). Store state in caller-controlled memory.123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
#include <stdlib.h>#include <string.h>#include <time.h> // TECHNIQUE 1: Caller-provided buffers// Non-reentrant (static buffer)char *get_time_string_NONREENTRANT() { static char buffer[64]; time_t now = time(NULL); struct tm *tm = localtime(&now); // Also non-reentrant! strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", tm); return buffer;} // Reentrant (caller provides buffer)char *get_time_string_REENTRANT(char *buffer, size_t bufsize) { time_t now = time(NULL); struct tm tm_result; struct tm *tm = localtime_r(&now, &tm_result); // Reentrant version strftime(buffer, bufsize, "%Y-%m-%d %H:%M:%S", tm); return buffer;} // Usage:// char my_buffer[64];// get_time_string_REENTRANT(my_buffer, sizeof(my_buffer)); // TECHNIQUE 2: Pass state explicitly// Non-reentrant tokenizer (internal state)typedef struct { char *str; char *saveptr;} Tokenizer; void tokenizer_init(Tokenizer *t, char *str) { t->str = str; t->saveptr = NULL;} // Reentrant: state held in caller's Tokenizer structchar *tokenizer_next(Tokenizer *t, const char *delim) { return strtok_r(t->str, delim, &t->saveptr);} // TECHNIQUE 3: Pure functions (no side effects)// Inherently reentrant: works only on inputs, produces outputint sum_array(const int *arr, size_t len) { int total = 0; for (size_t i = 0; i < len; i++) { total += arr[i]; } return total; // No static state, no global access, no I/O // Perfectly reentrant} // TECHNIQUE 4: Context object patterntypedef struct { // All state for this "session" char buffer[4096]; size_t offset; int flags; // ... more state ...} ParserContext; // Functions take context as first parametervoid parser_init(ParserContext *ctx);int parser_parse(ParserContext *ctx, const char *input);const char *parser_get_result(ParserContext *ctx); // Each invocation (including reentrant ones) uses its own context// Caller is responsible for providing distinct contextsPure functions—those with no side effects that depend only on their inputs—are inherently reentrant. They use no global state, return derived values (not pointers to internal data), and call only other pure functions. While not always practical, pure functions are the gold standard for reentrancy. Functional programming languages leverage this for inherent thread safety and reentrancy.
In practice, achieving reentrancy involves trade-offs and careful design decisions. Here are real-world patterns and considerations:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
// PRACTICE 1: Signal-safe memory operations// Can't use malloc in signal handlers, but can use pre-allocated pools typedef struct SignalSafePool { char buffer[4096]; size_t used;} SignalSafePool; // Global, per-thread, or context-specific pool__thread SignalSafePool signal_pool = {0}; // Bump allocator: reentrant (each call advances the pointer)void *signal_safe_alloc(SignalSafePool *pool, size_t size) { if (pool->used + size > sizeof(pool->buffer)) { return NULL; // Out of space } void *ptr = &pool->buffer[pool->used]; pool->used += size; return ptr; // Note: No "free" operation - pool is reset as a unit} // PRACTICE 2: Lock-free data structures for reentrant access#include <stdatomic.h> typedef struct { atomic_int flag; int data;} ReentrantFlag; // Reentrant set operation (atomic)void set_flag(ReentrantFlag *f, int value) { atomic_store(&f->flag, 1); // Write barrier implicit in atomic_store with default memory order f->data = value;} // PRACTICE 3: Reentrant logging// Can't use fprintf in reentrant context, use write() #define REENTRANT_LOG_BUFFER_SIZE 256 void reentrant_log(const char *msg) { char buffer[REENTRANT_LOG_BUFFER_SIZE]; size_t len = strlen(msg); if (len >= sizeof(buffer)) len = sizeof(buffer) - 1; memcpy(buffer, msg, len); buffer[len] = '\n'; write(STDERR_FILENO, buffer, len + 1); // write() is async-signal-safe/reentrant} // For formatted output in signal handlers:void reentrant_log_int(const char *prefix, int value) { char buffer[REENTRANT_LOG_BUFFER_SIZE]; char *p = buffer; // Copy prefix while (*prefix && p < buffer + sizeof(buffer) - 16) { *p++ = *prefix++; } // Convert int to string (manual, no sprintf) if (value < 0) { *p++ = '-'; value = -value; } char digits[12]; int i = 0; do { digits[i++] = '0' + (value % 10); value /= 10; } while (value); while (i > 0) { *p++ = digits[--i]; } *p++ = '\n'; write(STDERR_FILENO, buffer, p - buffer);} // PRACTICE 4: Recursive-safe callbackstypedef struct Callback { void (*fn)(void *data, int depth); void *data; int max_depth;} Callback; void invoke_callback(Callback *cb, int depth) { if (depth >= cb->max_depth) { // Prevent infinite recursion return; } cb->fn(cb->data, depth);}| Category | Functions |
|---|---|
| Process | _exit, _Exit, abort, execve, fork, getpid, getppid |
| Signals | kill, raise, signal, sigaction, sigprocmask, sigsuspend |
| I/O | read, write, open, close, dup, dup2, pipe, fcntl |
| File | access, chmod, chown, link, unlink, rename, stat, mkdir |
| Time | alarm, clock_gettime, nanosleep, time |
| Memory | (no heap functions) - must use pre-allocated or stack memory |
Even though errno is thread-local, a signal handler can corrupt the main code's errno. If a signal handler calls a function that sets errno, and the main code was checking errno, the value is lost. Safe signal handlers should save and restore errno: int saved_errno = errno; /* do work */ errno = saved_errno;
Testing for reentrancy is challenging because it requires simulating interruption at arbitrary points. Here are strategies for verification:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
#include <signal.h>#include <stdio.h>#include <setjmp.h> // Testing reentrancy by forcing reentry via signals // Function under testchar *func_under_test(char *buf, size_t size); // Test statestatic sigjmp_buf jump_buffer;static int reentry_result_valid;static char reentry_buffer[256];static char reentry_result[256]; void reentry_signal_handler(int sig) { // Call function again from signal handler char result[256]; func_under_test(result, sizeof(result)); // Store result for comparison reentry_result_valid = 1; strcpy(reentry_result, result); // Return to test} void test_reentrancy() { char main_result[256]; // Set up signal handler struct sigaction sa; sa.sa_handler = reentry_signal_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGUSR1, &sa, NULL); reentry_result_valid = 0; // Call function func_under_test(main_result, sizeof(main_result)); // During the call above, arrange for SIGUSR1 to be delivered // This can be done with a timer or another thread // After function completes, check: // 1. main_result is correct (not corrupted by reentry) // 2. reentry_result is correct (reentry succeeded) if (reentry_result_valid) { printf("Main result: %s\n", main_result); printf("Reentry result: %s\n", reentry_result); // Verify both are correct }} // Documentation pattern: /** * @brief Converts a string to uppercase. * * @param src Source string * @param dst Destination buffer (caller-provided) * @param dst_size Size of destination buffer * * @reentrant Yes - uses only caller-provided buffers * @async-signal-safe Yes - no heap allocation, no locks * * @thread-safety Safe if different threads use different buffers. * Not safe if threads share dst buffer. */char *to_uppercase(const char *src, char *dst, size_t dst_size);Always document the reentrancy and async-signal-safety properties of your functions. Use consistent terminology: 'reentrant', 'async-signal-safe', 'not reentrant', 'calls non-reentrant functions'. This documentation helps users decide if a function is safe for their signal handlers or interrupt contexts.
Reentrancy is a subtle but essential property for systems programming, especially when dealing with signals, interrupts, and callbacks. Let's consolidate the key concepts:
You have now completed the Thread Issues module. You understand thread cancellation, signal handling in threads, thread-local storage, thread safety, and reentrancy—the critical challenges in concurrent programming. These concepts form the foundation for building robust, correct, and maintainable multithreaded software. The next chapter will build on this foundation to explore synchronization primitives and concurrent programming patterns.