Signal Handling
The signal_set class provides asynchronous signal handling. It allows
coroutines to wait for operating system signals like SIGINT (Ctrl+C) or
SIGTERM.
| Code snippets assume: |
#include <boost/corosio/signal_set.hpp>
#include <csignal>
namespace corosio = boost::corosio;
Overview
corosio::signal_set signals(ioc, SIGINT, SIGTERM);
auto [ec, signum] = co_await signals.async_wait();
if (!ec)
std::cout << "Received signal " << signum << "\n";
Supported Signals
Managing Signals
add()
Add a signal to the set:
signals.add(SIGUSR1);
Adding a signal that’s already in the set has no effect.
Signal Flags (POSIX)
On POSIX systems, you can specify signal flags when adding a signal:
using flags = corosio::signal_set;
// Restart interrupted system calls automatically
signals.add(SIGCHLD, flags::restart);
// Multiple flags can be combined
signals.add(SIGCHLD, flags::restart | flags::no_child_stop);
Available flags:
| Flag | Description |
|---|---|
|
No special flags (default) |
|
Automatically restart interrupted system calls (SA_RESTART) |
|
Don’t generate SIGCHLD when children stop (SA_NOCLDSTOP) |
|
Don’t create zombie processes on child termination (SA_NOCLDWAIT) |
|
Don’t block the signal while its handler runs (SA_NODEFER) |
|
Reset handler to SIG_DFL after one invocation (SA_RESETHAND) |
|
Accept existing flags if signal is already registered |
On Windows, only none and dont_care flags are supported. On some POSIX
systems, no_child_wait may not be available. Using unsupported flags returns
operation_not_supported.
|
Flag Compatibility
When multiple signal_set objects register for the same signal, they must use
compatible flags:
corosio::signal_set s1(ioc);
corosio::signal_set s2(ioc);
s1.add(SIGINT, flags::restart); // OK - first registration
s2.add(SIGINT, flags::restart); // OK - same flags
s2.add(SIGINT, flags::no_defer); // Error! - different flags
// Use dont_care to accept existing flags
s2.add(SIGINT, flags::dont_care); // OK - accepts existing flags
Waiting for Signals
The async_wait() operation waits for any signal in the set:
auto [ec, signum] = co_await signals.async_wait();
if (!ec)
{
switch (signum)
{
case SIGINT:
std::cout << "Interrupt received\n";
break;
case SIGTERM:
std::cout << "Termination requested\n";
break;
}
}
Cancellation
cancel()
Cancel pending wait operations:
signals.cancel();
The wait completes with capy::error::canceled:
auto [ec, signum] = co_await signals.async_wait();
if (ec == capy::error::canceled)
std::cout << "Wait was cancelled\n";
Cancellation does NOT remove signals from the set. The signal set remains configured and can be waited on again.
Use Cases
Graceful Shutdown
capy::task<void> shutdown_handler(
corosio::io_context& ioc,
std::atomic<bool>& running)
{
corosio::signal_set signals(ioc, SIGINT, SIGTERM);
auto [ec, signum] = co_await signals.async_wait();
if (!ec)
{
std::cout << "Shutdown signal received\n";
running = false;
ioc.stop();
}
}
Multiple Signal Waits
You can wait for signals multiple times:
capy::task<void> signal_loop(corosio::io_context& ioc)
{
corosio::signal_set signals(ioc, SIGUSR1);
for (;;)
{
auto [ec, signum] = co_await signals.async_wait();
if (ec)
break;
std::cout << "Received USR1, doing work...\n";
// Handle signal
}
}
Reload Configuration
capy::task<void> config_reloader(
corosio::io_context& ioc,
Config& config)
{
corosio::signal_set signals(ioc, SIGHUP);
for (;;)
{
auto [ec, signum] = co_await signals.async_wait();
if (ec)
break;
std::cout << "Reloading configuration...\n";
config.reload();
}
}
Child Process Management (POSIX)
capy::task<void> child_reaper(corosio::io_context& ioc)
{
using flags = corosio::signal_set;
corosio::signal_set signals(ioc);
// Only notify on child termination, not stop/continue
// Prevent zombie processes automatically
signals.add(SIGCHLD, flags::no_child_stop | flags::no_child_wait);
for (;;)
{
auto [ec, signum] = co_await signals.async_wait();
if (ec)
break;
// With no_child_wait, children are reaped automatically
std::cout << "Child process terminated\n";
}
}
Move Semantics
Signal sets are move-only:
corosio::signal_set s1(ioc, SIGINT);
corosio::signal_set s2 = std::move(s1); // OK
corosio::signal_set s3 = s2; // Error: deleted copy constructor
| Source and destination must share the same execution context. |
Thread Safety
| Operation | Thread Safety |
|---|---|
Distinct signal_sets |
Safe from different threads |
Same signal_set |
NOT safe for concurrent operations |
Don’t call async_wait(), add(), remove(), clear(), or cancel()
concurrently on the same signal_set.
Example: Server with Graceful Shutdown
capy::task<void> run_server(corosio::io_context& ioc)
{
std::atomic<bool> running{true};
// Start signal handler
capy::run_async(ioc.get_executor())(
[](corosio::io_context& ioc, std::atomic<bool>& running)
-> capy::task<void>
{
corosio::signal_set signals(ioc, SIGINT, SIGTERM);
co_await signals.async_wait();
running = false;
ioc.stop();
}(ioc, running));
// Accept loop
corosio::acceptor acc(ioc);
acc.listen(corosio::endpoint(8080));
while (running)
{
corosio::socket peer(ioc);
auto [ec] = co_await acc.accept(peer);
if (ec)
break;
// Handle connection...
}
}
Platform Notes
Windows
Windows has limited signal support. The library uses signal() from the
C runtime for compatibility. Only none and dont_care flags are supported;
other flags return operation_not_supported.
POSIX
On POSIX systems, signals are handled using sigaction() which provides:
-
Reliable signal delivery (handler doesn’t reset to SIG_DFL)
-
Support for signal flags (SA_RESTART, SA_NOCLDSTOP, etc.)
-
Proper signal masking during handler execution
The restart flag is particularly useful—without it, blocking calls like
read() can fail with EINTR when a signal arrives.
Next Steps
-
Timers — Timed operations
-
I/O Context — The event loop
-
Echo Server Tutorial — Server example