include/boost/corosio/native/detail/posix/posix_signal_service.hpp

98.0% Lines (401 / 409) 97.1% Functions (33 / 34)
posix_signal_service.hpp
f(x) Functions (34)
Function Calls Lines Blocks
boost::corosio::detail::posix_signal_service::destroy(boost::corosio::io_object::implementation*) :177 5220x 100.0% 100.0% boost::corosio::detail::posix_signal_service::reset_token_cancel(boost::corosio::detail::posix_signal&) :219 309115x 100.0% 100.0% boost::corosio::detail::posix_signal_detail::flags_supported(boost::corosio::signal_set::flags_t) :296 5241x 100.0% 100.0% boost::corosio::detail::posix_signal_detail::to_sigaction_flags(boost::corosio::signal_set::flags_t) :308 966x 100.0% 100.0% boost::corosio::detail::posix_signal_detail::flags_compatible(boost::corosio::signal_set::flags_t, boost::corosio::signal_set::flags_t) :328 4264x 100.0% 100.0% boost::corosio::detail::posix_signal_detail::open_signal_pipe(boost::corosio::detail::posix_signal_detail::signal_state*) :348 5241x 100.0% 97.0% boost::corosio::detail::posix_signal_detail::corosio_posix_signal_handler(int) :382 2283x 100.0% 100.0% boost::corosio::detail::posix_signal_detail::drain_signal_pipe() :398 1278x 100.0% 100.0% boost::corosio::detail::signal_op::operator()() :414 3218x 100.0% 100.0% boost::corosio::detail::signal_op::destroy() :439 0 0.0% 0.0% boost::corosio::detail::posix_signal::posix_signal(boost::corosio::detail::posix_signal_service&) :451 5112x 100.0% 100.0% boost::corosio::detail::posix_signal::wait(boost::capy::continuation&, boost::capy::executor_ref, std::stop_token, std::error_code*, int*) :457 309216x 100.0% 100.0% boost::corosio::detail::posix_signal::add(int, boost::corosio::signal_set::flags_t) :502 5247x 100.0% 100.0% boost::corosio::detail::posix_signal::remove(int) :508 37x 100.0% 100.0% boost::corosio::detail::posix_signal::clear() :514 5240x 100.0% 100.0% boost::corosio::detail::posix_signal::cancel() :520 305251x 100.0% 100.0% boost::corosio::detail::posix_signal_service::posix_signal_service(boost::capy::execution_context&) :527 876x 100.0% 71.0% boost::corosio::detail::posix_signal_service::~posix_signal_service() :539 1752x 100.0% 100.0% boost::corosio::detail::posix_signal_service::shutdown() :545 876x 100.0% 82.0% boost::corosio::detail::posix_signal_service::shutdown()::{lambda(boost::corosio::detail::posix_signal*)#1}::operator()(boost::corosio::detail::posix_signal*) const :563 4818x 100.0% 100.0% boost::corosio::detail::posix_signal_service::construct() :626 5220x 100.0% 100.0% boost::corosio::detail::posix_signal::retire() :632 5220x 100.0% 100.0% boost::corosio::detail::posix_signal_service::add_signal(boost::corosio::detail::posix_signal&, int, boost::corosio::signal_set::flags_t) :638 5247x 98.2% 84.0% boost::corosio::detail::posix_signal_service::remove_signal(boost::corosio::detail::posix_signal&, int) :747 37x 100.0% 95.0% boost::corosio::detail::posix_signal_service::clear_signals(boost::corosio::detail::posix_signal&) :800 5240x 100.0% 95.0% boost::corosio::detail::posix_signal_service::cancel_wait(boost::corosio::detail::posix_signal&) :848 305251x 100.0% 100.0% boost::corosio::detail::posix_signal_service::cancel_wait_token(boost::corosio::detail::posix_signal&) :878 4148x 100.0% 100.0% boost::corosio::detail::posix_signal::token_canceller::operator()() const :911 4148x 100.0% 100.0% boost::corosio::detail::posix_signal_service::start_wait(boost::corosio::detail::posix_signal&, boost::corosio::detail::signal_op*) :917 309115x 100.0% 94.0% boost::corosio::detail::posix_signal_service::deliver_signal(int) :974 2282x 95.8% 74.0% boost::corosio::detail::posix_signal_service::work_finished() :1026 1231x 100.0% 100.0% boost::corosio::detail::posix_signal_service::post(boost::corosio::detail::signal_op*) :1032 1231x 100.0% 100.0% boost::corosio::detail::posix_signal_service::add_service(boost::corosio::detail::posix_signal_service*) :1038 876x 100.0% 100.0% boost::corosio::detail::posix_signal_service::remove_service(boost::corosio::detail::posix_signal_service*) :1052 876x 100.0% 100.0%
Line TLA Hits Source Code
1 //
2 // Copyright (c) 2026 Steve Gerbino
3 // Copyright (c) 2026 Michael Vandeberg
4 //
5 // Distributed under the Boost Software License, Version 1.0. (See accompanying
6 // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
7 //
8 // Official repository: https://github.com/cppalliance/corosio
9 //
10
11 #ifndef BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_SIGNAL_SERVICE_HPP
12 #define BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_SIGNAL_SERVICE_HPP
13
14 #include <boost/corosio/detail/platform.hpp>
15
16 #if BOOST_COROSIO_POSIX
17
18 #include <boost/corosio/native/detail/posix/posix_signal.hpp>
19
20 #include <boost/corosio/detail/config.hpp>
21 #include <boost/corosio/detail/object_pool.hpp>
22 #include <boost/corosio/detail/object_ref.hpp>
23 #include <boost/capy/continuation.hpp>
24 #include <boost/capy/ex/abandon.hpp>
25 #include <boost/capy/ex/execution_context.hpp>
26 #include <boost/corosio/detail/scheduler.hpp>
27 #include <boost/corosio/native/detail/make_err.hpp>
28 #include <boost/capy/error.hpp>
29
30 #include <coroutine>
31 #include <mutex>
32 #include <tuple>
33 #include <utility>
34 #include <vector>
35
36 #include <errno.h>
37 #include <fcntl.h>
38 #include <signal.h>
39 #include <unistd.h>
40
41 /*
42 POSIX Signal Service
43 ====================
44
45 Concrete signal service implementation for POSIX backends. Manages signal
46 registrations via sigaction() and dispatches completions through the
47 scheduler. One instance per execution_context, created on first use
48 by the public signal_set.
49
50 See the block comment further down for the full architecture overview.
51 */
52
53 /*
54 POSIX Signal Implementation
55 ===========================
56
57 This file implements signal handling for POSIX systems using sigaction().
58 The implementation supports signal flags (SA_RESTART, etc.) and integrates
59 with any POSIX-compatible scheduler via the abstract scheduler interface.
60
61 Architecture Overview
62 ---------------------
63
64 Three layers manage signal registrations:
65
66 1. signal_state (global singleton)
67 - Tracks the global service list and per-signal registration counts
68 - Stores the flags used for first registration of each signal (for
69 conflict detection when multiple signal_sets register same signal)
70 - Owns the mutex that protects signal handler installation/removal
71
72 2. posix_signal_service (one per execution_context)
73 - Maintains registrations_[] table indexed by signal number
74 - Each slot is a doubly-linked list of signal_registrations for that signal
75 - Also owns a recycling pool of all posix_signal objects it owns
76
77 3. posix_signal (one per signal_set)
78 - Owns a singly-linked list (sorted by signal number) of signal_registrations
79 - Contains the pending_op_ used for wait operations
80
81 Signal Delivery Flow
82 --------------------
83
84 Delivery uses the self-pipe trick so the signal handler itself performs
85 only async-signal-safe work (mirrors Boost.Asio):
86
87 1. Signal arrives -> corosio_posix_signal_handler(). The handler only
88 write()s the signal number to the global self-pipe (write_fd) and
89 restores errno. No locks, no allocation, no scheduler dispatch.
90
91 2. The read end of the pipe is watched by one backend's event loop
92 (registered via scheduler::register_signal_reader on the first
93 registration). When it becomes readable the backend drains it
94 (drain_signal_pipe) and calls deliver_signal() in normal context.
95
96 3. deliver_signal() iterates all posix_signal_service services:
97 - If a signal_set is waiting (impl->waiting_ == true), post the signal_op
98 to the scheduler for immediate completion
99 - Otherwise, increment reg->undelivered to queue the signal
100
101 4. When wait() is called via start_wait():
102 - First check for queued signals (undelivered > 0); if found, post
103 immediate completion without blocking
104 - Otherwise, set waiting_ = true and call work_started() to keep
105 the io_context alive
106
107 Locking Protocol
108 ----------------
109
110 Two mutex levels exist (MUST acquire in this order to avoid deadlock):
111 1. signal_state::mutex - protects handler registration and service list
112 2. posix_signal_service::mutex_ - protects per-service registration tables
113
114 Async-Signal-Safety
115 -------------------
116
117 The C signal handler (corosio_posix_signal_handler) performs only
118 async-signal-safe operations: it reads the single global write_fd and
119 calls write(), saving/restoring errno. It never locks a mutex, allocates
120 memory, or dispatches through the scheduler. All of that happens in
121 deliver_signal(), which runs in normal thread context from the backend
122 event loop after draining the self-pipe. There is therefore no
123 self-deadlock risk if a signal arrives while a thread holds state->mutex
124 or service->mutex_.
125
126 Flag Handling
127 -------------
128
129 - Flags are abstract values in the public API (signal_set::flags_t)
130 - flags_supported() validates that requested flags are available on
131 this platform; returns false if SA_NOCLDWAIT is unavailable and
132 no_child_wait is requested
133 - to_sigaction_flags() maps validated flags to actual SA_* constants
134 - First registration of a signal establishes the flags; subsequent
135 registrations must be compatible (same flags or dont_care)
136 - Requesting unavailable flags returns operation_not_supported
137
138 Work Tracking
139 -------------
140
141 When waiting for a signal:
142 - start_wait() calls sched_->work_started() to prevent io_context::run()
143 from returning while we wait
144 - signal_op::svc is set to point to the service
145 - signal_op::operator()() calls work_finished() after resuming the coroutine
146
147 If a signal was already queued (undelivered > 0), no work tracking is needed
148 because completion is posted immediately.
149 */
150
151 namespace boost::corosio {
152
153 namespace detail {
154
155 /** Signal service for POSIX backends.
156
157 Manages signal registrations via sigaction() and dispatches signal
158 completions through the scheduler. One instance per execution_context.
159 */
160 class BOOST_COROSIO_DECL posix_signal_service final
161 : public capy::execution_context::service
162 , public io_object::io_service
163 {
164 friend class posix_signal;
165
166 public:
167 using key_type = posix_signal_service;
168
169 explicit posix_signal_service(capy::execution_context& ctx);
170 ~posix_signal_service() override;
171
172 posix_signal_service(posix_signal_service const&) = delete;
173 posix_signal_service& operator=(posix_signal_service const&) = delete;
174
175 io_object::implementation* construct() override;
176
177 5220x void destroy(io_object::implementation* p) override
178 {
179 5220x auto& impl = static_cast<posix_signal&>(*p);
180 5220x [[maybe_unused]] auto n = impl.clear();
181 5220x impl.disarm_stop();
182 5220x impl.cancel();
183 5220x release(&impl);
184 5220x }
185
186 /** Shut down the service.
187
188 Gives every registration its live implementations still hold
189 back to the process-global table. The implementations are
190 freed when their handles release them, or when the service is
191 destroyed if a handle outlives it.
192 */
193 void shutdown() override;
194
195 std::error_code add_signal(
196 posix_signal& impl, int signal_number, signal_set::flags_t flags);
197
198 std::error_code remove_signal(posix_signal& impl, int signal_number);
199
200 std::error_code clear_signals(posix_signal& impl);
201
202 void cancel_wait(posix_signal& impl) noexcept;
203 void start_wait(posix_signal& impl, signal_op* op);
204
205 /** Cancel an in-flight wait on behalf of a stop token.
206
207 Identical to @ref cancel_wait except that it does not set the
208 sticky `cancelled_` latch: a stop token scopes to one operation,
209 so a request arriving after the wait completed must do nothing.
210 */
211 void cancel_wait_token(posix_signal& impl) noexcept;
212
213 /** Clear the per-operation stop flag before a new wait arms.
214
215 Lives here rather than on the implementation because `mutex_` is
216 the service's; the service is a friend of `posix_signal`, not the
217 reverse.
218 */
219 309115x void reset_token_cancel(posix_signal& impl) noexcept
220 {
221 309115x std::lock_guard lock(mutex_);
222 309115x impl.token_cancelled_ = false;
223 309115x }
224
225 static void deliver_signal(int signal_number);
226
227 void work_started() noexcept;
228 void work_finished() noexcept;
229 void post(signal_op* op);
230
231 private:
232 static void add_service(posix_signal_service* service);
233 static void remove_service(posix_signal_service* service);
234
235 scheduler* sched_;
236 std::mutex mutex_;
237
238 // Registers the signal self-pipe's read end with sched_ exactly once per
239 // service, so every io_context that waits on a signal can drain the pipe.
240 // A once_flag (not a bool under mutex_) because registration must run
241 // without holding mutex_ or the signal-state mutex — see add_signal.
242 std::mutex reader_mutex_;
243 bool reader_registered_ = false;
244
245 object_pool<posix_signal> pool_;
246
247 // Per-signal registration table
248 signal_registration* registrations_[max_signal_number];
249
250 // Registration counts for each signal
251 std::size_t registration_count_[max_signal_number];
252
253 // Linked list of all posix_signal_service services for signal delivery
254 posix_signal_service* next_ = nullptr;
255 posix_signal_service* prev_ = nullptr;
256 };
257
258 } // namespace detail
259
260 } // namespace boost::corosio
261
262 // ---------------------------------------------------------------------------
263 // Inline implementation
264 // ---------------------------------------------------------------------------
265
266 namespace boost::corosio {
267
268 namespace detail {
269
270 namespace posix_signal_detail {
271
272 struct signal_state
273 {
274 std::mutex mutex;
275 posix_signal_service* service_list = nullptr;
276 std::size_t registration_count[max_signal_number] = {};
277 signal_set::flags_t registered_flags[max_signal_number] = {};
278
279 // Self-pipe used to defer signal delivery out of handler context.
280 // The C handler writes the signal number to write_fd (async-signal-
281 // safe); a backend event loop drains read_fd and calls deliver_signal()
282 // in normal context. Created once (on the first signal registration) and
283 // kept for the process lifetime. Each posix_signal_service registers the
284 // read end with its own scheduler (see reader_once_) so every running
285 // io_context can drain it; multiple readers on one pipe are safe because
286 // each signal is a fixed sizeof(int) record read atomically.
287 int read_fd = -1;
288 int write_fd = -1;
289 };
290
291 BOOST_COROSIO_DECL signal_state* get_signal_state();
292
293 // Check if requested flags are supported on this platform.
294 // Returns true if all flags are supported, false otherwise.
295 inline bool
296 5241x flags_supported([[maybe_unused]] signal_set::flags_t flags)
297 {
298 #ifndef SA_NOCLDWAIT
299 if (flags & signal_set::no_child_wait)
300 return false;
301 #endif
302 5241x return true;
303 }
304
305 // Map abstract flags to sigaction() flags.
306 // Caller must ensure flags_supported() returns true first.
307 inline int
308 966x to_sigaction_flags(signal_set::flags_t flags)
309 {
310 966x int sa_flags = 0;
311 966x if (flags & signal_set::restart)
312 33x sa_flags |= SA_RESTART;
313 966x if (flags & signal_set::no_child_stop)
314 4x sa_flags |= SA_NOCLDSTOP;
315 #ifdef SA_NOCLDWAIT
316 966x if (flags & signal_set::no_child_wait)
317 3x sa_flags |= SA_NOCLDWAIT;
318 #endif
319 966x if (flags & signal_set::no_defer)
320 6x sa_flags |= SA_NODEFER;
321 966x if (flags & signal_set::reset_handler)
322 3x sa_flags |= SA_RESETHAND;
323 966x return sa_flags;
324 }
325
326 // Check if two flag values are compatible
327 inline bool
328 4264x flags_compatible(signal_set::flags_t existing, signal_set::flags_t requested)
329 {
330 // dont_care is always compatible
331 8525x if ((existing & signal_set::dont_care) ||
332 4261x (requested & signal_set::dont_care))
333 10x return true;
334
335 // Mask out dont_care bit for comparison
336 4254x constexpr auto mask = ~signal_set::dont_care;
337 4254x return (existing & mask) == (requested & mask);
338 }
339
340 // Lazily create the global signal self-pipe. Idempotent; call under
341 // state->mutex before installing the first signal handler so write_fd is
342 // valid by the time the handler can fire. Both ends are non-blocking and
343 // close-on-exec (mirrors the reactor self-pipe setup in select_scheduler).
344 // Returns the failing call's errno and leaves the fds at -1 if creation
345 // fails: an exhausted descriptor table and a rejected fcntl are different
346 // problems to the caller of add().
347 [[nodiscard]] inline std::error_code
348 5241x open_signal_pipe(signal_state* state)
349 {
350 5241x if (state->read_fd >= 0)
351 5222x return {};
352
353 int fds[2];
354 19x if (::pipe(fds) < 0)
355 1x return make_err(errno);
356
357 45x for (int i = 0; i < 2; ++i)
358 {
359 33x int fl = ::fcntl(fds[i], F_GETFL, 0);
360 62x if (fl == -1 || ::fcntl(fds[i], F_SETFL, fl | O_NONBLOCK) == -1 ||
361 29x ::fcntl(fds[i], F_SETFD, FD_CLOEXEC) == -1)
362 {
363 6x auto ec = make_err(errno);
364 6x ::close(fds[0]);
365 6x ::close(fds[1]);
366 6x return ec;
367 }
368 }
369
370 12x state->read_fd = fds[0];
371 12x state->write_fd = fds[1];
372 12x return {};
373 }
374
375 // C signal handler. Async-signal-safe: it touches only the single global
376 // write_fd (an int set before any handler is installed) and calls write(),
377 // which POSIX lists as async-signal-safe. errno is saved and restored so an
378 // interrupted foreground syscall is unaffected. A full pipe (write returns
379 // EAGAIN) or a short write is intentionally dropped — the reactor still
380 // coalesces because deliver_signal reports the signal to every waiting set.
381 inline void
382 2283x corosio_posix_signal_handler(int signal_number)
383 {
384 2283x int saved_errno = errno;
385 2283x signal_state* state = get_signal_state();
386 [[maybe_unused]] ssize_t r =
387 2283x ::write(state->write_fd, &signal_number, sizeof(int));
388 2283x errno = saved_errno;
389 // With sigaction(), the handler persists automatically (unlike some
390 // signal() implementations that reset to SIG_DFL).
391 2283x }
392
393 // Drain the signal self-pipe and deliver each pending signal. Runs in normal
394 // thread context from the backend event loop, so deliver_signal()'s mutex
395 // locking and scheduler post are safe here. Reads until EAGAIN (edge-
396 // triggered backends require a full drain per readiness event).
397 inline void
398 1278x drain_signal_pipe()
399 {
400 1278x signal_state* state = get_signal_state();
401 int signal_number;
402 3560x while (::read(state->read_fd, &signal_number, sizeof(int)) ==
403 static_cast<ssize_t>(sizeof(int)))
404 {
405 2282x posix_signal_service::deliver_signal(signal_number);
406 }
407 1278x }
408
409 } // namespace posix_signal_detail
410
411 // signal_op implementation
412
413 inline void
414 3218x signal_op::operator()() noexcept
415 {
416 3218x if (ec_out)
417 3218x *ec_out = {};
418 3218x if (signal_out)
419 3218x *signal_out = signal_number;
420
421 // Copied out before the reference drops: this op is embedded in
422 // the set, which the release may recycle.
423 3218x auto* service = svc;
424 3218x svc = nullptr;
425 3218x auto* c = cont;
426 3218x auto ex = d;
427 3218x auto k = std::move(keep);
428
429 // A throw terminates: the wait is no longer parked, so nothing else
430 // could resume or abandon the coroutine.
431 3218x ex.post(*c);
432
433 // Balance the work_started() from start_wait
434 3218x if (service)
435 1231x service->work_finished();
436 3218x }
437
438 inline void
439 ✗ signal_op::destroy()
440 {
441 // Embedded in posix_signal: the queued reference keeps it alive
442 // while the awaiting coroutine, which may own the set, is destroyed.
443 ✗ auto* c = std::exchange(cont, nullptr);
444 ✗ auto k = std::move(keep);
445 ✗ if (c && c->h)
446 ✗ capy::abandon(*c);
447 ✗ }
448
449 // posix_signal implementation
450
451 5112x inline posix_signal::posix_signal(posix_signal_service& svc) noexcept
452 5112x : svc_(svc)
453 {
454 5112x }
455
456 inline std::coroutine_handle<>
457 309216x posix_signal::wait(
458 capy::continuation& cont,
459 capy::executor_ref d,
460 std::stop_token token,
461 std::error_code* ec,
462 int* signal_out)
463 {
464 309216x pending_op_.cont = &cont;
465 309216x pending_op_.d = d;
466 309216x pending_op_.ec_out = ec;
467 309216x pending_op_.signal_out = signal_out;
468 309216x pending_op_.signal_number = 0;
469
470 // Disarm any callback left over from a previous wait before doing
471 // anything else, including the early return below: otherwise that
472 // path leaves this object owning a callback it no longer uses.
473 // Outside start_wait's lock on purpose: ~stop_callback blocks until a
474 // concurrently running callback returns, and that callback takes
475 // posix_signal_service::mutex_.
476 309216x stop_cb_.reset();
477
478 309216x if (token.stop_requested())
479 {
480 101x if (ec)
481 101x *ec = make_error_code(capy::error::canceled);
482 101x if (signal_out)
483 101x *signal_out = 0;
484 101x d.post(*pending_op_.cont);
485 // completion is always posted to scheduler queue, never inline.
486 101x return std::noop_coroutine();
487 }
488
489 // Clearing the flag before arming is load-bearing: reset_token_cancel
490 // must run immediately before emplace, not before the early return
491 // above.
492 309115x svc_.reset_token_cancel(*this);
493 309115x if (token.stop_possible())
494 7996x stop_cb_.emplace(token, token_canceller{this});
495
496 309115x svc_.start_wait(*this, &pending_op_);
497 // completion is always posted to scheduler queue, never inline.
498 309115x return std::noop_coroutine();
499 }
500
501 inline std::error_code
502 5247x posix_signal::add(int signal_number, signal_set::flags_t flags)
503 {
504 5247x return svc_.add_signal(*this, signal_number, flags);
505 }
506
507 inline std::error_code
508 37x posix_signal::remove(int signal_number)
509 {
510 37x return svc_.remove_signal(*this, signal_number);
511 }
512
513 inline std::error_code
514 5240x posix_signal::clear()
515 {
516 5240x return svc_.clear_signals(*this);
517 }
518
519 inline void
520 305251x posix_signal::cancel() noexcept
521 {
522 305251x svc_.cancel_wait(*this);
523 305251x }
524
525 // posix_signal_service implementation
526
527 876x inline posix_signal_service::posix_signal_service(
528 876x capy::execution_context& ctx)
529 876x : sched_(&get_scheduler(ctx))
530 {
531 56940x for (int i = 0; i < max_signal_number; ++i)
532 {
533 56064x registrations_[i] = nullptr;
534 56064x registration_count_[i] = 0;
535 }
536 876x add_service(this);
537 876x }
538
539 1752x inline posix_signal_service::~posix_signal_service()
540 {
541 876x remove_service(this);
542 1752x }
543
544 inline void
545 876x posix_signal_service::shutdown()
546 {
547 // Each visited impl is pinned with acquire() so a handle
548 // destroy() racing this walk cannot free it mid-walk. The
549 // matching release() runs after the locks drop: if that handle's
550 // destroy() raced in, this release() is the last one, and
551 // ~posix_signal destroys an armed stop_cb_ whose ~stop_callback
552 // waits on a token_canceller that takes mutex_.
553 876x std::vector<posix_signal*> doomed;
554 876x std::vector<capy::continuation> parked;
555
556 {
557 posix_signal_detail::signal_state* state =
558 876x posix_signal_detail::get_signal_state();
559 876x std::lock_guard state_lock(state->mutex);
560 876x std::lock_guard lock(mutex_);
561
562 876x pool_.shutdown(
563 876x [&](posix_signal* impl)
564 {
565 4818x acquire(impl);
566 4818x doomed.push_back(impl);
567 4818x });
568 5694x for (auto* impl : doomed)
569 {
570 // The parked wait's coroutine is destroyed once the locks
571 // drop: destroying it can destroy the set, which takes them.
572 4818x if (std::exchange(impl->waiting_, false))
573 {
574 4818x if (auto* c = std::exchange(impl->pending_op_.cont, nullptr))
575 {
576 4818x if (c->h)
577 4818x parked.push_back(*c);
578 }
579 }
580
581 9636x while (auto* reg = impl->signals_)
582 {
583 4818x int const signal_number = reg->signal_number;
584
585 // The registration table outlives every io_context, so a set
586 // still registered here has to give its count and disposition
587 // back the way clear() would: otherwise the signal stays
588 // installed with these flags and the next add() of it is
589 // refused. The per-node table unlink clear() also does is
590 // skipped in favour of the wholesale null-out below.
591 4818x if (state->registration_count[signal_number] == 1)
592 {
593 615x struct sigaction sa = {};
594 615x sa.sa_handler = SIG_DFL;
595 615x sigemptyset(&sa.sa_mask);
596 615x sa.sa_flags = 0;
597 615x std::ignore = ::sigaction(signal_number, &sa, nullptr);
598 615x state->registered_flags[signal_number] = signal_set::none;
599 }
600
601 4818x --state->registration_count[signal_number];
602 4818x --registration_count_[signal_number];
603
604 4818x impl->signals_ = reg->next_in_set;
605 4818x delete reg;
606 4818x }
607 }
608
609 // Every live registration hung off an implementation this pool
610 // owns, so the whole table goes stale at once and can be dropped
611 // wholesale rather than node by node. It has to be dropped:
612 // deliver_signal() walks this service until the destructor
613 // unlinks it from the global list.
614 56940x for (int i = 0; i < max_signal_number; ++i)
615 56064x registrations_[i] = nullptr;
616 876x }
617
618 5694x for (auto& c : parked)
619 4818x capy::abandon(c);
620
621 5694x for (auto* impl : doomed)
622 4818x release(impl);
623 876x }
624
625 inline io_object::implementation*
626 5220x posix_signal_service::construct()
627 {
628 5220x return pool_.acquire(*this);
629 }
630
631 inline void
632 5220x posix_signal::retire() noexcept
633 {
634 5220x svc_.pool_.recycle(this);
635 5220x }
636
637 inline std::error_code
638 5247x posix_signal_service::add_signal(
639 posix_signal& impl, int signal_number, signal_set::flags_t flags)
640 {
641 5247x if (signal_number < 0 || signal_number >= max_signal_number)
642 6x return make_error_code(std::errc::invalid_argument);
643
644 // Validate that requested flags are supported on this platform
645 // (e.g., SA_NOCLDWAIT may not be available on all POSIX systems)
646 5241x if (!posix_signal_detail::flags_supported(flags))
647 ✗ return make_error_code(std::errc::operation_not_supported);
648
649 posix_signal_detail::signal_state* state =
650 5241x posix_signal_detail::get_signal_state();
651
652 // Ensure the global self-pipe exists and this service's scheduler is
653 // watching its read end, BEFORE taking the registration locks:
654 // register_signal_reader takes the descriptor mutex (via
655 // register_descriptor), which no path may take while holding the
656 // signal-state or service mutexes. call_once makes the once-per-service
657 // registration safe when two signal_sets on this context race add()
658 // from different threads.
659 {
660 5241x std::lock_guard state_lock(state->mutex);
661 5241x if (auto ec = posix_signal_detail::open_signal_pipe(state))
662 7x return ec;
663 5241x }
664 {
665 // Success-latched so a failed environmental registration
666 // (epoll_ctl ENOMEM/ENOSPC) is retried by the next add()
667 // instead of being lost; the code travels the return channel.
668 5234x std::lock_guard reg_lock(reader_mutex_);
669 5234x if (!reader_registered_)
670 {
671 839x if (auto ec = sched_->register_signal_reader(state->read_fd))
672 4x return ec;
673 835x reader_registered_ = true;
674 }
675 5234x }
676
677 5230x std::lock_guard state_lock(state->mutex);
678 5230x std::lock_guard lock(mutex_);
679
680 // Find insertion point (list is sorted by signal number)
681 5230x signal_registration** insertion_point = &impl.signals_;
682 5230x signal_registration* reg = impl.signals_;
683 5264x while (reg && reg->signal_number < signal_number)
684 {
685 34x insertion_point = &reg->next_in_set;
686 34x reg = reg->next_in_set;
687 }
688
689 // Already registered in this set - check flag compatibility
690 // (same signal_set adding same signal twice with different flags)
691 5230x if (reg && reg->signal_number == signal_number)
692 {
693 18x if (!posix_signal_detail::flags_compatible(reg->flags, flags))
694 5x return make_error_code(std::errc::invalid_argument);
695 13x return {};
696 }
697
698 // Check flag compatibility with global registration
699 // (different signal_set already registered this signal with different flags)
700 5212x if (state->registration_count[signal_number] > 0)
701 {
702 4246x if (!posix_signal_detail::flags_compatible(
703 state->registered_flags[signal_number], flags))
704 3x return make_error_code(std::errc::invalid_argument);
705 }
706
707 5209x auto* new_reg = new signal_registration;
708 5209x new_reg->signal_number = signal_number;
709 5209x new_reg->flags = flags;
710 5209x new_reg->owner = &impl;
711 5209x new_reg->undelivered = 0;
712
713 // Install signal handler on first global registration
714 5209x if (state->registration_count[signal_number] == 0)
715 {
716 966x struct sigaction sa = {};
717 966x sa.sa_handler = posix_signal_detail::corosio_posix_signal_handler;
718 966x sigemptyset(&sa.sa_mask);
719 966x sa.sa_flags = posix_signal_detail::to_sigaction_flags(flags);
720
721 966x if (::sigaction(signal_number, &sa, nullptr) < 0)
722 {
723 1x delete new_reg;
724 1x return make_error_code(std::errc::invalid_argument);
725 }
726
727 // Store the flags used for first registration
728 965x state->registered_flags[signal_number] = flags;
729 }
730
731 5208x new_reg->next_in_set = reg;
732 5208x *insertion_point = new_reg;
733
734 5208x new_reg->next_in_table = registrations_[signal_number];
735 5208x new_reg->prev_in_table = nullptr;
736 5208x if (registrations_[signal_number])
737 4231x registrations_[signal_number]->prev_in_table = new_reg;
738 5208x registrations_[signal_number] = new_reg;
739
740 5208x ++state->registration_count[signal_number];
741 5208x ++registration_count_[signal_number];
742
743 5208x return {};
744 5230x }
745
746 inline std::error_code
747 37x posix_signal_service::remove_signal(posix_signal& impl, int signal_number)
748 {
749 37x if (signal_number < 0 || signal_number >= max_signal_number)
750 3x return make_error_code(std::errc::invalid_argument);
751
752 posix_signal_detail::signal_state* state =
753 34x posix_signal_detail::get_signal_state();
754 34x std::lock_guard state_lock(state->mutex);
755 34x std::lock_guard lock(mutex_);
756
757 34x signal_registration** deletion_point = &impl.signals_;
758 34x signal_registration* reg = impl.signals_;
759 37x while (reg && reg->signal_number < signal_number)
760 {
761 3x deletion_point = &reg->next_in_set;
762 3x reg = reg->next_in_set;
763 }
764
765 34x if (!reg || reg->signal_number != signal_number)
766 4x return {};
767
768 // Restore default handler on last global unregistration
769 30x if (state->registration_count[signal_number] == 1)
770 {
771 24x struct sigaction sa = {};
772 24x sa.sa_handler = SIG_DFL;
773 24x sigemptyset(&sa.sa_mask);
774 24x sa.sa_flags = 0;
775
776 24x if (::sigaction(signal_number, &sa, nullptr) < 0)
777 1x return make_error_code(std::errc::invalid_argument);
778
779 // Clear stored flags
780 23x state->registered_flags[signal_number] = signal_set::none;
781 }
782
783 29x *deletion_point = reg->next_in_set;
784
785 29x if (registrations_[signal_number] == reg)
786 26x registrations_[signal_number] = reg->next_in_table;
787 29x if (reg->prev_in_table)
788 3x reg->prev_in_table->next_in_table = reg->next_in_table;
789 29x if (reg->next_in_table)
790 3x reg->next_in_table->prev_in_table = reg->prev_in_table;
791
792 29x --state->registration_count[signal_number];
793 29x --registration_count_[signal_number];
794
795 29x delete reg;
796 29x return {};
797 34x }
798
799 inline std::error_code
800 5240x posix_signal_service::clear_signals(posix_signal& impl)
801 {
802 posix_signal_detail::signal_state* state =
803 5240x posix_signal_detail::get_signal_state();
804 5240x std::lock_guard state_lock(state->mutex);
805 5240x std::lock_guard lock(mutex_);
806
807 5240x std::error_code first_error;
808
809 5601x while (signal_registration* reg = impl.signals_)
810 {
811 361x int signal_number = reg->signal_number;
812
813 361x if (state->registration_count[signal_number] == 1)
814 {
815 327x struct sigaction sa = {};
816 327x sa.sa_handler = SIG_DFL;
817 327x sigemptyset(&sa.sa_mask);
818 327x sa.sa_flags = 0;
819
820 327x if (::sigaction(signal_number, &sa, nullptr) < 0 && !first_error)
821 1x first_error = make_error_code(std::errc::invalid_argument);
822
823 // Clear stored flags
824 327x state->registered_flags[signal_number] = signal_set::none;
825 }
826
827 361x impl.signals_ = reg->next_in_set;
828
829 361x if (registrations_[signal_number] == reg)
830 355x registrations_[signal_number] = reg->next_in_table;
831 361x if (reg->prev_in_table)
832 6x reg->prev_in_table->next_in_table = reg->next_in_table;
833 361x if (reg->next_in_table)
834 19x reg->next_in_table->prev_in_table = reg->prev_in_table;
835
836 361x --state->registration_count[signal_number];
837 361x --registration_count_[signal_number];
838
839 361x delete reg;
840 361x }
841
842 5240x if (first_error)
843 1x return first_error;
844 5239x return {};
845 5240x }
846
847 inline void
848 305251x posix_signal_service::cancel_wait(posix_signal& impl) noexcept
849 {
850 305251x bool was_waiting = false;
851 305251x signal_op* op = nullptr;
852
853 {
854 305251x std::lock_guard lock(mutex_);
855 305251x impl.cancelled_ = true;
856 305251x if (impl.waiting_)
857 {
858 19x was_waiting = true;
859 19x impl.waiting_ = false;
860 19x op = &impl.pending_op_;
861 19x sched_->cancel_posting_begin();
862 }
863 305251x }
864
865 305251x if (was_waiting)
866 {
867 19x if (op->ec_out)
868 19x *op->ec_out = make_error_code(capy::error::canceled);
869 19x if (op->signal_out)
870 19x *op->signal_out = 0;
871 19x op->d.post(*op->cont);
872 19x sched_->work_finished();
873 19x sched_->cancel_posting_end();
874 }
875 305251x }
876
877 inline void
878 4148x posix_signal_service::cancel_wait_token(posix_signal& impl) noexcept
879 {
880 4148x bool was_waiting = false;
881 4148x signal_op* op = nullptr;
882
883 {
884 4148x std::lock_guard lock(mutex_);
885 // Persist the request even when no wait is parked yet: wait()
886 // arms the callback before start_wait takes this lock, and
887 // start_wait consumes this flag.
888 4148x impl.token_cancelled_ = true;
889 4148x if (impl.waiting_)
890 {
891 887x was_waiting = true;
892 887x impl.waiting_ = false;
893 887x op = &impl.pending_op_;
894 887x sched_->cancel_posting_begin();
895 }
896 4148x }
897
898 4148x if (was_waiting)
899 {
900 887x if (op->ec_out)
901 887x *op->ec_out = make_error_code(capy::error::canceled);
902 887x if (op->signal_out)
903 887x *op->signal_out = 0;
904 887x op->d.post(*op->cont);
905 887x sched_->work_finished();
906 887x sched_->cancel_posting_end();
907 }
908 4148x }
909
910 inline void
911 4148x posix_signal::token_canceller::operator()() const noexcept
912 {
913 4148x self->svc_.cancel_wait_token(*self);
914 4148x }
915
916 inline void
917 309115x posix_signal_service::start_wait(posix_signal& impl, signal_op* op)
918 {
919 {
920 309115x std::lock_guard lock(mutex_);
921
922 // Check if cancel() was called before this wait started
923 309115x if (impl.cancelled_)
924 {
925 300003x impl.cancelled_ = false;
926 300003x if (op->ec_out)
927 300003x *op->ec_out = make_error_code(capy::error::canceled);
928 300003x if (op->signal_out)
929 300003x *op->signal_out = 0;
930 300003x op->d.post(*op->cont);
931 300003x return;
932 }
933
934 // A stop request that arrived between wait() arming the callback
935 // and this lock: complete now rather than parking forever.
936 9112x if (impl.token_cancelled_)
937 {
938 170x impl.token_cancelled_ = false;
939 170x if (op->ec_out)
940 170x *op->ec_out = make_error_code(capy::error::canceled);
941 170x if (op->signal_out)
942 170x *op->signal_out = 0;
943 170x op->d.post(*op->cont);
944 170x return;
945 }
946
947 // Check for queued signals first (signal arrived before wait started)
948 8942x signal_registration* reg = impl.signals_;
949 15902x while (reg)
950 {
951 8947x if (reg->undelivered > 0)
952 {
953 1987x --reg->undelivered;
954 1987x op->signal_number = reg->signal_number;
955 // svc=nullptr: no work_finished needed since we never called work_started
956 1987x op->svc = nullptr;
957 // The caller's wait() holds the handle's reference.
958 1987x op->keep = detail::object_ref(&impl);
959 1987x sched_->post(op);
960 1987x return;
961 }
962 6960x reg = reg->next_in_set;
963 }
964
965 // No queued signals - wait for delivery
966 6955x impl.waiting_ = true;
967 // svc=this: signal_op::operator() will call work_finished() to balance this
968 6955x op->svc = this;
969 6955x sched_->work_started();
970 309115x }
971 }
972
973 inline void
974 2282x posix_signal_service::deliver_signal(int signal_number)
975 {
976 2282x if (signal_number < 0 || signal_number >= max_signal_number)
977 ✗ return;
978
979 posix_signal_detail::signal_state* state =
980 2282x posix_signal_detail::get_signal_state();
981 2282x std::lock_guard lock(state->mutex);
982
983 2282x posix_signal_service* service = state->service_list;
984 5764x while (service)
985 {
986 3482x std::lock_guard svc_lock(service->mutex_);
987
988 3482x signal_registration* reg = service->registrations_[signal_number];
989 6970x while (reg)
990 {
991 3488x posix_signal* impl = static_cast<posix_signal*>(reg->owner);
992
993 3488x if (impl->waiting_)
994 {
995 // Reached through the registry, not a held reference.
996 // A set whose count reached zero is being destroyed,
997 // and that destruction completes its wait itself.
998 1231x auto keep = detail::object_ref::try_from(impl);
999 1231x if (keep)
1000 {
1001 1231x impl->waiting_ = false;
1002 1231x impl->pending_op_.signal_number = signal_number;
1003 1231x impl->pending_op_.keep = std::move(keep);
1004 1231x service->post(&impl->pending_op_);
1005 }
1006 1231x }
1007 else
1008 {
1009 2257x ++reg->undelivered;
1010 }
1011
1012 3488x reg = reg->next_in_table;
1013 }
1014
1015 3482x service = service->next_;
1016 3482x }
1017 2282x }
1018
1019 inline void
1020 posix_signal_service::work_started() noexcept
1021 {
1022 sched_->work_started();
1023 }
1024
1025 inline void
1026 1231x posix_signal_service::work_finished() noexcept
1027 {
1028 1231x sched_->work_finished();
1029 1231x }
1030
1031 inline void
1032 1231x posix_signal_service::post(signal_op* op)
1033 {
1034 1231x sched_->post(op);
1035 1231x }
1036
1037 inline void
1038 876x posix_signal_service::add_service(posix_signal_service* service)
1039 {
1040 posix_signal_detail::signal_state* state =
1041 876x posix_signal_detail::get_signal_state();
1042 876x std::lock_guard lock(state->mutex);
1043
1044 876x service->next_ = state->service_list;
1045 876x service->prev_ = nullptr;
1046 876x if (state->service_list)
1047 15x state->service_list->prev_ = service;
1048 876x state->service_list = service;
1049 876x }
1050
1051 inline void
1052 876x posix_signal_service::remove_service(posix_signal_service* service)
1053 {
1054 posix_signal_detail::signal_state* state =
1055 876x posix_signal_detail::get_signal_state();
1056 876x std::lock_guard lock(state->mutex);
1057
1058 876x if (service->next_ || service->prev_ || state->service_list == service)
1059 {
1060 876x if (state->service_list == service)
1061 873x state->service_list = service->next_;
1062 876x if (service->prev_)
1063 3x service->prev_->next_ = service->next_;
1064 876x if (service->next_)
1065 12x service->next_->prev_ = service->prev_;
1066 876x service->next_ = nullptr;
1067 876x service->prev_ = nullptr;
1068 }
1069 876x }
1070
1071 } // namespace detail
1072 } // namespace boost::corosio
1073
1074 #endif // BOOST_COROSIO_POSIX
1075
1076 #endif // BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_SIGNAL_SERVICE_HPP
1077