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, or flag += 1 are 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_t within 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_t is 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.