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

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