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"):
- The kernel does not discard the signal when it is generated.
- The signal is placed into a "pending" state for the process or thread.
- Execution inside the critical section continues uninterrupted.
- 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 insetto the current mask.SIG_UNBLOCK: Removes the signals insetfrom the current mask.SIG_SETMASK: Overwrites the current mask completely withset.
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_SETMASKwithold_setrather thanSIG_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, usepthread_sigmask()instead ofsigprocmask(), assigprocmask()behavior in multi-threaded environments is undefined under POSIX standards. Each thread maintains its own independent signal mask.