How sig_atomic_t Guarantees Safe Signal Handling in C
In the C programming language, asynchronous signal handlers can
interrupt normal execution at virtually any moment, introducing the risk
of data corruption if shared variables are modified mid-operation. The
sig_atomic_t type addresses this problem by providing an
integer type guaranteed by the C standard to be read and written in a
single, indivisible (atomic) operation. When paired with the
volatile qualifier, sig_atomic_t allows a
program's main execution flow and signal handlers to share state flags
safely without the risk of torn reads, torn writes, or inconsistent
data.
The Problem: Asynchronous Interrupts and Torn Writes
When an operating system sends a signal to a process (such as
SIGINT or SIGTERM), the execution of the main
program halts immediately, and the CPU jumps to the corresponding signal
handler function. Once the handler finishes, execution resumes where it
left off.
If the main program is in the middle of updating a multi-byte variable when a signal arrives, the update may require multiple CPU instructions. For example, updating a 64-bit integer on a 32-bit architecture typically takes two 32-bit store operations. If a signal handler runs after the first store but before the second, the variable enters an inconsistent state known as a "torn write." If the handler reads or writes that variable, it operates on corrupt data.
How sig_atomic_t
Provides Safety
Defined in the <signal.h> header,
sig_atomic_t is tailored by the C implementation to avoid
torn memory access. It achieves safe state updates through specific
architectural and standards-based guarantees:
1. Indivisible Load and Store Operations
The C standard defines sig_atomic_t as an integer type
that can be accessed as an atomic entity, even in the presence of
asynchronous signals. The compiler and platform architecture map
sig_atomic_t to an underlying native integer size (such as
an int or CPU word) where read (load) and write (store)
instructions execute in a single machine cycle. Because the processor
cannot interrupt itself in the middle of a single load or store
instruction, the value is guaranteed never to be partially written.
2. Standardized Limits
The C standard mandates that sig_atomic_t must support
at least the range of values from -127 to 127 (or 0 to 255 if unsigned),
though most modern architectures implement it as a standard 32-bit or
64-bit signed integer. The macro constants SIG_ATOMIC_MIN
and SIG_ATOMIC_MAX (defined in
<stdint.h>) provide the exact range supported by the
target platform.
The Necessity of the
volatile Qualifier
While sig_atomic_t guarantees that CPU-level memory
transfers are atomic, it does not prevent compiler-level optimizations.
Compilers often cache variables in CPU registers instead of reading them
directly from RAM.
If a loop in the main program checks a flag:
while (!exit_flag) {
/* work */
}The compiler might optimize the check by reading
exit_flag into a register once and never checking physical
memory again. To prevent this, variables shared with signal handlers
must be declared as volatile sig_atomic_t:
volatile sig_atomic_t exit_flag = 0;The volatile keyword instructs the compiler to read from
and write to actual memory every single time the variable is referenced,
ensuring that updates made by a signal handler are immediately visible
to the main program loop.
Critical Limitations of
sig_atomic_t
Although sig_atomic_t prevents torn reads and writes,
its safety guarantees are strictly limited to simple load and store
operations:
- No Read-Modify-Write Safety: Operations like
flag++,--flag, orflag += 1are not atomic. They consist of three distinct operations: reading the value, modifying it in a register, and writing it back. If a signal occurs between the read and the write, race conditions still occur. - Simple Assignments Only: Safe usage of
sig_atomic_twithin signal handlers is generally limited to directly reading the variable or setting it to a literal constant (e.g.,flag = 1;). - Not a Replacement for Multi-threading Atomics:
sig_atomic_tis explicitly designed for single-thread signal interruption, not multi-core concurrent thread synchronization. For thread safety across multiple threads, standard C11 atomics (<stdatomic.h>) or platform-specific mutexes must be used instead.