Skip to content

C++ Signal Handling

Signals are a mechanism provided by the operating system to notify a process about an asynchronous event. These events can range from user actions (like pressing Ctrl+C) to program errors (like division by zero) or termination requests from other processes.

When a signal is delivered to a process, the OS typically interrupts the process’s normal execution flow. The process can then:

  • Ignore the signal.
  • Perform the default action (often terminating the process).
  • Execute a custom function called a signal handler.

The C++ standard library provides access to standard C signals via the <csignal> header. Some common (though not all guaranteed by the C++ standard itself, often POSIX-defined) signals include:

Note: Not all signals can be caught or ignored (e.g., SIGKILL, SIGSTOP on POSIX systems).

The primary function for registering a signal handler is std::signal from <csignal>.

// Simplified signature:
signal_handler_t signal(int sig, signal_handler_t func);

Where:

  • sig: The integer signal number (e.g., SIGINT, SIGTERM).
  • func: A pointer to the function to be called when the signal sig occurs. This function (the handler) must take an int argument (the signal number) and return void.
  • Special values for func exist: SIG_DFL to restore the default action, and SIG_IGN to ignore the signal.

The return value is the previous handler associated with the signal, or SIG_ERR on error.

Signal handlers operate in a very constrained environment. Because they can interrupt the program at almost any point, performing complex operations within them is extremely dangerous and often leads to undefined behavior, crashes, or deadlocks.

Key restrictions and best practices:

  • Reentrancy: Only call functions that are guaranteed to be async-signal-safe. Most standard library functions (including iostream, memory allocation new/delete, mutex operations, etc.) are NOT async-signal-safe.
  • Keep Handlers Simple: Ideally, a handler should do the absolute minimum, like setting a flag (of type volatile std::sig_atomic_t or std::atomic_flag) and returning.
  • Avoid Global State Modification (Carefully): Modifying shared data needs extreme care (e.g., using atomic types).
  • Termination: Calling exit() or std::terminate() directly from a handler bypasses normal stack unwinding, meaning destructors (and RAII cleanup) won’t run. If immediate exit is required, std::_Exit or std::quick_exit (C++11) perform minimal cleanup but still risk data loss/corruption.
  • Portability: Signal behavior can vary significantly between operating systems.

This example shows a safer way to handle SIGINT: set a flag in the handler and check the flag in the main program loop.

#include <iostream>
#include <csignal> // For signal handling functions and types
#include <atomic> // For std::atomic_flag (thread-safe and signal-safe flag)
#include <chrono> // For std::chrono::seconds
#include <thread> // For std::this_thread::sleep_for
// Use an atomic flag for safe communication between handler and main thread
// std::atomic_flag is guaranteed to be lock-free and thus signal-safe
std::atomic_flag keep_running = ATOMIC_FLAG_INIT; // Initialize to clear state
// Signal handler function
void signalHandler(int signum) {
// What NOT to do inside a handler:
// std::cout << "Interrupt received!" << std::endl; // Unsafe (iostream)
// exit(signum); // Unsafe (no cleanup)
// Safe action: Set the atomic flag to signal termination
if (signum == SIGINT) {
keep_running.clear(std::memory_order_release); // Set flag to false (clear it)
}
}
int main() {
// Set the initial state of the flag (true = running)
keep_running.test_and_set(std::memory_order_acquire);
// Register the signal handler for SIGINT
signal(SIGINT, signalHandler);
std::cout << "Program running. Press Ctrl+C to interrupt." << std::endl;
// Main loop checks the flag
while (keep_running.test_and_set(std::memory_order_acquire)) { // Loop while flag is true
std::cout << "Working..." << std::endl;
// Simulate work
std::this_thread::sleep_for(std::chrono::seconds(1));
}
// Cleanup happens here, after the loop exits gracefully
std::cout << "\nInterrupt signal received. Exiting gracefully." << std::endl;
// Destructors for objects in main scope will run here
return 0;
}

When you run this code and press Ctrl+C, the signalHandler sets the keep_running flag to false. The while loop condition will eventually evaluate to false, allowing the program to exit the loop and perform cleanup before terminating.

You can programmatically send a signal to your own process using the std::raise() function from <csignal>.

int raise(int sig);

This function sends the signal sig to the calling process. It returns 0 on success and a non-zero value on failure. This is sometimes used for testing signal handlers or triggering specific signal-related actions internally.

Example using raise():

#include <iostream>
#include <csignal>
#include <chrono>
#include <thread>
#include <atomic>
std::atomic_flag keep_running_raise = ATOMIC_FLAG_INIT;
void signalHandlerRaise(int signum) {
if (signum == SIGTERM) { // Handle a different signal this time
keep_running_raise.clear(std::memory_order_release);
// Re-registering might be needed on some systems if signal handler resets
// signal(SIGTERM, signalHandlerRaise);
}
}
int main() {
keep_running_raise.test_and_set(std::memory_order_acquire);
signal(SIGTERM, signalHandlerRaise); // Register handler for SIGTERM
int counter = 0;
while (keep_running_raise.test_and_set(std::memory_order_acquire)) {
std::cout << "Counter: " << ++counter << std::endl;
if (counter == 3) {
std::cout << "Raising SIGTERM..." << std::endl;
raise(SIGTERM); // Send SIGTERM to this process
}
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
std::cout << "\nSIGTERM received or loop finished. Exiting." << std::endl;
return 0;
}

While signals are necessary for handling certain OS-level events, they are often not the preferred C++ way for general error handling or inter-thread communication.

  • Exceptions: The standard C++ mechanism for handling synchronous errors. They allow for proper stack unwinding and RAII.
  • Condition Variables & Futures (<condition_variable>, <future>): Better suited for signaling between threads within the same process.
  • Application-Level Logic: For graceful shutdown requests, often a dedicated mechanism (like checking a shared atomic flag, or using a dedicated thread) is cleaner and safer than relying on SIGTERM or SIGINT alone.

Use signals when you specifically need to interact with the OS’s asynchronous event mechanism, but handle them with extreme care.