Chapter 21.1.3 — Global Variables & sig_atomic_t

 

Chapter 21.1.3 — Global Variables & sig_atomic_t
Linux Systems Programming · EmbeddedPathashala
21.1.3
Section
3
Code Examples
6
Interview Qs

Key Terms
sig_atomic_t volatile Atomic Access SIG_ATOMIC_MIN SIG_ATOMIC_MAX Register Optimization Non-atomic Read/Write Race Condition

The Problem: Sharing Variables Between Main and Handler

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:

Problem 1: Compiler Optimization

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.

Fix: declare with volatile
Problem 2: Non-atomic Access

On 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.

Fix: use type sig_atomic_t

The Solution: volatile sig_atomic_t

The 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;
Important Limitation: The ++ (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.

Code Example 1 — Correct Usage of volatile sig_atomic_t
#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;
}

Code Example 2 — Dangerous: What Happens Without volatile
#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;
}
Compile both versions:
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.

Code Example 3 — Multiple Flags for Multiple Signal Types
#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;
}
Important: When you reset a flag in main (e.g., 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.

Interview Questions — Global Variables & sig_atomic_t
Q1. What is sig_atomic_t and why do we need it?
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.
Q2. Why is volatile needed in addition to sig_atomic_t?
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.
Q3. Is it safe to use ++ on a sig_atomic_t variable inside a signal handler?
No. The C standard does NOT guarantee that the ++ 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.
Q4. What is the guaranteed range of sig_atomic_t on Linux?
On Linux, 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.
Q5. Can you use a plain int with volatile to share a variable between a handler and main program?
In practice, yes for simple flag variables on most architectures (where 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.
Q6. A colleague writes: int flag = 0; in a handler they do flag++; in main they check if (flag > 0). What are the two bugs?
Bug 1: Missing 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.

Next: Other Ways to Terminate a Signal Handler →

Chapter 21.2 — Nonlocal Goto & abort() ← Previous

Leave a Reply

Your email address will not be published. Required fields are marked *