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 3476x 100.0% 100.0% boost::corosio::detail::posix_signal_service::reset_token_cancel(boost::corosio::detail::posix_signal&) :219 205897x 100.0% 100.0% boost::corosio::detail::posix_signal_detail::flags_supported(boost::corosio::signal_set::flags_t) :296 3491x 100.0% 100.0% boost::corosio::detail::posix_signal_detail::to_sigaction_flags(boost::corosio::signal_set::flags_t) :308 641x 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 2841x 100.0% 100.0% boost::corosio::detail::posix_signal_detail::open_signal_pipe(boost::corosio::detail::posix_signal_detail::signal_state*) :348 3491x 100.0% 97.0% boost::corosio::detail::posix_signal_detail::corosio_posix_signal_handler(int) :382 1521x 100.0% 100.0% boost::corosio::detail::posix_signal_detail::drain_signal_pipe() :398 941x 100.0% 100.0% boost::corosio::detail::signal_op::operator()() :414 1982x 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 3404x 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 205987x 100.0% 100.0% boost::corosio::detail::posix_signal::add(int, boost::corosio::signal_set::flags_t) :502 3495x 100.0% 100.0% boost::corosio::detail::posix_signal::remove(int) :508 26x 100.0% 100.0% boost::corosio::detail::posix_signal::clear() :514 3490x 100.0% 100.0% boost::corosio::detail::posix_signal::cancel() :520 203497x 100.0% 100.0% boost::corosio::detail::posix_signal_service::posix_signal_service(boost::capy::execution_context&) :527 583x 100.0% 71.0% boost::corosio::detail::posix_signal_service::~posix_signal_service() :539 1166x 100.0% 100.0% boost::corosio::detail::posix_signal_service::shutdown() :545 583x 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 3210x 100.0% 100.0% boost::corosio::detail::posix_signal_service::construct() :626 3476x 100.0% 100.0% boost::corosio::detail::posix_signal::retire() :632 3476x 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 3495x 98.2% 84.0% boost::corosio::detail::posix_signal_service::remove_signal(boost::corosio::detail::posix_signal&, int) :747 26x 100.0% 95.0% boost::corosio::detail::posix_signal_service::clear_signals(boost::corosio::detail::posix_signal&) :800 3490x 100.0% 95.0% boost::corosio::detail::posix_signal_service::cancel_wait(boost::corosio::detail::posix_signal&) :848 203497x 100.0% 100.0% boost::corosio::detail::posix_signal_service::cancel_wait_token(boost::corosio::detail::posix_signal&) :878 961x 100.0% 100.0% boost::corosio::detail::posix_signal::token_canceller::operator()() const :911 961x 100.0% 100.0% boost::corosio::detail::posix_signal_service::start_wait(boost::corosio::detail::posix_signal&, boost::corosio::detail::signal_op*) :917 205897x 100.0% 94.0% boost::corosio::detail::posix_signal_service::deliver_signal(int) :974 1521x 95.8% 74.0% boost::corosio::detail::posix_signal_service::work_finished() :1026 792x 100.0% 100.0% boost::corosio::detail::posix_signal_service::post(boost::corosio::detail::signal_op*) :1032 792x 100.0% 100.0% boost::corosio::detail::posix_signal_service::add_service(boost::corosio::detail::posix_signal_service*) :1038 583x 100.0% 100.0% boost::corosio::detail::posix_signal_service::remove_service(boost::corosio::detail::posix_signal_service*) :1052 583x 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 3476x void destroy(io_object::implementation* p) override
178 {
179 3476x auto& impl = static_cast<posix_signal&>(*p);
180 3476x [[maybe_unused]] auto n = impl.clear();
181 3476x impl.disarm_stop();
182 3476x impl.cancel();
183 3476x release(&impl);
184 3476x }
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 205897x void reset_token_cancel(posix_signal& impl) noexcept
220 {
221 205897x std::lock_guard lock(mutex_);
222 205897x impl.token_cancelled_ = false;
223 205897x }
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 3491x 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 3491x return true;
303 }
304
305 // Map abstract flags to sigaction() flags.
306 // Caller must ensure flags_supported() returns true first.
307 inline int
308 641x to_sigaction_flags(signal_set::flags_t flags)
309 {
310 641x int sa_flags = 0;
311 641x if (flags & signal_set::restart)
312 23x sa_flags |= SA_RESTART;
313 641x if (flags & signal_set::no_child_stop)
314 3x sa_flags |= SA_NOCLDSTOP;
315 #ifdef SA_NOCLDWAIT
316 641x if (flags & signal_set::no_child_wait)
317 2x sa_flags |= SA_NOCLDWAIT;
318 #endif
319 641x if (flags & signal_set::no_defer)
320 4x sa_flags |= SA_NODEFER;
321 641x if (flags & signal_set::reset_handler)
322 2x sa_flags |= SA_RESETHAND;
323 641x return sa_flags;
324 }
325
326 // Check if two flag values are compatible
327 inline bool
328 2841x flags_compatible(signal_set::flags_t existing, signal_set::flags_t requested)
329 {
330 // dont_care is always compatible
331 5680x if ((existing & signal_set::dont_care) ||
332 2839x (requested & signal_set::dont_care))
333 7x return true;
334
335 // Mask out dont_care bit for comparison
336 2834x constexpr auto mask = ~signal_set::dont_care;
337 2834x 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 3491x open_signal_pipe(signal_state* state)
349 {
350 3491x if (state->read_fd >= 0)
351 3475x return {};
352
353 int fds[2];
354 16x if (::pipe(fds) < 0)
355 1x return make_err(errno);
356
357 36x for (int i = 0; i < 2; ++i)
358 {
359 27x int fl = ::fcntl(fds[i], F_GETFL, 0);
360 50x if (fl == -1 || ::fcntl(fds[i], F_SETFL, fl | O_NONBLOCK) == -1 ||
361 23x ::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 9x state->read_fd = fds[0];
371 9x state->write_fd = fds[1];
372 9x 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 1521x corosio_posix_signal_handler(int signal_number)
383 {
384 1521x int saved_errno = errno;
385 1521x signal_state* state = get_signal_state();
386 [[maybe_unused]] ssize_t r =
387 1521x ::write(state->write_fd, &signal_number, sizeof(int));
388 1521x errno = saved_errno;
389 // With sigaction(), the handler persists automatically (unlike some
390 // signal() implementations that reset to SIG_DFL).
391 1521x }
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 941x drain_signal_pipe()
399 {
400 941x signal_state* state = get_signal_state();
401 int signal_number;
402 2462x while (::read(state->read_fd, &signal_number, sizeof(int)) ==
403 static_cast<ssize_t>(sizeof(int)))
404 {
405 1521x posix_signal_service::deliver_signal(signal_number);
406 }
407 941x }
408
409 } // namespace posix_signal_detail
410
411 // signal_op implementation
412
413 inline void
414 1982x signal_op::operator()() noexcept
415 {
416 1982x if (ec_out)
417 1982x *ec_out = {};
418 1982x if (signal_out)
419 1982x *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 1982x auto* service = svc;
424 1982x svc = nullptr;
425 1982x auto* c = cont;
426 1982x auto ex = d;
427 1982x 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 1982x ex.post(*c);
432
433 // Balance the work_started() from start_wait
434 1982x if (service)
435 792x service->work_finished();
436 1982x }
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 3404x inline posix_signal::posix_signal(posix_signal_service& svc) noexcept
452 3404x : svc_(svc)
453 {
454 3404x }
455
456 inline std::coroutine_handle<>
457 205987x 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 205987x pending_op_.cont = &cont;
465 205987x pending_op_.d = d;
466 205987x pending_op_.ec_out = ec;
467 205987x pending_op_.signal_out = signal_out;
468 205987x 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 205987x stop_cb_.reset();
477
478 205987x if (token.stop_requested())
479 {
480 90x if (ec)
481 90x *ec = make_error_code(capy::error::canceled);
482 90x if (signal_out)
483 90x *signal_out = 0;
484 90x d.post(*pending_op_.cont);
485 // completion is always posted to scheduler queue, never inline.
486 90x 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 205897x svc_.reset_token_cancel(*this);
493 205897x if (token.stop_possible())
494 5155x stop_cb_.emplace(token, token_canceller{this});
495
496 205897x svc_.start_wait(*this, &pending_op_);
497 // completion is always posted to scheduler queue, never inline.
498 205897x return std::noop_coroutine();
499 }
500
501 inline std::error_code
502 3495x posix_signal::add(int signal_number, signal_set::flags_t flags)
503 {
504 3495x return svc_.add_signal(*this, signal_number, flags);
505 }
506
507 inline std::error_code
508 26x posix_signal::remove(int signal_number)
509 {
510 26x return svc_.remove_signal(*this, signal_number);
511 }
512
513 inline std::error_code
514 3490x posix_signal::clear()
515 {
516 3490x return svc_.clear_signals(*this);
517 }
518
519 inline void
520 203497x posix_signal::cancel() noexcept
521 {
522 203497x svc_.cancel_wait(*this);
523 203497x }
524
525 // posix_signal_service implementation
526
527 583x inline posix_signal_service::posix_signal_service(
528 583x capy::execution_context& ctx)
529 583x : sched_(&get_scheduler(ctx))
530 {
531 37895x for (int i = 0; i < max_signal_number; ++i)
532 {
533 37312x registrations_[i] = nullptr;
534 37312x registration_count_[i] = 0;
535 }
536 583x add_service(this);
537 583x }
538
539 1166x inline posix_signal_service::~posix_signal_service()
540 {
541 583x remove_service(this);
542 1166x }
543
544 inline void
545 583x 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 583x std::vector<posix_signal*> doomed;
554 583x std::vector<capy::continuation> parked;
555
556 {
557 posix_signal_detail::signal_state* state =
558 583x posix_signal_detail::get_signal_state();
559 583x std::lock_guard state_lock(state->mutex);
560 583x std::lock_guard lock(mutex_);
561
562 583x pool_.shutdown(
563 583x [&](posix_signal* impl)
564 {
565 3210x acquire(impl);
566 3210x doomed.push_back(impl);
567 3210x });
568 3793x 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 3210x if (std::exchange(impl->waiting_, false))
573 {
574 3210x if (auto* c = std::exchange(impl->pending_op_.cont, nullptr))
575 {
576 3210x if (c->h)
577 3210x parked.push_back(*c);
578 }
579 }
580
581 6420x while (auto* reg = impl->signals_)
582 {
583 3210x 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 3210x if (state->registration_count[signal_number] == 1)
592 {
593 408x struct sigaction sa = {};
594 408x sa.sa_handler = SIG_DFL;
595 408x sigemptyset(&sa.sa_mask);
596 408x sa.sa_flags = 0;
597 408x std::ignore = ::sigaction(signal_number, &sa, nullptr);
598 408x state->registered_flags[signal_number] = signal_set::none;
599 }
600
601 3210x --state->registration_count[signal_number];
602 3210x --registration_count_[signal_number];
603
604 3210x impl->signals_ = reg->next_in_set;
605 3210x delete reg;
606 3210x }
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 37895x for (int i = 0; i < max_signal_number; ++i)
615 37312x registrations_[i] = nullptr;
616 583x }
617
618 3793x for (auto& c : parked)
619 3210x capy::abandon(c);
620
621 3793x for (auto* impl : doomed)
622 3210x release(impl);
623 583x }
624
625 inline io_object::implementation*
626 3476x posix_signal_service::construct()
627 {
628 3476x return pool_.acquire(*this);
629 }
630
631 inline void
632 3476x posix_signal::retire() noexcept
633 {
634 3476x svc_.pool_.recycle(this);
635 3476x }
636
637 inline std::error_code
638 3495x posix_signal_service::add_signal(
639 posix_signal& impl, int signal_number, signal_set::flags_t flags)
640 {
641 3495x if (signal_number < 0 || signal_number >= max_signal_number)
642 4x 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 3491x 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 3491x 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 3491x std::lock_guard state_lock(state->mutex);
661 3491x if (auto ec = posix_signal_detail::open_signal_pipe(state))
662 7x return ec;
663 3491x }
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 3484x std::lock_guard reg_lock(reader_mutex_);
669 3484x if (!reader_registered_)
670 {
671 555x if (auto ec = sched_->register_signal_reader(state->read_fd))
672 2x return ec;
673 553x reader_registered_ = true;
674 }
675 3484x }
676
677 3482x std::lock_guard state_lock(state->mutex);
678 3482x std::lock_guard lock(mutex_);
679
680 // Find insertion point (list is sorted by signal number)
681 3482x signal_registration** insertion_point = &impl.signals_;
682 3482x signal_registration* reg = impl.signals_;
683 3505x while (reg && reg->signal_number < signal_number)
684 {
685 23x insertion_point = &reg->next_in_set;
686 23x 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 3482x if (reg && reg->signal_number == signal_number)
692 {
693 13x if (!posix_signal_detail::flags_compatible(reg->flags, flags))
694 4x return make_error_code(std::errc::invalid_argument);
695 9x return {};
696 }
697
698 // Check flag compatibility with global registration
699 // (different signal_set already registered this signal with different flags)
700 3469x if (state->registration_count[signal_number] > 0)
701 {
702 2828x if (!posix_signal_detail::flags_compatible(
703 state->registered_flags[signal_number], flags))
704 2x return make_error_code(std::errc::invalid_argument);
705 }
706
707 3467x auto* new_reg = new signal_registration;
708 3467x new_reg->signal_number = signal_number;
709 3467x new_reg->flags = flags;
710 3467x new_reg->owner = &impl;
711 3467x new_reg->undelivered = 0;
712
713 // Install signal handler on first global registration
714 3467x if (state->registration_count[signal_number] == 0)
715 {
716 641x struct sigaction sa = {};
717 641x sa.sa_handler = posix_signal_detail::corosio_posix_signal_handler;
718 641x sigemptyset(&sa.sa_mask);
719 641x sa.sa_flags = posix_signal_detail::to_sigaction_flags(flags);
720
721 641x 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 640x state->registered_flags[signal_number] = flags;
729 }
730
731 3466x new_reg->next_in_set = reg;
732 3466x *insertion_point = new_reg;
733
734 3466x new_reg->next_in_table = registrations_[signal_number];
735 3466x new_reg->prev_in_table = nullptr;
736 3466x if (registrations_[signal_number])
737 2818x registrations_[signal_number]->prev_in_table = new_reg;
738 3466x registrations_[signal_number] = new_reg;
739
740 3466x ++state->registration_count[signal_number];
741 3466x ++registration_count_[signal_number];
742
743 3466x return {};
744 3482x }
745
746 inline std::error_code
747 26x posix_signal_service::remove_signal(posix_signal& impl, int signal_number)
748 {
749 26x if (signal_number < 0 || signal_number >= max_signal_number)
750 2x return make_error_code(std::errc::invalid_argument);
751
752 posix_signal_detail::signal_state* state =
753 24x posix_signal_detail::get_signal_state();
754 24x std::lock_guard state_lock(state->mutex);
755 24x std::lock_guard lock(mutex_);
756
757 24x signal_registration** deletion_point = &impl.signals_;
758 24x signal_registration* reg = impl.signals_;
759 26x while (reg && reg->signal_number < signal_number)
760 {
761 2x deletion_point = &reg->next_in_set;
762 2x reg = reg->next_in_set;
763 }
764
765 24x if (!reg || reg->signal_number != signal_number)
766 3x return {};
767
768 // Restore default handler on last global unregistration
769 21x if (state->registration_count[signal_number] == 1)
770 {
771 17x struct sigaction sa = {};
772 17x sa.sa_handler = SIG_DFL;
773 17x sigemptyset(&sa.sa_mask);
774 17x sa.sa_flags = 0;
775
776 17x if (::sigaction(signal_number, &sa, nullptr) < 0)
777 1x return make_error_code(std::errc::invalid_argument);
778
779 // Clear stored flags
780 16x state->registered_flags[signal_number] = signal_set::none;
781 }
782
783 20x *deletion_point = reg->next_in_set;
784
785 20x if (registrations_[signal_number] == reg)
786 18x registrations_[signal_number] = reg->next_in_table;
787 20x if (reg->prev_in_table)
788 2x reg->prev_in_table->next_in_table = reg->next_in_table;
789 20x if (reg->next_in_table)
790 2x reg->next_in_table->prev_in_table = reg->prev_in_table;
791
792 20x --state->registration_count[signal_number];
793 20x --registration_count_[signal_number];
794
795 20x delete reg;
796 20x return {};
797 24x }
798
799 inline std::error_code
800 3490x posix_signal_service::clear_signals(posix_signal& impl)
801 {
802 posix_signal_detail::signal_state* state =
803 3490x posix_signal_detail::get_signal_state();
804 3490x std::lock_guard state_lock(state->mutex);
805 3490x std::lock_guard lock(mutex_);
806
807 3490x std::error_code first_error;
808
809 3726x while (signal_registration* reg = impl.signals_)
810 {
811 236x int signal_number = reg->signal_number;
812
813 236x if (state->registration_count[signal_number] == 1)
814 {
815 216x struct sigaction sa = {};
816 216x sa.sa_handler = SIG_DFL;
817 216x sigemptyset(&sa.sa_mask);
818 216x sa.sa_flags = 0;
819
820 216x 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 216x state->registered_flags[signal_number] = signal_set::none;
825 }
826
827 236x impl.signals_ = reg->next_in_set;
828
829 236x if (registrations_[signal_number] == reg)
830 234x registrations_[signal_number] = reg->next_in_table;
831 236x if (reg->prev_in_table)
832 2x reg->prev_in_table->next_in_table = reg->next_in_table;
833 236x if (reg->next_in_table)
834 12x reg->next_in_table->prev_in_table = reg->prev_in_table;
835
836 236x --state->registration_count[signal_number];
837 236x --registration_count_[signal_number];
838
839 236x delete reg;
840 236x }
841
842 3490x if (first_error)
843 1x return first_error;
844 3489x return {};
845 3490x }
846
847 inline void
848 203497x posix_signal_service::cancel_wait(posix_signal& impl) noexcept
849 {
850 203497x bool was_waiting = false;
851 203497x signal_op* op = nullptr;
852
853 {
854 203497x std::lock_guard lock(mutex_);
855 203497x impl.cancelled_ = true;
856 203497x if (impl.waiting_)
857 {
858 11x was_waiting = true;
859 11x impl.waiting_ = false;
860 11x op = &impl.pending_op_;
861 11x sched_->cancel_posting_begin();
862 }
863 203497x }
864
865 203497x if (was_waiting)
866 {
867 11x if (op->ec_out)
868 11x *op->ec_out = make_error_code(capy::error::canceled);
869 11x if (op->signal_out)
870 11x *op->signal_out = 0;
871 11x op->d.post(*op->cont);
872 11x sched_->work_finished();
873 11x sched_->cancel_posting_end();
874 }
875 203497x }
876
877 inline void
878 961x posix_signal_service::cancel_wait_token(posix_signal& impl) noexcept
879 {
880 961x bool was_waiting = false;
881 961x signal_op* op = nullptr;
882
883 {
884 961x 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 961x impl.token_cancelled_ = true;
889 961x if (impl.waiting_)
890 {
891 610x was_waiting = true;
892 610x impl.waiting_ = false;
893 610x op = &impl.pending_op_;
894 610x sched_->cancel_posting_begin();
895 }
896 961x }
897
898 961x if (was_waiting)
899 {
900 610x if (op->ec_out)
901 610x *op->ec_out = make_error_code(capy::error::canceled);
902 610x if (op->signal_out)
903 610x *op->signal_out = 0;
904 610x op->d.post(*op->cont);
905 610x sched_->work_finished();
906 610x sched_->cancel_posting_end();
907 }
908 961x }
909
910 inline void
911 961x posix_signal::token_canceller::operator()() const noexcept
912 {
913 961x self->svc_.cancel_wait_token(*self);
914 961x }
915
916 inline void
917 205897x posix_signal_service::start_wait(posix_signal& impl, signal_op* op)
918 {
919 {
920 205897x std::lock_guard lock(mutex_);
921
922 // Check if cancel() was called before this wait started
923 205897x if (impl.cancelled_)
924 {
925 200002x impl.cancelled_ = false;
926 200002x if (op->ec_out)
927 200002x *op->ec_out = make_error_code(capy::error::canceled);
928 200002x if (op->signal_out)
929 200002x *op->signal_out = 0;
930 200002x op->d.post(*op->cont);
931 200002x 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 5895x if (impl.token_cancelled_)
937 {
938 82x impl.token_cancelled_ = false;
939 82x if (op->ec_out)
940 82x *op->ec_out = make_error_code(capy::error::canceled);
941 82x if (op->signal_out)
942 82x *op->signal_out = 0;
943 82x op->d.post(*op->cont);
944 82x return;
945 }
946
947 // Check for queued signals first (signal arrived before wait started)
948 5813x signal_registration* reg = impl.signals_;
949 10440x while (reg)
950 {
951 5817x if (reg->undelivered > 0)
952 {
953 1190x --reg->undelivered;
954 1190x op->signal_number = reg->signal_number;
955 // svc=nullptr: no work_finished needed since we never called work_started
956 1190x op->svc = nullptr;
957 // The caller's wait() holds the handle's reference.
958 1190x op->keep = detail::object_ref(&impl);
959 1190x sched_->post(op);
960 1190x return;
961 }
962 4627x reg = reg->next_in_set;
963 }
964
965 // No queued signals - wait for delivery
966 4623x impl.waiting_ = true;
967 // svc=this: signal_op::operator() will call work_finished() to balance this
968 4623x op->svc = this;
969 4623x sched_->work_started();
970 205897x }
971 }
972
973 inline void
974 1521x posix_signal_service::deliver_signal(int signal_number)
975 {
976 1521x if (signal_number < 0 || signal_number >= max_signal_number)
977 ✗ return;
978
979 posix_signal_detail::signal_state* state =
980 1521x posix_signal_detail::get_signal_state();
981 1521x std::lock_guard lock(state->mutex);
982
983 1521x posix_signal_service* service = state->service_list;
984 3842x while (service)
985 {
986 2321x std::lock_guard svc_lock(service->mutex_);
987
988 2321x signal_registration* reg = service->registrations_[signal_number];
989 4646x while (reg)
990 {
991 2325x posix_signal* impl = static_cast<posix_signal*>(reg->owner);
992
993 2325x 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 792x auto keep = detail::object_ref::try_from(impl);
999 792x if (keep)
1000 {
1001 792x impl->waiting_ = false;
1002 792x impl->pending_op_.signal_number = signal_number;
1003 792x impl->pending_op_.keep = std::move(keep);
1004 792x service->post(&impl->pending_op_);
1005 }
1006 792x }
1007 else
1008 {
1009 1533x ++reg->undelivered;
1010 }
1011
1012 2325x reg = reg->next_in_table;
1013 }
1014
1015 2321x service = service->next_;
1016 2321x }
1017 1521x }
1018
1019 inline void
1020 posix_signal_service::work_started() noexcept
1021 {
1022 sched_->work_started();
1023 }
1024
1025 inline void
1026 792x posix_signal_service::work_finished() noexcept
1027 {
1028 792x sched_->work_finished();
1029 792x }
1030
1031 inline void
1032 792x posix_signal_service::post(signal_op* op)
1033 {
1034 792x sched_->post(op);
1035 792x }
1036
1037 inline void
1038 583x posix_signal_service::add_service(posix_signal_service* service)
1039 {
1040 posix_signal_detail::signal_state* state =
1041 583x posix_signal_detail::get_signal_state();
1042 583x std::lock_guard lock(state->mutex);
1043
1044 583x service->next_ = state->service_list;
1045 583x service->prev_ = nullptr;
1046 583x if (state->service_list)
1047 10x state->service_list->prev_ = service;
1048 583x state->service_list = service;
1049 583x }
1050
1051 inline void
1052 583x posix_signal_service::remove_service(posix_signal_service* service)
1053 {
1054 posix_signal_detail::signal_state* state =
1055 583x posix_signal_detail::get_signal_state();
1056 583x std::lock_guard lock(state->mutex);
1057
1058 583x if (service->next_ || service->prev_ || state->service_list == service)
1059 {
1060 583x if (state->service_list == service)
1061 581x state->service_list = service->next_;
1062 583x if (service->prev_)
1063 2x service->prev_->next_ = service->next_;
1064 583x if (service->next_)
1065 8x service->next_->prev_ = service->prev_;
1066 583x service->next_ = nullptr;
1067 583x service->prev_ = nullptr;
1068 }
1069 583x }
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