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 % 168 167 1
Test Date: 2026-09-09 02:31:18 Functions: 100.0 % 12 12

           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                 : #include <boost/corosio/native/detail/posix/posix_resolver_service.hpp>
      28                 : #include <boost/corosio/native/detail/posix/posix_signal_service.hpp>
      29                 : #include <boost/corosio/native/detail/posix/posix_stream_file_service.hpp>
      30                 : #include <boost/corosio/native/detail/posix/posix_random_access_file_service.hpp>
      31                 : 
      32                 : #include <boost/corosio/detail/except.hpp>
      33                 : 
      34                 : #include <sys/select.h>
      35                 : #include <unistd.h>
      36                 : #include <errno.h>
      37                 : #include <fcntl.h>
      38                 : 
      39                 : #include <atomic>
      40                 : #include <chrono>
      41                 : #include <cstdint>
      42                 : #include <limits>
      43                 : #include <mutex>
      44                 : #include <new>
      45                 : #include <unordered_map>
      46                 : 
      47                 : namespace boost::corosio::detail {
      48                 : 
      49                 : struct select_op;
      50                 : 
      51                 : /** POSIX scheduler using select() for I/O multiplexing.
      52                 : 
      53                 :     This scheduler implements the scheduler interface using the POSIX select()
      54                 :     call for I/O event notification. It inherits the shared reactor threading
      55                 :     model from reactor_scheduler: signal state machine, inline completion
      56                 :     budget, work counting, and the do_one event loop.
      57                 : 
      58                 :     The design mirrors epoll_scheduler for behavioral consistency:
      59                 :     - Same single-reactor thread coordination model
      60                 :     - Same deferred I/O pattern (reactor marks ready; workers do I/O)
      61                 :     - Same timer integration pattern
      62                 : 
      63                 :     Known Limitations:
      64                 :     - FD_SETSIZE (~1024) limits maximum concurrent connections
      65                 :     - O(n) scanning: rebuilds fd_sets each iteration
      66                 :     - Level-triggered only (no edge-triggered mode)
      67                 : 
      68                 :     @par Thread Safety
      69                 :     All public member functions are thread-safe.
      70                 : */
      71                 : class BOOST_COROSIO_DECL select_scheduler final : public reactor_scheduler
      72                 : {
      73                 : public:
      74                 :     /** Construct the scheduler.
      75                 : 
      76                 :         Creates a self-pipe for reactor interruption.
      77                 : 
      78                 :         @param ctx Reference to the owning execution_context.
      79                 :         @param concurrency_hint Hint for expected thread count (unused).
      80                 :     */
      81                 :     select_scheduler(capy::execution_context& ctx, int concurrency_hint = -1);
      82                 : 
      83                 :     /// Destroy the scheduler.
      84                 :     ~select_scheduler() override;
      85                 : 
      86                 :     select_scheduler(select_scheduler const&)            = delete;
      87                 :     select_scheduler& operator=(select_scheduler const&) = delete;
      88                 : 
      89                 :     /// Shut down the scheduler, draining pending operations.
      90                 :     void shutdown() override;
      91                 : 
      92                 :     /** Return the maximum file descriptor value supported.
      93                 : 
      94                 :         Returns FD_SETSIZE - 1, the maximum fd value that can be
      95                 :         monitored by select(). Operations with fd >= FD_SETSIZE
      96                 :         will fail with EINVAL.
      97                 : 
      98                 :         @return The maximum supported file descriptor value.
      99                 :     */
     100                 :     static constexpr int max_fd() noexcept
     101                 :     {
     102                 :         return FD_SETSIZE - 1;
     103                 :     }
     104                 : 
     105                 :     /** Register a descriptor for persistent monitoring.
     106                 : 
     107                 :         The fd is added to the registered_descs_ map and will be
     108                 :         included in subsequent select() calls. The reactor is
     109                 :         interrupted so a blocked select() rebuilds its fd_sets.
     110                 : 
     111                 :         @param fd The file descriptor to register.
     112                 :         @param desc Pointer to descriptor state for this fd.
     113                 : 
     114                 :         @return The error if the fd cannot be tracked, otherwise a
     115                 :         default constructed error code.
     116                 :     */
     117                 :     std::error_code
     118                 :     register_descriptor(int fd, reactor_descriptor_state* desc) const;
     119                 : 
     120                 :     /** Deregister a persistently registered descriptor.
     121                 : 
     122                 :         @param fd The file descriptor to deregister.
     123                 :     */
     124                 :     void deregister_descriptor(int fd) const;
     125                 : 
     126                 :     /** Interrupt the reactor so it rebuilds its fd_sets.
     127                 : 
     128                 :         Called when a write, connect, or write-wait op is registered
     129                 :         after the reactor's snapshot was taken. Without this,
     130                 :         select() may block not watching for writability on the fd.
     131                 :     */
     132                 :     void notify_reactor() const;
     133                 : 
     134                 :     /// Watch the read end of the POSIX signal self-pipe (see scheduler.hpp).
     135 HIT          55 :     [[nodiscard]] std::error_code register_signal_reader(int read_fd) override
     136                 :     {
     137              55 :         return register_descriptor(read_fd, signal_pipe_reader_.arm());
     138                 :     }
     139                 : 
     140                 : private:
     141                 :     void run_task(lock_type& lock, context_type& ctx, long timeout_us) override;
     142                 :     void interrupt_reactor() const override;
     143                 :     long calculate_timeout(long requested_timeout_us) const;
     144                 : 
     145                 :     // Watches the global signal self-pipe's read end (armed lazily by
     146                 :     // register_signal_reader on the first signal registration).
     147                 :     reactor_signal_pipe_reader signal_pipe_reader_;
     148                 : 
     149                 :     // Self-pipe for interrupting select()
     150                 :     int pipe_fds_[2]; // [0]=read, [1]=write
     151                 : 
     152                 :     // Per-fd tracking for fd_set building
     153                 :     mutable std::unordered_map<int, reactor_descriptor_state*>
     154                 :         registered_descs_;
     155                 :     mutable int max_fd_ = -1;
     156                 : };
     157                 : 
     158             886 : inline select_scheduler::select_scheduler(capy::execution_context& ctx, int)
     159             886 :     : pipe_fds_{-1, -1}
     160             886 :     , max_fd_(-1)
     161                 : {
     162             886 :     if (::pipe(pipe_fds_) < 0)
     163               1 :         detail::throw_system_error(make_err(errno), "pipe");
     164                 : 
     165            2646 :     for (int i = 0; i < 2; ++i)
     166                 :     {
     167            1767 :         int flags = ::fcntl(pipe_fds_[i], F_GETFL, 0);
     168            1767 :         if (flags == -1)
     169                 :         {
     170               2 :             int errn = errno;
     171               2 :             ::close(pipe_fds_[0]);
     172               2 :             ::close(pipe_fds_[1]);
     173               2 :             detail::throw_system_error(make_err(errn), "fcntl F_GETFL");
     174                 :         }
     175            1765 :         if (::fcntl(pipe_fds_[i], F_SETFL, flags | O_NONBLOCK) == -1)
     176                 :         {
     177               2 :             int errn = errno;
     178               2 :             ::close(pipe_fds_[0]);
     179               2 :             ::close(pipe_fds_[1]);
     180               2 :             detail::throw_system_error(make_err(errn), "fcntl F_SETFL");
     181                 :         }
     182            1763 :         if (::fcntl(pipe_fds_[i], F_SETFD, FD_CLOEXEC) == -1)
     183                 :         {
     184               2 :             int errn = errno;
     185               2 :             ::close(pipe_fds_[0]);
     186               2 :             ::close(pipe_fds_[1]);
     187               2 :             detail::throw_system_error(make_err(errn), "fcntl F_SETFD");
     188                 :         }
     189                 :     }
     190                 : 
     191             879 :     timer_svc_ = &get_timer_service(ctx, *this);
     192             879 :     timer_svc_->set_on_earliest_changed(
     193            3628 :         timer_service::callback(this, [](void* p) {
     194            2749 :             static_cast<select_scheduler*>(p)->interrupt_reactor();
     195            2749 :         }));
     196                 : 
     197             879 :     get_resolver_service(ctx, *this);
     198             879 :     get_signal_service(ctx, *this);
     199             879 :     get_stream_file_service(ctx, *this);
     200             879 :     get_random_access_file_service(ctx, *this);
     201                 : 
     202             879 :     completed_ops_.push(&task_op_);
     203             900 : }
     204                 : 
     205            1758 : inline select_scheduler::~select_scheduler()
     206                 : {
     207             879 :     if (pipe_fds_[0] >= 0)
     208             879 :         ::close(pipe_fds_[0]);
     209             879 :     if (pipe_fds_[1] >= 0)
     210             879 :         ::close(pipe_fds_[1]);
     211            1758 : }
     212                 : 
     213                 : inline void
     214             879 : select_scheduler::shutdown()
     215                 : {
     216             879 :     shutdown_drain();
     217                 : 
     218             879 :     if (pipe_fds_[1] >= 0)
     219             879 :         interrupt_reactor();
     220             879 : }
     221                 : 
     222                 : inline std::error_code
     223            4894 : select_scheduler::register_descriptor(
     224                 :     int fd, reactor_descriptor_state* desc) const
     225                 : {
     226            4894 :     if (fd < 0 || fd >= FD_SETSIZE)
     227               1 :         return make_err(EMFILE);
     228                 : 
     229            4893 :     desc->registered_events = reactor_event_read | reactor_event_write;
     230            4893 :     desc->fd                = fd;
     231            4893 :     desc->scheduler_        = this;
     232            4893 :     desc->mutex.set_enabled(reactor_io_locking_);
     233            4893 :     desc->ready_events_.store(0, std::memory_order_relaxed);
     234                 : 
     235                 :     {
     236            4893 :         conditionally_enabled_mutex::scoped_lock lock(desc->mutex);
     237            4893 :         desc->impl_ref_.reset();
     238            4893 :         desc->read_ready  = false;
     239            4893 :         desc->write_ready = false;
     240            4893 :     }
     241                 : 
     242                 :     {
     243            4893 :         mutex_type::scoped_lock lock(mutex_);
     244                 :         try
     245                 :         {
     246            4893 :             registered_descs_[fd] = desc;
     247                 :         }
     248               1 :         catch (std::bad_alloc const&)
     249                 :         {
     250               1 :             return make_err(ENOMEM);
     251               1 :         }
     252            4892 :         if (fd > max_fd_)
     253            4838 :             max_fd_ = fd;
     254            4893 :     }
     255                 : 
     256            4892 :     interrupt_reactor();
     257            4892 :     return {};
     258                 : }
     259                 : 
     260                 : inline void
     261            4838 : select_scheduler::deregister_descriptor(int fd) const
     262                 : {
     263            4838 :     mutex_type::scoped_lock lock(mutex_);
     264                 : 
     265            4838 :     auto it = registered_descs_.find(fd);
     266            4838 :     if (it == registered_descs_.end())
     267 MIS           0 :         return;
     268                 : 
     269 HIT        4838 :     registered_descs_.erase(it);
     270                 : 
     271            4838 :     if (fd == max_fd_)
     272                 :     {
     273            4501 :         max_fd_ = pipe_fds_[0];
     274            8600 :         for (auto& [registered_fd, state] : registered_descs_)
     275                 :         {
     276            4099 :             if (registered_fd > max_fd_)
     277            4006 :                 max_fd_ = registered_fd;
     278                 :         }
     279                 :     }
     280            4838 : }
     281                 : 
     282                 : inline void
     283            2179 : select_scheduler::notify_reactor() const
     284                 : {
     285            2179 :     interrupt_reactor();
     286            2179 : }
     287                 : 
     288                 : inline void
     289           11496 : select_scheduler::interrupt_reactor() const
     290                 : {
     291           11496 :     char byte               = 1;
     292           11496 :     [[maybe_unused]] auto r = ::write(pipe_fds_[1], &byte, 1);
     293           11496 : }
     294                 : 
     295                 : inline long
     296          291727 : select_scheduler::calculate_timeout(long requested_timeout_us) const
     297                 : {
     298          291727 :     if (requested_timeout_us == 0)
     299                 :         return 0; // LCOV_EXCL_LINE run_task passes 0 via task_interrupted_, never through this argument
     300                 : 
     301          291727 :     auto nearest = timer_svc_->nearest_expiry();
     302          291727 :     if (nearest == timer_service::time_point::max())
     303             728 :         return requested_timeout_us;
     304                 : 
     305          290999 :     auto now = std::chrono::steady_clock::now();
     306          290999 :     if (nearest <= now)
     307             530 :         return 0;
     308                 : 
     309                 :     auto timer_timeout_us =
     310          290469 :         std::chrono::duration_cast<std::chrono::microseconds>(nearest - now)
     311          290469 :             .count();
     312                 : 
     313          290469 :     constexpr auto long_max =
     314                 :         static_cast<long long>((std::numeric_limits<long>::max)());
     315                 :     auto capped_timer_us =
     316          290469 :         (std::min)((std::max)(static_cast<long long>(timer_timeout_us),
     317          290469 :                               static_cast<long long>(0)),
     318          290469 :                    long_max);
     319                 : 
     320          290469 :     if (requested_timeout_us < 0)
     321          290467 :         return static_cast<long>(capped_timer_us);
     322                 : 
     323                 :     return static_cast<long>(
     324               2 :         (std::min)(static_cast<long long>(requested_timeout_us),
     325               2 :                    capped_timer_us));
     326                 : }
     327                 : 
     328                 : inline void
     329          315410 : select_scheduler::run_task(lock_type& lock, context_type& ctx, long timeout_us)
     330                 : {
     331                 :     long effective_timeout_us =
     332          315410 :         task_interrupted_ ? 0 : calculate_timeout(timeout_us);
     333                 : 
     334                 :     // Snapshot registered descriptors while holding lock.
     335                 :     // Record which fds need write monitoring to avoid a hot loop:
     336                 :     // select is level-triggered so writable sockets (nearly always
     337                 :     // writable) would cause select() to return immediately every
     338                 :     // iteration if unconditionally added to write_fds. Membership
     339                 :     // stays opt-in: a parked write wait opts in the same way a
     340                 :     // parked write or connect op does.
     341                 :     struct fd_entry
     342                 :     {
     343                 :         int fd;
     344                 :         reactor_descriptor_state* desc;
     345                 :         bool needs_write;
     346                 :     };
     347                 :     fd_entry snapshot[FD_SETSIZE];
     348          315410 :     int snapshot_count = 0;
     349                 : 
     350          816233 :     for (auto& [fd, desc] : registered_descs_)
     351                 :     {
     352          500823 :         if (snapshot_count < FD_SETSIZE)
     353                 :         {
     354          500823 :             conditionally_enabled_mutex::scoped_lock desc_lock(desc->mutex);
     355          500823 :             snapshot[snapshot_count].fd   = fd;
     356          500823 :             snapshot[snapshot_count].desc = desc;
     357          500823 :             snapshot[snapshot_count].needs_write =
     358          500823 :                 (desc->write_op || desc->connect_op || desc->wait_write_op);
     359          500823 :             ++snapshot_count;
     360          500823 :         }
     361                 :     }
     362                 : 
     363          315410 :     if (lock.owns_lock())
     364          291728 :         lock.unlock();
     365                 : 
     366          315410 :     task_cleanup on_exit{this, &lock, ctx};
     367                 : 
     368                 :     fd_set read_fds, write_fds, except_fds;
     369         5361970 :     FD_ZERO(&read_fds);
     370         5361970 :     FD_ZERO(&write_fds);
     371         5361970 :     FD_ZERO(&except_fds);
     372                 : 
     373          315410 :     FD_SET(pipe_fds_[0], &read_fds);
     374          315410 :     int nfds = pipe_fds_[0];
     375                 : 
     376          816233 :     for (int i = 0; i < snapshot_count; ++i)
     377                 :     {
     378          500823 :         int fd = snapshot[i].fd;
     379          500823 :         FD_SET(fd, &read_fds);
     380          500823 :         if (snapshot[i].needs_write)
     381           13218 :             FD_SET(fd, &write_fds);
     382          500823 :         FD_SET(fd, &except_fds);
     383          500823 :         if (fd > nfds)
     384          315019 :             nfds = fd;
     385                 :     }
     386                 : 
     387                 :     struct timeval tv;
     388          315410 :     struct timeval* tv_ptr = nullptr;
     389          315410 :     if (effective_timeout_us >= 0)
     390                 :     {
     391          314700 :         tv.tv_sec  = effective_timeout_us / 1000000;
     392          314700 :         tv.tv_usec = effective_timeout_us % 1000000;
     393          314700 :         tv_ptr     = &tv;
     394                 :     }
     395                 : 
     396          315410 :     int ready = ::select(nfds + 1, &read_fds, &write_fds, &except_fds, tv_ptr);
     397                 : 
     398                 :     // EINTR: signal interrupted select(), just retry.
     399                 :     // EBADF: an fd was closed between snapshot and select(); retry
     400                 :     // with a fresh snapshot from registered_descs_.
     401                 :     // Both fall through with no ready descriptors rather than
     402                 :     // returning: the caller handed this function an owned lock that
     403                 :     // only the epilogue below re-acquires.
     404          315410 :     if (ready < 0)
     405                 :     {
     406               3 :         if (errno != EINTR && errno != EBADF)
     407               1 :             detail::throw_system_error(make_err(errno), "select");
     408               2 :         ready = 0;
     409                 :     }
     410                 : 
     411                 :     // Process timers outside the lock
     412          315409 :     timer_svc_->process_expired();
     413                 : 
     414          315409 :     ready_queue local_ops;
     415                 : 
     416          315409 :     if (ready > 0)
     417                 :     {
     418          299359 :         if (FD_ISSET(pipe_fds_[0], &read_fds))
     419                 :         {
     420                 :             char buf[256];
     421           10110 :             while (::read(pipe_fds_[0], buf, sizeof(buf)) > 0)
     422                 :             {
     423                 :             }
     424                 :         }
     425                 : 
     426          754097 :         for (int i = 0; i < snapshot_count; ++i)
     427                 :         {
     428          454738 :             int fd                         = snapshot[i].fd;
     429          454738 :             reactor_descriptor_state* desc = snapshot[i].desc;
     430                 : 
     431          454738 :             std::uint32_t flags = 0;
     432          454738 :             if (FD_ISSET(fd, &read_fds))
     433          298926 :                 flags |= reactor_event_read;
     434          454738 :             if (FD_ISSET(fd, &write_fds))
     435            2171 :                 flags |= reactor_event_write;
     436          454738 :             if (FD_ISSET(fd, &except_fds))
     437              16 :                 flags |= reactor_event_error;
     438                 : 
     439          454738 :             if (flags == 0)
     440          153650 :                 continue;
     441                 : 
     442          301088 :             desc->add_ready_events(flags);
     443                 : 
     444          301088 :             bool expected = false;
     445          301088 :             if (desc->is_enqueued_.compare_exchange_strong(
     446                 :                     expected, true, std::memory_order_release,
     447                 :                     std::memory_order_relaxed))
     448                 :             {
     449          301088 :                 local_ops.push(desc);
     450                 :             }
     451                 :         }
     452                 :     }
     453                 : 
     454          315409 :     lock.lock();
     455                 : 
     456          315409 :     completed_ops_.splice(local_ops);
     457          315410 : }
     458                 : 
     459                 : } // namespace boost::corosio::detail
     460                 : 
     461                 : #endif // BOOST_COROSIO_HAS_SELECT
     462                 : 
     463                 : #endif // BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_SCHEDULER_HPP
        

Generated by: LCOV version 2.3