<csignal>
C-style signal handling in C++ — signal(), raise(), std::sig_atomic_t, and the strict rules for writing portable, async-signal-safe handlers.
<csignal>since C++98The <csignal> header exposes the C library's signal facilities (std::signal, std::raise, std::sig_atomic_t, and the SIG* macros), letting a program install an asynchronous handler for a small set of implementation-defined events such as SIGINT and SIGTERM.
Overview
<csignal> is a thin wrapper over C's <signal.h>. It is available in every C++ standard and has barely changed. The C++ standard gives it very little defined behaviour, so portable code must stay inside a narrow envelope.
| Entity | Purpose |
|---|---|
std::signal(sig, handler) | Install a handler; returns the previous handler or SIG_ERR |
std::raise(sig) | Synchronously send a signal to the calling program |
std::sig_atomic_t | Integer type that can be read and written safely relative to a signal handler |
SIG_DFL, SIG_IGN, SIG_ERR | Default action, ignore, and error return |
SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM | The six signals the standard names |
The handler type is void(int) with C language linkage. Strictly, the handler must have C linkage, but every mainstream implementation accepts a plain C++ free function or captureless lambda converted to a function pointer.
What the standard guarantees
Inside a signal handler, a program that is not using <csignal> for a synchronous raise or abort is limited to:
- Reading or writing volatile
std::sig_atomic_tobjects. - Reading or writing lock-free
std::atomic<T>objects (since C++11, viastd::atomic<T>::is_always_lock_freefrom C++17), and usingstd::atomic_signal_fence. - Calling
std::_Exit,std::abort,std::quick_exit(C++11), orstd::signalfor the same signal.
Anything else (malloc, new, printf, std::cout, taking a mutex, throwing an exception) is undefined behaviour under the standard. POSIX widens the list to "async-signal-safe" functions such as write, _exit, and sigaction, but that is a POSIX guarantee, not a C++ one.
Practical rule: a handler should set a flag and return. Do the real work on a normal thread.
Syntax
#include <csignal>
using handler_t = void (*)(int);
handler_t std::signal(int sig, handler_t handler);
int std::raise(int sig);
using sig_atomic_t = /* implementation-defined integer type */;std::signal returns the previous handler on success and SIG_ERR on failure (check errno). On POSIX, the semantics of signal() vary historically (handler reset after delivery on System V, persistent on BSD/glibc). Prefer sigaction when portability across Unix flavours matters.
Examples
Graceful shutdown flag
#include <csignal>
#include <chrono>
#include <iostream>
#include <thread>
namespace {
volatile std::sig_atomic_t g_stop = 0;
extern "C" void on_signal(int) {
g_stop = 1; // only touch sig_atomic_t
}
} // namespace
int main() {
if (std::signal(SIGINT, on_signal) == SIG_ERR ||
std::signal(SIGTERM, on_signal) == SIG_ERR) {
std::cerr << "failed to install handler\n";
return 1;
}
while (!g_stop) {
// do work
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // C++11
}
std::cout << "shutting down cleanly\n";
}Lock-free atomic flag (C++11 and later)
volatile sig_atomic_t provides no inter-thread synchronization. If another thread must observe the flag, use a lock-free atomic.
#include <atomic>
#include <csignal>
std::atomic<bool> g_quit{false}; // C++11
static_assert(std::atomic<bool>::is_always_lock_free); // C++17
extern "C" void on_term(int) {
g_quit.store(true, std::memory_order_relaxed);
}
void install() { std::signal(SIGTERM, on_term); }Counting signals with a captureless lambda
A handler cannot carry state, so state lives in a global or static. A captureless lambda (C++11) converts to a function pointer:
#include <csignal>
static volatile std::sig_atomic_t hits = 0;
void setup() {
std::signal(SIGINT, [](int) { hits = hits + 1; }); // C++11 lambda
}Note that hits++ on a volatile is deprecated in C++20 (compound operations on volatile types); write hits = hits + 1 as above. Also, the counter is not guaranteed atomic with respect to a second delivery, so treat it as approximate.
Self-pipe for real work (POSIX)
When you need to wake an event loop, write one byte to a pipe from the handler. write is async-signal-safe on POSIX.
#include <csignal>
#include <unistd.h> // POSIX, not standard C++
#include <cerrno>
static int g_wake_fd = -1; // write end of a pipe, set before installing
extern "C" void on_int(int) {
int saved = errno; // preserve errno
const char b = 1;
(void)!::write(g_wake_fd, &b, 1);
errno = saved;
}The event loop polls the read end and performs cleanup on a regular thread.
Testing a handler with raise
#include <csignal>
#include <cassert>
static volatile std::sig_atomic_t seen = 0;
int main() {
std::signal(SIGINT, [](int s) { seen = s; });
std::raise(SIGINT); // handler runs before raise returns
assert(seen == SIGINT);
}For raise, the handler runs synchronously in the calling thread before raise returns, which makes it a convenient unit-test hook.
Best Practices
- Set a flag, return. Keep the handler to a
volatile std::sig_atomic_tor lock-free atomic store. - Save and restore
errnoif the handler calls anything that can modify it. - Install handlers before spawning threads, and decide deliberately which thread receives asynchronous signals. On POSIX, block signals in worker threads with
pthread_sigmaskand dedicate one thread tosigwait. - Prefer
sigaction(POSIX) overstd::signalforSA_RESTART,SA_SIGINFO, and well-defined re-arming semantics. - Use
std::atomicfor inter-thread flags, and assertis_always_lock_free(C++17) or checkis_lock_free()(C++11) at startup. - Wrap in RAII. A small guard that installs a handler and restores the previous one on destruction keeps tests and libraries tidy.
- On Windows, only
SIGINT,SIGTERM,SIGABRT,SIGFPE,SIGILL,SIGSEGVexist, and console control events are better handled viaSetConsoleCtrlHandler.
Common Pitfalls
- Calling non-safe functions.
std::cout << ...,printf,new,std::stringoperations, andstd::mutex::lockcan deadlock or corrupt state if the signal interrupts the same facility. - Throwing from a handler. Exceptions do not propagate across the signal frame. Behaviour is undefined, though some platforms offer non-portable
-fnon-call-exceptionstricks for synchronous signals. - Returning from
SIGSEGV/SIGFPE/SIGILLhandlers. Returning from a handler for a computational-exception signal is undefined behaviour. Log with safe calls and_Exit. - Using plain
boolorint. A non-sig_atomic_t, non-atomic object accessed in a handler is a data race / undefined behaviour, and the compiler may hoist the load out of your loop. - Assuming
volatilemeans thread-safe.volatileonly constrains the compiler against the handler in the same thread; it is not a synchronization primitive between threads. - Using
std::functionorstd::bindhandlers.std::signaltakes a bare function pointer; stateful callables need a global or static trampoline. Wrapping withstd::functioninside the handler also risks allocation. - Relying on handler persistence. On some systems the disposition resets to
SIG_DFLafter delivery; re-install inside the handler if portability to such systems matters. - Signal-unsafe static initialization. Referencing a function-local static from a handler may trigger its first-time initialization (which can lock). Initialize before installing.
- Treating it as modern C++. Many style guides advise against
<csignal>in general application code. Use it only at the process boundary (shutdown, diagnostics) and keep everything else in ordinary C++ constructs, such as condition variables,std::stop_token(C++20), and event loops.
Cooperation with C++20 stop tokens
A common bridge is: handler sets a lock-free atomic, a watcher thread polls it (or is woken by a self-pipe) and calls std::stop_source::request_stop() (C++20). request_stop is not async-signal-safe, so never call it directly from the handler.