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:
|
Overview
corosio::signal_set signals(ioc, SIGINT, SIGTERM);
auto [ec, signum] = co_await signals.wait();
if (!ec)
std::cout << "Received signal " << signum << "\n";
Construction
Supported Signals
Managing Signals
add()
Add a signal to the set:
if (auto ec = signals.add(SIGUSR1))
std::cout << "add failed: " << ec.message() << "\n";
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
std::error_code ec = signals.add(SIGHUP, flags::restart);
// Multiple flags can be combined
if (!ec)
ec = 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);
std::error_code ec;
ec = s1.add(SIGINT, flags::restart); // OK - first registration
ec = s2.add(SIGINT, flags::restart); // OK - same flags
ec = s2.add(
SIGINT, flags::no_defer); // invalid_argument - different flags
// Use dont_care to accept existing flags
ec = s2.add(SIGINT, flags::dont_care); // OK - accepts existing flags
Waiting for Signals
The wait() operation waits for any signal in the set:
auto [ec, signum] = co_await signals.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::cond::canceled:
auto [ec, signum] = co_await signals.wait();
if (ec == capy::cond::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.
Stop Token Cancellation
Signal waits support stop token cancellation through the affine protocol:
// Inside a task launched with a stop token:
auto [ec, signum] = co_await signals.wait();
if (ec == capy::cond::canceled)
std::cout << "Wait cancelled by stop request\n";
A stop request cancels only the wait that is pending when it arrives. Unlike
cancel(), whose cancellation is latched and consumed by the next wait(),
a stop request that arrives between waits is discarded: the following
wait() starts uncancelled.
On Windows the cancelled wait resumes inline, on the thread that
called request_stop(). On POSIX it always resumes on a thread running the
execution context.
|
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.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.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.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
if (signals.add(SIGCHLD, flags::no_child_stop | flags::no_child_wait))
co_return;
for (;;)
{
auto [ec, signum] = co_await signals.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
| After a move, the destination uses the source’s 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 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.wait();
running = false;
ioc.stop();
}(ioc, running));
// Accept loop
corosio::tcp_acceptor acc(ioc, corosio::endpoint(8080));
while (running)
{
corosio::tcp_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.
The installed handler is async-signal-safe: it only writes the signal number
to an internal self-pipe (a write(), saving and restoring errno). It never
locks a mutex, allocates memory, or dispatches handlers. The event loop drains
the pipe and completes waiting wait() operations in normal context. As a
result it is safe for a signal to arrive at any time, including while another
thread is modifying a signal_set.
Next Steps
-
Delays and Timeouts — Timed operations
-
I/O Context — The event loop
-
Echo Server Tutorial — Server example