Signal handlers often need to communicate with the main program — the most common way is a shared global variable. But this has two subtle problems:
The compiler may cache the variable in a CPU register, so the main program never re-reads it from memory. The handler updates RAM but the main loop keeps reading the stale register value — the change is invisible.
volatileOn some architectures, reading or writing a variable takes more than one machine instruction. A signal can fire between these instructions, leaving the variable in a half-updated state.
sig_atomic_tThe C standard provides the type sig_atomic_t (defined in <signal.h>) specifically for this purpose. Reads and writes to this type are guaranteed to be atomic on every POSIX-compliant platform.
You must combine it with volatile to prevent compiler optimizations from caching the value in a register.
/* Correct declaration of a flag shared between main() and a signal handler */
static volatile sig_atomic_t flag = 0;
++ (increment) and -- (decrement) operators are NOT guaranteed to be atomic on all architectures, even for sig_atomic_t. On some hardware, increment requires a read + modify + write sequence, which can be interrupted. You should only set the variable in the handler and read/check it in main (or vice versa).| Declaration | Compiler Optimized? | Atomic? | Safe? |
|---|---|---|---|
int flag; |
May be cached | Not guaranteed | No |
volatile int flag; |
Never cached | Not guaranteed | Partial |
volatile sig_atomic_t flag; |
Never cached | Guaranteed | Yes |
On Linux, SIG_ATOMIC_MIN and SIG_ATOMIC_MAX are defined as the limits of a signed 32-bit integer (–2147483648 to 2147483647). POSIX guarantees at minimum –127 to 127.
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
/*
* CORRECT: volatile prevents compiler from caching in a register.
* sig_atomic_t guarantees atomic set/read.
*/
static volatile sig_atomic_t shutdown_requested = 0;
static volatile sig_atomic_t alarm_fired = 0;
static void sigint_handler(int sig)
{
shutdown_requested = 1; /* Only SET — do not increment */
}
static void sigalrm_handler(int sig)
{
alarm_fired = 1; /* Only SET */
}
int main(void)
{
struct sigaction sa;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = sigint_handler;
sigaction(SIGINT, &sa, NULL);
sa.sa_handler = sigalrm_handler;
sigaction(SIGALRM, &sa, NULL);
alarm(5); /* Fire SIGALRM after 5 seconds */
printf("PID=%d. Ctrl+C to stop, or wait 5s for alarm.\n", (int)getpid());
while (!shutdown_requested && !alarm_fired) {
/*
* Main loop safely reads both flags.
* Because they are volatile, the compiler MUST re-read from memory
* every iteration — it cannot cache the value in a register.
*/
sleep(1);
printf("Working...\n");
}
if (shutdown_requested)
printf("Stopped by SIGINT.\n");
if (alarm_fired)
printf("Stopped by SIGALRM after 5 seconds.\n");
return 0;
}
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
/*
* WRONG: no volatile. With -O2 optimization, the compiler MAY
* hoist the check outside the loop, turning:
*
* while (!flag) { ... }
*
* into:
*
* if (!flag) { while(1) { ... } } // flag never re-read!
*
* The signal handler sets flag = 1, but the loop never sees it.
*/
static sig_atomic_t flag = 0; /* Missing volatile! */
static void handler(int sig) { flag = 1; }
int main(void)
{
signal(SIGINT, handler);
/*
* With aggressive optimization (-O2), this loop may run forever
* even after Ctrl+C, because the compiler cached 'flag' in a register.
* Adding 'volatile' fixes this.
*/
while (!flag) {
/* do work */
}
printf("Flag was set.\n");
return 0;
}
Without volatile:
gcc -O2 -o no_volatile no_volatile.c — may loop forever.With volatile:
gcc -O2 -o with_volatile with_volatile.c — stops correctly.#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
/* One flag per signal type of interest */
static volatile sig_atomic_t got_sigint = 0;
static volatile sig_atomic_t got_sigterm = 0;
static volatile sig_atomic_t got_sighup = 0;
static volatile sig_atomic_t got_sigusr1 = 0;
/*
* A single handler for multiple signals.
* We use 'sig' to decide which flag to set.
*/
static void generic_handler(int sig)
{
switch (sig) {
case SIGINT: got_sigint = 1; break;
case SIGTERM: got_sigterm = 1; break;
case SIGHUP: got_sighup = 1; break;
case SIGUSR1: got_sigusr1 = 1; break;
}
}
int main(void)
{
struct sigaction sa;
sa.sa_handler = generic_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGHUP, &sa, NULL);
sigaction(SIGUSR1, &sa, NULL);
printf("PID=%d. Ready. Send signals: kill -INT/-TERM/-HUP/-USR1 %d\n",
(int)getpid(), (int)getpid());
while (1) {
/* Check all flags in main loop */
if (got_sigint) { printf("[Main] SIGINT received\n"); got_sigint = 0; break; }
if (got_sigterm) { printf("[Main] SIGTERM received\n"); got_sigterm = 0; break; }
if (got_sighup) { printf("[Main] SIGHUP — reload config\n"); got_sighup = 0; }
if (got_sigusr1) { printf("[Main] SIGUSR1 — status dump\n"); got_sigusr1 = 0; }
sleep(1);
}
printf("Shutting down cleanly.\n");
return 0;
}
got_sighup = 0), you must do so AFTER you have finished processing it. Resetting before processing creates a window where a second signal could be missed.sig_atomic_t is an integer type defined in <signal.h> for which reads and writes are guaranteed to be atomic — meaning they complete in a single machine instruction and cannot be interrupted mid-operation. We need it because reading/writing larger types (like long long) may require multiple instructions, and a signal arriving between them can see a half-written value.sig_atomic_t guarantees atomicity at the machine instruction level. But volatile tells the compiler to never cache the variable in a register — it must re-read from memory every access. Without volatile, an optimising compiler may cache the value in a register and the main loop never sees updates made by the signal handler in memory.++ operator is atomic for sig_atomic_t. On some architectures, increment is a read-modify-write sequence of three instructions. A signal can arrive between the read and the write, causing a lost update. Only simple assignment (set) and comparison (read) are guaranteed safe.SIG_ATOMIC_MIN = –2147483648 and SIG_ATOMIC_MAX = 2147483647 (signed 32-bit integer range). POSIX requires at minimum –127 to 127 for signed, or 0–255 for unsigned. Since the typical use is a flag (0/1), the exact range rarely matters in practice.int read/write is naturally atomic). But it is not portable or standards-compliant. The correct way per POSIX is volatile sig_atomic_t, which is guaranteed to work on all conformant platforms.volatile — the compiler may cache flag in a register and main never sees the update. Bug 2: flag++ is not atomic — on some architectures it is a 3-step read-modify-write; a second signal between the read and write causes a lost update. Fix: volatile sig_atomic_t flag = 0; and set flag = 1 (not flag++) in the handler.