-
Notifications
You must be signed in to change notification settings - Fork 0
Signal Handling
This page explains how to handle Unix signals safely in Elio coroutines using signalfd.
Traditional signal handlers have significant limitations in coroutine-based applications:
-
Async-signal-safety: Signal handlers can interrupt at any point, even in the middle of coroutine scheduling operations. Only a limited set of functions are safe to call in signal handlers.
-
Race conditions: Modifying scheduler state, coroutine handles, or shared data structures from a signal handler is inherently unsafe.
-
Limited functionality: You can't use
co_await, I/O operations, or most standard library functions in signal handlers.
Linux's signalfd(2) converts signals into file descriptor events. By blocking signals with sigprocmask and reading them via signalfd, signals become normal I/O events that can be handled in a regular coroutine context.
#include <elio/signal/signalfd.hpp>
using namespace elio::signal;coro::task<void> handle_shutdown() {
// Create signal set with signals to handle
signal_set sigs{SIGINT, SIGTERM};
// Create signalfd - automatically blocks the signals
signal_fd sigfd(sigs);
// Wait for signal in coroutine context
while (running) {
auto info = co_await sigfd.wait();
if (info) {
if (info->signo == SIGINT || info->signo == SIGTERM) {
ELIO_LOG_INFO("Shutdown requested via {}", info->full_name());
running = false;
}
}
}
co_return;
}// Simple one-shot signal wait
auto info = co_await wait_signal(SIGTERM);
ELIO_LOG_INFO("Received {}", info.full_name());
// wait_signal() acquires the block; release it after all users are finished
signal_set{SIGTERM}.unblock();Manages a set of signals using sigset_t.
// Create empty set
signal_set sigs;
// Add signals (chainable)
sigs.add(SIGINT).add(SIGTERM).add(SIGUSR1);
// Create with initializer list
signal_set sigs{SIGINT, SIGTERM, SIGUSR1};
// Remove a signal
sigs.remove(SIGUSR1);
// Check membership
if (sigs.contains(SIGINT)) { /* ... */ }
// Clear or fill
sigs.clear(); // Empty set
sigs.fill(); // All signals
// Block/unblock for current thread
sigset_t old_mask;
sigs.block(&old_mask); // Block these signals
sigs.unblock(); // Unblock these signals
sigs.set_mask(&old_mask); // Replace signal mask
// Use the *_error variants when callers need the direct pthread error number.
int rc = sigs.block_error(&old_mask);
if (rc != 0) {
// rc is the pthread_sigmask() error code
}Async-friendly signalfd wrapper.
The object retains the io_context selected at construction. For scheduler
use, construct and await it on the same worker; pending waits pin that worker's
backend until completion or cleanup. Do not construct it against a standalone
context and later await it in a scheduler coroutine. Standalone use requires
the caller to serialize and poll that context.
// Create with automatic blocking
signal_fd sigfd(sigs);
// Don't auto-block (caller manages signal mask)
signal_fd sigfd(sigs, io::current_io_context(), false);
// Check validity
if (sigfd.valid()) { /* ... */ }
if (sigfd) { /* ... */ } // bool conversion
// Get file descriptor
int fd = sigfd.fd();
// Async wait
auto async_info = co_await sigfd.wait();
// Sync try-read (non-blocking)
auto ready_info = sigfd.try_read();
// Update signal set
signal_set new_sigs{SIGUSR2};
sigfd.update(new_sigs);
// Explicitly release signals after every user that needs them blocked is done
signal_set released{SIGUSR1, SIGUSR2};
released.unblock();
// Close explicitly
sigfd.close();With automatic blocking enabled, update() blocks the new signal set but does
not unblock signals removed from the descriptor. This preserves caller-owned
masks and avoids exposing a removed pending signal before the descriptor update
commits. Explicitly unblock signals when their process-level disposition is no
longer needed. If the descriptor update fails, update() restores the calling
thread's prior mask. Passing false as the second argument leaves the thread
mask unchanged. Closing, moving, or destroying a signal_fd never changes the
thread mask.
signal_fd::restore_mask() is deprecated and is now a no-op that returns
false. A whole-thread snapshot cannot be restored safely when other live
signal_fd instances or later caller operations have added blocks. Keep the
signals blocked for as long as any descriptor or worker depends on them, then
release them explicitly with signal_set::unblock() from the owning thread.
The wait_signal() convenience helpers follow the same acquire-only rule.
Information about a received signal.
auto info = co_await sigfd.wait();
if (info) {
int signo = info->signo; // Signal number
const char* name = info->name(); // "INT", "TERM", etc.
std::string full = info->full_name(); // "SIGINT", "SIGTERM"
uint32_t pid = info->pid; // Sender PID
uint32_t uid = info->uid; // Sender UID
int32_t code = info->code; // Signal code (SI_USER, SI_KERNEL, etc.)
}RAII guard for temporary signal blocking.
{
signal_block_guard guard(sigs);
// Signals are blocked here
} // Signals restored automatically// Get signal name from number
const char* name = signal_name(SIGINT); // "INT"
// Get signal number from name
int signo = signal_number("SIGINT"); // 2
int signo = signal_number("INT"); // 2 (prefix optional)Block signals before creating any threads to ensure all threads inherit the blocked mask:
int main() {
// Block signals FIRST, before anything else
signal_set sigs{SIGINT, SIGTERM, SIGUSR1};
sigs.block_all_threads();
// Now create scheduler and start threads
scheduler sched(4);
sched.start();
// ...
}coro::task<void> signal_router() {
signal_set sigs{SIGINT, SIGTERM, SIGUSR1, SIGUSR2, SIGHUP};
signal_fd sigfd(sigs);
while (running) {
auto info = co_await sigfd.wait();
if (!info) continue;
switch (info->signo) {
case SIGINT:
case SIGTERM:
initiate_shutdown();
break;
case SIGHUP:
reload_configuration();
break;
case SIGUSR1:
print_status();
break;
case SIGUSR2:
rotate_logs();
break;
}
}
co_return;
}For simple server applications, use elio::serve() with shutdown signals
blocked before the scheduler starts:
coro::task<int> async_main(int argc, char* argv[]) {
http::router r;
r.get("/", handler);
http::server srv(r);
// serve() waits for masked SIGINT/SIGTERM and calls srv.stop()
co_await elio::serve(srv, [&]() { return srv.listen(addr); });
co_return 0;
}
int main(int argc, char* argv[]) {
elio::signal::signal_set shutdown_signals(elio::default_shutdown_signals);
shutdown_signals.block_all_threads();
return elio::run(async_main, argc, argv);
}For more complex scenarios with custom shutdown logic:
std::atomic<bool> g_running{true};
coro::task<void> shutdown_handler() {
signal_set sigs{SIGINT, SIGTERM};
signal_fd sigfd(sigs);
auto info = co_await sigfd.wait();
if (!info) {
co_return;
}
ELIO_LOG_INFO("Shutdown signal received: {}", info->full_name());
g_running = false;
co_return;
}
coro::task<void> worker() {
while (g_running) {
// Do work
co_await process_request();
}
// Cleanup before exit
co_return;
}void log_signal(const signal_info& info) {
ELIO_LOG_INFO("Signal: {} ({})", info.full_name(), info.signo);
ELIO_LOG_INFO(" Sender: PID={}, UID={}", info.pid, info.uid);
ELIO_LOG_INFO(" Code: {}", info.code);
}See examples/signal_handling.cpp for a complete example showing:
- Signal blocking before thread creation
- Signal handler coroutine
- Graceful shutdown coordination
- Status requests via SIGUSR1
Build and run:
cmake --build build --target signal_handling
./build/examples/signal_handling
# In another terminal:
kill -USR1 <pid> # Print status
kill -INT <pid> # or Ctrl+C for graceful shutdown// DON'T do this in coroutine applications!
void signal_handler(int signo) {
// Can't use co_await here
// Can't safely modify scheduler state
// Limited to async-signal-safe functions
g_shutdown_flag = true; // Only atomic operations are safe
}coro::task<void> signal_handler() {
signal_fd sigfd(signal_set{SIGINT, SIGTERM});
auto info = co_await sigfd.wait(); // Full coroutine support!
if (!info) {
co_return;
}
// Can use any function here
co_await cleanup_connections();
co_await flush_caches();
ELIO_LOG_INFO("Clean shutdown complete");
co_return;
}- Core-Concepts - Coroutines and scheduler basics
- Examples - More code examples
-
man signalfd- Linux signalfd documentation -
man sigprocmask- Signal mask manipulation