How Signal Masking Protects Critical Sections in C

This article explains how signal masking in the C programming language prevents asynchronous interrupts from corrupting shared data and program state within critical execution paths. Readers will learn the mechanics of asynchronous signals, how race conditions emerge during uninterrupted execution needs, and the practical implementation of POSIX functions like sigprocmask() to temporarily defer signal delivery until critical tasks complete safely.

The Threat of Asynchronous Interruption

In C, signals are asynchronous notifications sent by the operating system to notify a process of an event (such as SIGINT, SIGTERM, or SIGALRM). Because signals can arrive at virtually any CPU instruction, the kernel immediately halts normal program execution to run the registered signal handler.

A critical section is a sequence of instructions that must execute atomically—meaning without interruption—to maintain program integrity. Typical critical sections involve updating shared global variables, manipulating complex data structures such as linked lists or ring buffers, and interacting with non-reentrant standard library functions like malloc(). If an asynchronous signal interrupts code halfway through modifying a shared pointer and the signal handler attempts to access that same pointer, the result is undefined behavior, race conditions, or memory corruption.

How Signal Masking Works

Signal masking allows a thread to temporarily block the delivery of specific signals. When a signal is masked (or "blocked"):

  1. The kernel does not discard the signal when it is generated.
  2. The signal is placed into a "pending" state for the process or thread.
  3. Execution inside the critical section continues uninterrupted.
  4. When the signal mask is cleared or restored, the kernel immediately delivers any pending signals, invoking their respective handlers.

By blocking signals right before entering a critical section and unblocking them immediately upon exit, a program creates an artificial boundary of atomicity relative to signal delivery. Note that SIGKILL and SIGSTOP cannot be masked or caught.

Implementing Signal Masking in POSIX C

The POSIX standard provides the sigset_t data type and the sigprocmask() function (or pthread_sigmask() for multi-threaded applications) to manipulate the signal mask.

Essential Functions

  • sigemptyset(sigset_t *set): Initializes a signal set to exclude all signals.
  • sigfillset(sigset_t *set): Initializes a signal set to include all signals.
  • sigaddset(sigset_t *set, int signum): Adds a specific signal to the set.
  • sigdelset(sigset_t *set, int signum): Removes a specific signal from the set.
  • sigprocmask(int how, const sigset_t *set, sigset_t *oldset): Examines and changes the blocked signal mask.

The how parameter determines the behavior:

  • SIG_BLOCK: Adds the signals in set to the current mask.
  • SIG_UNBLOCK: Removes the signals in set from the current mask.
  • SIG_SETMASK: Overwrites the current mask completely with set.

Practical Pattern

To safeguard a critical section properly, always save the existing mask in oldset so it can be restored accurately, preserving any prior configurations.

#include <signal.h>
#include <stdio.h>

void critical_section(void) {
    sigset_t block_set, old_set;

    // Define which signals to mask
    sigemptyset(&block_set);
    sigaddset(&block_set, SIGINT);
    sigaddset(&block_set, SIGTERM);

    // 1. Block the signals and save the current mask
    if (sigprocmask(SIG_BLOCK, &block_set, &old_set) < 0) {
        perror("sigprocmask - block");
        return;
    }

    // 2. CRITICAL SECTION START
    // Operations here cannot be interrupted by SIGINT or SIGTERM
    // e.g., modifying global state or hardware registers
    // CRITICAL SECTION END

    // 3. Restore the original signal mask
    // Any pending SIGINT or SIGTERM is delivered immediately here
    if (sigprocmask(SIG_SETMASK, &old_set, NULL) < 0) {
        perror("sigprocmask - restore");
    }
}

Best Practices

  • Always Restore, Do Not Blindly Unblock: Use SIG_SETMASK with old_set rather than SIG_UNBLOCK. This prevents inadvertently unblocking signals that were intentionally blocked by a caller higher up the call stack.
  • Keep Critical Sections Brief: Keep the masked region as short as possible. Blocking signals indefinitely delays essential system notifications and can make an application unresponsive to termination requests.
  • Consider Multi-Threading: In multi-threaded programs using pthreads, use pthread_sigmask() instead of sigprocmask(), as sigprocmask() behavior in multi-threaded environments is undefined under POSIX standards. Each thread maintains its own independent signal mask.