C++ Signal Handling
C++ Signal Handling
Section titled “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.
Common Signals
Section titled “Common Signals”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 std::signal Function
Section titled “The std::signal Function”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 signalsigoccurs. This function (the handler) must take anintargument (the signal number) and returnvoid.- Special values for
funcexist:SIG_DFLto restore the default action, andSIG_IGNto ignore the signal.
The return value is the previous handler associated with the signal, or SIG_ERR on error.
Important Caveats about Signal Handlers
Section titled “Important Caveats about Signal Handlers”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 allocationnew/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_torstd::atomic_flag) and returning. - Avoid Global State Modification (Carefully): Modifying shared data needs extreme care (e.g., using atomic types).
- Termination: Calling
exit()orstd::terminate()directly from a handler bypasses normal stack unwinding, meaning destructors (and RAII cleanup) won’t run. If immediate exit is required,std::_Exitorstd::quick_exit(C++11) perform minimal cleanup but still risk data loss/corruption. - Portability: Signal behavior can vary significantly between operating systems.
Example: Catching SIGINT (Ctrl+C) Safely
Section titled “Example: Catching SIGINT (Ctrl+C) Safely”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-safestd::atomic_flag keep_running = ATOMIC_FLAG_INIT; // Initialize to clear state
// Signal handler functionvoid 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.
The raise() function
Section titled “The raise() function”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;}Alternatives to Signals
Section titled “Alternatives to Signals”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
SIGTERMorSIGINTalone.
Use signals when you specifically need to interact with the OS’s asynchronous event mechanism, but handle them with extreme care.