Skip to content
C++
Library
since C++98
Advanced

<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++98

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

EntityPurpose
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_tInteger type that can be read and written safely relative to a signal handler
SIG_DFL, SIG_IGN, SIG_ERRDefault action, ignore, and error return
SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERMThe 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_t objects.
  • Reading or writing lock-free std::atomic<T> objects (since C++11, via std::atomic<T>::is_always_lock_free from C++17), and using std::atomic_signal_fence.
  • Calling std::_Exit, std::abort, std::quick_exit (C++11), or std::signal for 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

cpp
#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

cpp
#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.

cpp
#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:

cpp
#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.

cpp
#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

cpp
#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_t or lock-free atomic store.
  • Save and restore errno if 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_sigmask and dedicate one thread to sigwait.
  • Prefer sigaction (POSIX) over std::signal for SA_RESTART, SA_SIGINFO, and well-defined re-arming semantics.
  • Use std::atomic for inter-thread flags, and assert is_always_lock_free (C++17) or check is_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, SIGSEGV exist, and console control events are better handled via SetConsoleCtrlHandler.

Common Pitfalls

  • Calling non-safe functions. std::cout << ..., printf, new, std::string operations, and std::mutex::lock can 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-exceptions tricks for synchronous signals.
  • Returning from SIGSEGV/SIGFPE/SIGILL handlers. Returning from a handler for a computational-exception signal is undefined behaviour. Log with safe calls and _Exit.
  • Using plain bool or int. 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 volatile means thread-safe. volatile only constrains the compiler against the handler in the same thread; it is not a synchronization primitive between threads.
  • Using std::function or std::bind handlers. std::signal takes a bare function pointer; stateful callables need a global or static trampoline. Wrapping with std::function inside the handler also risks allocation.
  • Relying on handler persistence. On some systems the disposition resets to SIG_DFL after 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.

See Also

  • <atomic>: lock-free flags and atomic_signal_fence
  • <bind>: why bound callables cannot be passed to std::signal