LCOV - code coverage report
Current view: top level - corosio/native/detail/select - select_scheduler.hpp (source / functions) Coverage Total Hit Missed
Test: coverage_remapped.info Lines: 99.4 % 178 177 1
Test Date: 2026-10-22 14:28:44 Functions: 100.0 % 13 13

           TLA  Line data    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_SELECT_SELECT_SCHEDULER_HPP
      12                 : #define BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_SCHEDULER_HPP
      13                 : 
      14                 : #include <boost/corosio/detail/platform.hpp>
      15                 : 
      16                 : #if BOOST_COROSIO_HAS_SELECT
      17                 : 
      18                 : #include <boost/corosio/detail/config.hpp>
      19                 : #include <boost/capy/ex/execution_context.hpp>
      20                 : 
      21                 : #include <boost/corosio/native/detail/reactor/reactor_scheduler.hpp>
      22                 : #include <boost/corosio/native/detail/reactor/reactor_signal_pipe.hpp>
      23                 : 
      24                 : #include <boost/corosio/native/detail/select/select_traits.hpp>
      25                 : #include <boost/corosio/detail/timer_service.hpp>
      26                 : #include <boost/corosio/native/detail/make_err.hpp>
      27                 : 
      28                 : #include <boost/corosio/detail/except.hpp>
      29                 : 
      30                 : #include <sys/select.h>
      31                 : #include <unistd.h>
      32                 : #include <errno.h>
      33                 : #include <fcntl.h>
      34                 : 
      35                 : #include <atomic>
      36                 : #include <chrono>
      37                 : #include <cstdint>
      38                 : #include <limits>
      39                 : #include <mutex>
      40                 : #include <new>
      41                 : #include <unordered_map>
      42                 : 
      43                 : namespace boost::corosio::detail {
      44                 : 
      45                 : struct select_op;
      46                 : 
      47                 : /** POSIX scheduler using select() for I/O multiplexing.
      48                 : 
      49                 :     This scheduler implements the scheduler interface using the POSIX select()
      50                 :     call for I/O event notification. It inherits the shared reactor threading
      51                 :     model from reactor_scheduler: signal state machine, inline completion
      52                 :     budget, work counting, and the do_one event loop.
      53                 : 
      54                 :     The design mirrors epoll_scheduler for behavioral consistency:
      55                 :     - Same single-reactor thread coordination model
      56                 :     - Same deferred I/O pattern (reactor marks ready; workers do I/O)
      57                 :     - Same timer integration pattern
      58                 : 
      59                 :     Known Limitations:
      60                 :     - FD_SETSIZE (~1024) limits maximum concurrent connections
      61                 :     - O(n) scanning: rebuilds fd_sets each iteration
      62                 :     - Level-triggered only (no edge-triggered mode)
      63                 : 
      64                 :     @par Thread Safety
      65                 :     All public member functions are thread-safe.
      66                 : */
      67                 : class BOOST_COROSIO_DECL select_scheduler final : public reactor_scheduler
      68                 : {
      69                 : public:
      70                 :     /** Construct the scheduler.
      71                 : 
      72                 :         Creates a self-pipe for reactor interruption.
      73                 : 
      74                 :         @param ctx Reference to the owning execution_context.
      75                 :         @param concurrency_hint Hint for expected thread count (unused).
      76                 :     */
      77                 :     select_scheduler(capy::execution_context& ctx, int concurrency_hint = -1);
      78                 : 
      79                 :     /// Destroy the scheduler.
      80                 :     ~select_scheduler() override;
      81                 : 
      82                 :     select_scheduler(select_scheduler const&)            = delete;
      83                 :     select_scheduler& operator=(select_scheduler const&) = delete;
      84                 : 
      85                 :     /// Shut down the scheduler, draining pending operations.
      86                 :     void shutdown() override;
      87                 : 
      88                 :     /** Return the maximum file descriptor value supported.
      89                 : 
      90                 :         Returns FD_SETSIZE - 1, the maximum fd value that can be
      91                 :         monitored by select(). Operations with fd >= FD_SETSIZE
      92                 :         will fail with EINVAL.
      93                 : 
      94                 :         @return The maximum supported file descriptor value.
      95                 :     */
      96                 :     static constexpr int max_fd() noexcept
      97                 :     {
      98                 :         return FD_SETSIZE - 1;
      99                 :     }
     100                 : 
     101                 :     /** Register a descriptor for persistent monitoring.
     102                 : 
     103                 :         The fd is added to the registered_descs_ map and will be
     104                 :         included in subsequent select() calls. The reactor is
     105                 :         interrupted so a blocked select() rebuilds its fd_sets.
     106                 : 
     107                 :         @param fd The file descriptor to register.
     108                 :         @param desc Pointer to descriptor state for this fd.
     109                 : 
     110                 :         @return The error if the fd cannot be tracked, otherwise a
     111                 :         default constructed error code.
     112                 :     */
     113                 :     std::error_code
     114                 :     register_descriptor(int fd, reactor_descriptor_state* desc) const;
     115                 : 
     116                 :     /// No-op: write readiness is watched from registration on.
     117                 :     std::error_code
     118 HIT        2709 :     ensure_write_registered(int, reactor_descriptor_state*) const noexcept
     119                 :     {
     120            2709 :         return {};
     121                 :     }
     122                 : 
     123                 :     /** Deregister a persistently registered descriptor.
     124                 : 
     125                 :         @param fd The file descriptor to deregister.
     126                 :     */
     127                 :     void deregister_descriptor(int fd) const;
     128                 : 
     129                 :     /** Interrupt the reactor so it rebuilds its fd_sets.
     130                 : 
     131                 :         Called when a write, connect, or write-wait op is registered
     132                 :         after the reactor's snapshot was taken. Without this,
     133                 :         select() may block not watching for writability on the fd.
     134                 :     */
     135                 :     void notify_reactor() const;
     136                 : 
     137                 :     /// Watch the read end of the POSIX signal self-pipe (see scheduler.hpp).
     138             273 :     [[nodiscard]] std::error_code register_signal_reader(int read_fd) override
     139                 :     {
     140             273 :         return register_descriptor(read_fd, signal_pipe_reader_.arm());
     141                 :     }
     142                 : 
     143                 : private:
     144                 :     void run_task(lock_type& lock, context_type& ctx, long timeout_us) override;
     145                 :     void interrupt_reactor() const override;
     146                 : 
     147                 :     /// Rebuild the poll set a blocked select() was given.
     148             467 :     void notify_reparked() const override
     149                 :     {
     150             467 :         interrupt_reactor();
     151             467 :     }
     152                 :     long calculate_timeout(long requested_timeout_us) const;
     153                 : 
     154                 :     // Watches the global signal self-pipe's read end (armed lazily by
     155                 :     // register_signal_reader on the first signal registration).
     156                 :     reactor_signal_pipe_reader signal_pipe_reader_;
     157                 : 
     158                 :     // Self-pipe for interrupting select()
     159                 :     int pipe_fds_[2]; // [0]=read, [1]=write
     160                 : 
     161                 :     // Per-fd tracking for fd_set building
     162                 :     mutable std::unordered_map<int, reactor_descriptor_state*>
     163                 :         registered_descs_;
     164                 :     mutable int max_fd_ = -1;
     165                 : };
     166                 : 
     167            1958 : inline select_scheduler::select_scheduler(capy::execution_context&, int)
     168            1958 :     : pipe_fds_{-1, -1}
     169            1958 :     , max_fd_(-1)
     170                 : {
     171            1958 :     if (::pipe(pipe_fds_) < 0)
     172               1 :         detail::throw_system_error(make_err(errno), "pipe");
     173                 : 
     174            5862 :     for (int i = 0; i < 2; ++i)
     175                 :     {
     176            3911 :         int flags = ::fcntl(pipe_fds_[i], F_GETFL, 0);
     177            3911 :         if (flags == -1)
     178                 :         {
     179               2 :             int errn = errno;
     180               2 :             ::close(pipe_fds_[0]);
     181               2 :             ::close(pipe_fds_[1]);
     182               2 :             detail::throw_system_error(make_err(errn), "fcntl F_GETFL");
     183                 :         }
     184            3909 :         if (::fcntl(pipe_fds_[i], F_SETFL, flags | O_NONBLOCK) == -1)
     185                 :         {
     186               2 :             int errn = errno;
     187               2 :             ::close(pipe_fds_[0]);
     188               2 :             ::close(pipe_fds_[1]);
     189               2 :             detail::throw_system_error(make_err(errn), "fcntl F_SETFL");
     190                 :         }
     191            3907 :         if (::fcntl(pipe_fds_[i], F_SETFD, FD_CLOEXEC) == -1)
     192                 :         {
     193               2 :             int errn = errno;
     194               2 :             ::close(pipe_fds_[0]);
     195               2 :             ::close(pipe_fds_[1]);
     196               2 :             detail::throw_system_error(make_err(errn), "fcntl F_SETFD");
     197                 :         }
     198                 :     }
     199                 : 
     200            1951 :     completed_ops_.push(&task_op_);
     201            1972 : }
     202                 : 
     203            3902 : inline select_scheduler::~select_scheduler()
     204                 : {
     205            1951 :     if (pipe_fds_[0] >= 0)
     206            1951 :         ::close(pipe_fds_[0]);
     207            1951 :     if (pipe_fds_[1] >= 0)
     208            1951 :         ::close(pipe_fds_[1]);
     209            3902 : }
     210                 : 
     211                 : inline void
     212            1951 : select_scheduler::shutdown()
     213                 : {
     214            1951 :     shutdown_drain();
     215                 : 
     216            1951 :     if (pipe_fds_[1] >= 0)
     217            1951 :         interrupt_reactor();
     218            1951 : }
     219                 : 
     220                 : inline std::error_code
     221            9846 : select_scheduler::register_descriptor(
     222                 :     int fd, reactor_descriptor_state* desc) const
     223                 : {
     224            9846 :     if (fd < 0 || fd >= FD_SETSIZE)
     225               1 :         return make_err(EMFILE);
     226                 : 
     227                 :     // A completion queued by an earlier registration of the same state
     228                 :     // may still be running, so the fields it reads change under its
     229                 :     // mutex, and the mutex mode is only written when it differs.
     230            9845 :     if (desc->mutex.enabled() != reactor_io_locking_)
     231               3 :         desc->mutex.set_enabled(reactor_io_locking_);
     232                 :     {
     233            9845 :         conditionally_enabled_mutex::scoped_lock lock(desc->mutex);
     234            9845 :         desc->registered_events = reactor_event_read | reactor_event_write;
     235            9845 :         desc->unpollable        = false; // the state is reused across adoptions
     236            9845 :         desc->fd                = fd;
     237            9845 :         desc->scheduler_        = this;
     238            9845 :         desc->ready_events_.store(0, std::memory_order_release);
     239                 :         // object_ref_ is not touched: a queued invocation from an
     240                 :         // earlier registration still owns the reference in it.
     241            9845 :         desc->read_ready  = false;
     242            9845 :         desc->write_ready = false;
     243            9845 :     }
     244                 : 
     245                 :     {
     246            9845 :         mutex_type::scoped_lock lock(mutex_);
     247                 :         try
     248                 :         {
     249            9845 :             registered_descs_[fd] = desc;
     250                 :         }
     251               1 :         catch (std::bad_alloc const&)
     252                 :         {
     253               1 :             return make_err(ENOMEM);
     254               1 :         }
     255            9844 :         if (fd > max_fd_)
     256            9363 :             max_fd_ = fd;
     257            9845 :     }
     258                 : 
     259            9844 :     interrupt_reactor();
     260            9844 :     return {};
     261                 : }
     262                 : 
     263                 : inline void
     264            9572 : select_scheduler::deregister_descriptor(int fd) const
     265                 : {
     266            9572 :     mutex_type::scoped_lock lock(mutex_);
     267                 : 
     268            9572 :     auto it = registered_descs_.find(fd);
     269            9572 :     if (it == registered_descs_.end())
     270 MIS           0 :         return;
     271                 : 
     272 HIT        9572 :     registered_descs_.erase(it);
     273                 : 
     274            9572 :     if (fd == max_fd_)
     275                 :     {
     276            7658 :         max_fd_ = pipe_fds_[0];
     277           12714 :         for (auto& [registered_fd, state] : registered_descs_)
     278                 :         {
     279            5056 :             if (registered_fd > max_fd_)
     280            4823 :                 max_fd_ = registered_fd;
     281                 :         }
     282                 :     }
     283            9572 : }
     284                 : 
     285                 : inline void
     286            6764 : select_scheduler::notify_reactor() const
     287                 : {
     288            6764 :     interrupt_reactor();
     289            6764 : }
     290                 : 
     291                 : inline void
     292           25515 : select_scheduler::interrupt_reactor() const
     293                 : {
     294           25515 :     char byte               = 1;
     295           25515 :     [[maybe_unused]] auto r = ::write(pipe_fds_[1], &byte, 1);
     296           25515 : }
     297                 : 
     298                 : inline long
     299           17846 : select_scheduler::calculate_timeout(long requested_timeout_us) const
     300                 : {
     301           17846 :     if (requested_timeout_us == 0)
     302                 :         return 0; // LCOV_EXCL_LINE run_task passes 0 via task_interrupted_, never through this argument
     303                 : 
     304           17846 :     auto* tsvc = timers();
     305           17846 :     if (!tsvc)
     306           11374 :         return requested_timeout_us;
     307            6472 :     auto nearest = tsvc->nearest_expiry();
     308            6472 :     if (nearest == timer_service::time_point::max())
     309             139 :         return requested_timeout_us;
     310                 : 
     311            6333 :     auto now = std::chrono::steady_clock::now();
     312            6333 :     if (nearest <= now)
     313             155 :         return 0;
     314                 : 
     315                 :     auto timer_timeout_us =
     316            6178 :         std::chrono::duration_cast<std::chrono::microseconds>(nearest - now)
     317            6178 :             .count();
     318                 : 
     319            6178 :     constexpr auto long_max =
     320                 :         static_cast<long long>((std::numeric_limits<long>::max)());
     321                 :     auto capped_timer_us =
     322            6178 :         (std::min)((std::max)(static_cast<long long>(timer_timeout_us),
     323            6178 :                               static_cast<long long>(0)),
     324            6178 :                    long_max);
     325                 : 
     326            6178 :     if (requested_timeout_us < 0)
     327            6172 :         return static_cast<long>(capped_timer_us);
     328                 : 
     329                 :     return static_cast<long>(
     330               6 :         (std::min)(static_cast<long long>(requested_timeout_us),
     331               6 :                    capped_timer_us));
     332                 : }
     333                 : 
     334                 : inline void
     335          144069 : select_scheduler::run_task(lock_type& lock, context_type& ctx, long timeout_us)
     336                 : {
     337                 :     long effective_timeout_us =
     338          144069 :         task_interrupted_ ? 0 : calculate_timeout(timeout_us);
     339                 : 
     340                 :     // Snapshot registered descriptors while holding lock.
     341                 :     // Record which directions each fd needs monitored to avoid a hot
     342                 :     // loop: select is level-triggered, so a writable socket (nearly
     343                 :     // always writable) or an always-readable fd (/dev/zero, a pipe at
     344                 :     // EOF) would return select() immediately every iteration if
     345                 :     // unconditionally added. Membership in both sets is opt-in: a
     346                 :     // parked op or wait in a direction opts that direction in. The
     347                 :     // exceptional set is opt-in too, for any parked op or wait:
     348                 :     // Darwin reports a character device (/dev/zero, /dev/null) as
     349                 :     // exceptional on every call.
     350                 :     struct fd_entry
     351                 :     {
     352                 :         int fd;
     353                 :         reactor_descriptor_state* desc;
     354                 :         std::uint32_t want;
     355                 :     };
     356                 :     fd_entry snapshot[FD_SETSIZE];
     357          144069 :     int snapshot_count = 0;
     358                 : 
     359                 :     // The caller may have released the lock to hand queued work to a
     360                 :     // peer, and deregister_descriptor() erases from this map under it.
     361          144069 :     if (!lock.owns_lock())
     362          126220 :         lock.lock();
     363                 : 
     364          398341 :     for (auto& [fd, desc] : registered_descs_)
     365                 :     {
     366          254272 :         if (snapshot_count < FD_SETSIZE)
     367                 :         {
     368          254272 :             conditionally_enabled_mutex::scoped_lock desc_lock(desc->mutex);
     369          254272 :             snapshot[snapshot_count].fd   = fd;
     370          254272 :             snapshot[snapshot_count].desc = desc;
     371          254272 :             snapshot[snapshot_count].want =
     372          254272 :                 ((desc->read_op || desc->wait_read_op) ? reactor_event_read
     373          254272 :                                                        : 0) |
     374          253898 :                 ((desc->write_op || desc->connect_op || desc->wait_write_op)
     375          508170 :                      ? reactor_event_write
     376          254272 :                      : 0) |
     377          254272 :                 (desc->wait_error_op ? reactor_event_error : 0);
     378          254272 :             ++snapshot_count;
     379          254272 :         }
     380                 :     }
     381                 : 
     382          144069 :     if (lock.owns_lock())
     383          144068 :         lock.unlock();
     384                 : 
     385          144069 :     task_cleanup on_exit{this, &lock, ctx};
     386                 : 
     387                 :     fd_set read_fds, write_fds, except_fds;
     388         2449173 :     FD_ZERO(&read_fds);
     389         2449173 :     FD_ZERO(&write_fds);
     390         2449173 :     FD_ZERO(&except_fds);
     391                 : 
     392          144069 :     FD_SET(pipe_fds_[0], &read_fds);
     393          144069 :     int nfds = pipe_fds_[0];
     394                 : 
     395          398341 :     for (int i = 0; i < snapshot_count; ++i)
     396                 :     {
     397          254272 :         int fd = snapshot[i].fd;
     398          254272 :         if (snapshot[i].want & reactor_event_read)
     399          117409 :             FD_SET(fd, &read_fds);
     400          254272 :         if (snapshot[i].want & reactor_event_write)
     401            2973 :             FD_SET(fd, &write_fds);
     402          254272 :         if (snapshot[i].want != 0)
     403          120577 :             FD_SET(fd, &except_fds);
     404          254272 :         if (fd > nfds)
     405          142229 :             nfds = fd;
     406                 :     }
     407                 : 
     408                 :     struct timeval tv;
     409          144069 :     struct timeval* tv_ptr = nullptr;
     410          144069 :     if (effective_timeout_us >= 0)
     411                 :     {
     412          133204 :         tv.tv_sec  = effective_timeout_us / 1000000;
     413          133204 :         tv.tv_usec = effective_timeout_us % 1000000;
     414          133204 :         tv_ptr     = &tv;
     415                 :     }
     416                 : 
     417          144069 :     int ready = ::select(nfds + 1, &read_fds, &write_fds, &except_fds, tv_ptr);
     418                 : 
     419                 :     // EINTR: signal interrupted select(), just retry.
     420                 :     // EBADF: an fd was closed between snapshot and select(); retry
     421                 :     // with a fresh snapshot from registered_descs_.
     422                 :     // Both fall through with no ready descriptors rather than
     423                 :     // returning: the caller handed this function an owned lock that
     424                 :     // only the epilogue below re-acquires.
     425          144069 :     if (ready < 0)
     426                 :     {
     427               3 :         if (errno != EINTR && errno != EBADF)
     428               1 :             detail::throw_system_error(make_err(errno), "select");
     429               2 :         ready = 0;
     430                 :     }
     431                 : 
     432                 :     // Process timers outside the lock
     433          144068 :     if (auto* tsvc = timers())
     434           27882 :         tsvc->process_expired();
     435                 : 
     436          144068 :     ready_queue local_ops;
     437                 : 
     438          144068 :     if (ready > 0)
     439                 :     {
     440           18344 :         if (FD_ISSET(pipe_fds_[0], &read_fds))
     441                 :         {
     442                 :             char buf[256];
     443           22660 :             while (::read(pipe_fds_[0], buf, sizeof(buf)) > 0)
     444                 :             {
     445                 :             }
     446                 :         }
     447                 : 
     448          105795 :         for (int i = 0; i < snapshot_count; ++i)
     449                 :         {
     450           87451 :             int fd                         = snapshot[i].fd;
     451           87451 :             reactor_descriptor_state* desc = snapshot[i].desc;
     452                 : 
     453           87451 :             std::uint32_t flags = 0;
     454           87451 :             if (FD_ISSET(fd, &read_fds))
     455            9725 :                 flags |= reactor_event_read;
     456           87451 :             if (FD_ISSET(fd, &write_fds))
     457            2689 :                 flags |= reactor_event_write;
     458           87451 :             if (FD_ISSET(fd, &except_fds))
     459              10 :                 flags |= reactor_event_error;
     460                 : 
     461           87451 :             if (flags == 0)
     462           75035 :                 continue;
     463                 : 
     464           12416 :             desc->add_ready_events(flags);
     465                 : 
     466           12416 :             bool expected = false;
     467           12416 :             if (desc->is_enqueued_.compare_exchange_strong(
     468                 :                     expected, true, std::memory_order_release,
     469                 :                     std::memory_order_relaxed))
     470                 :             {
     471            6215 :                 local_ops.push(desc);
     472                 :             }
     473                 :         }
     474                 :     }
     475                 : 
     476          144068 :     lock.lock();
     477                 : 
     478          144068 :     completed_ops_.splice(local_ops);
     479          144069 : }
     480                 : 
     481                 : } // namespace boost::corosio::detail
     482                 : 
     483                 : #endif // BOOST_COROSIO_HAS_SELECT
     484                 : 
     485                 : #endif // BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_SCHEDULER_HPP
        

Generated by: LCOV version 2.3