LCOV - code coverage report
Current view: top level - corosio/native/detail/epoll - epoll_scheduler.hpp (source / functions) Coverage Total Hit Missed
Test: coverage_remapped.info Lines: 97.7 % 177 173 4
Test Date: 2026-10-22 14:28:44 Functions: 100.0 % 15 15

           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_EPOLL_EPOLL_SCHEDULER_HPP
      12                 : #define BOOST_COROSIO_NATIVE_DETAIL_EPOLL_EPOLL_SCHEDULER_HPP
      13                 : 
      14                 : #include <boost/corosio/detail/platform.hpp>
      15                 : 
      16                 : #if BOOST_COROSIO_HAS_EPOLL
      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/epoll/epoll_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 <atomic>
      31                 : #include <chrono>
      32                 : #include <cstdint>
      33                 : #include <limits>
      34                 : #include <mutex>
      35                 : #include <vector>
      36                 : 
      37                 : #include <errno.h>
      38                 : #include <sys/epoll.h>
      39                 : #include <sys/eventfd.h>
      40                 : #include <sys/timerfd.h>
      41                 : #include <unistd.h>
      42                 : 
      43                 : namespace boost::corosio::detail {
      44                 : 
      45                 : /** Linux scheduler using epoll for I/O multiplexing.
      46                 : 
      47                 :     This scheduler implements the scheduler interface using Linux epoll
      48                 :     for efficient I/O event notification. It uses a single reactor model
      49                 :     where one thread runs epoll_wait while other threads
      50                 :     wait on a condition variable for handler work. This design provides:
      51                 : 
      52                 :     - Handler parallelism: N posted handlers can execute on N threads
      53                 :     - No thundering herd: condition_variable wakes exactly one thread
      54                 :     - IOCP parity: Behavior matches Windows I/O completion port semantics
      55                 : 
      56                 :     When threads call run(), they first try to execute queued handlers.
      57                 :     If the queue is empty and no reactor is running, one thread becomes
      58                 :     the reactor and runs epoll_wait. Other threads wait on a condition
      59                 :     variable until handlers are available.
      60                 : 
      61                 :     @par Thread Safety
      62                 :     All public member functions are thread-safe.
      63                 : */
      64                 : class BOOST_COROSIO_DECL epoll_scheduler final : public reactor_scheduler
      65                 : {
      66                 : public:
      67                 :     /** Construct the scheduler.
      68                 : 
      69                 :         Creates an epoll instance and an eventfd for reactor
      70                 :         interruption. The timerfd for timer expiry is created with the
      71                 :         context's first timer.
      72                 : 
      73                 :         @param ctx Reference to the owning execution_context.
      74                 :         @param concurrency_hint Hint for expected thread count (unused).
      75                 :     */
      76                 :     epoll_scheduler(capy::execution_context& ctx, int concurrency_hint = -1);
      77                 : 
      78                 :     /// Destroy the scheduler.
      79                 :     ~epoll_scheduler() override;
      80                 : 
      81                 :     epoll_scheduler(epoll_scheduler const&)            = delete;
      82                 :     epoll_scheduler& operator=(epoll_scheduler const&) = delete;
      83                 : 
      84                 :     /// Shut down the scheduler, draining pending operations.
      85                 :     void shutdown() override;
      86                 : 
      87                 :     /// Apply runtime configuration, resizing the event buffer.
      88                 :     void configure_reactor(
      89                 :         unsigned max_events,
      90                 :         unsigned budget_init,
      91                 :         unsigned budget_max,
      92                 :         unsigned unassisted) override;
      93                 : 
      94                 :     /** Return the epoll file descriptor.
      95                 : 
      96                 :         Used by socket services to register file descriptors
      97                 :         for I/O event notification.
      98                 : 
      99                 :         @return The epoll file descriptor.
     100                 :     */
     101                 :     int epoll_fd() const noexcept
     102                 :     {
     103                 :         return epoll_fd_;
     104                 :     }
     105                 : 
     106                 :     /** Register a descriptor for persistent monitoring.
     107                 : 
     108                 :         The fd is registered once and stays registered until explicitly
     109                 :         deregistered. Events are dispatched via reactor_descriptor_state which
     110                 :         tracks pending read/write/connect operations.
     111                 : 
     112                 :         @param fd The file descriptor to register.
     113                 :         @param desc Pointer to descriptor data (stored in epoll_event.data.ptr).
     114                 : 
     115                 :         @return The error if registration fails, otherwise a default
     116                 :         constructed error code.
     117                 :     */
     118                 :     std::error_code
     119                 :     register_descriptor(int fd, reactor_descriptor_state* desc) const;
     120                 : 
     121                 :     /// No-op: write readiness is watched from registration on.
     122                 :     std::error_code
     123 HIT        5401 :     ensure_write_registered(int, reactor_descriptor_state*) const noexcept
     124                 :     {
     125            5401 :         return {};
     126                 :     }
     127                 : 
     128                 :     /** Deregister a persistently registered descriptor.
     129                 : 
     130                 :         @param fd The file descriptor to deregister.
     131                 :     */
     132                 :     void deregister_descriptor(int fd) const;
     133                 : 
     134                 :     /// Watch the read end of the POSIX signal self-pipe (see scheduler.hpp).
     135             286 :     [[nodiscard]] std::error_code register_signal_reader(int read_fd) override
     136                 :     {
     137             286 :         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                 :     void update_timerfd() const;
     144                 :     static int timer_timeout_ms(timer_service& svc, int timeout_ms) noexcept;
     145                 :     void on_timer_service_attached(timer_service& svc) override;
     146                 : 
     147                 :     int epoll_fd_;
     148                 :     int event_fd_;
     149                 :     int timer_fd_;
     150                 : 
     151                 :     // Watches the global signal self-pipe's read end (armed lazily by
     152                 :     // register_signal_reader on the first signal registration).
     153                 :     reactor_signal_pipe_reader signal_pipe_reader_;
     154                 : 
     155                 :     // Edge-triggered eventfd state
     156                 :     mutable std::atomic<bool> eventfd_armed_{false};
     157                 : 
     158                 :     // Set when the earliest timer changes; flushed before epoll_wait
     159                 :     mutable std::atomic<bool> timerfd_stale_{false};
     160                 : 
     161                 :     // Event buffer sized from max_events_per_poll_ (set at construction,
     162                 :     // resized by configure_reactor via io_context_options).
     163                 :     std::vector<epoll_event> event_buffer_;
     164                 : };
     165                 : 
     166            2484 : inline epoll_scheduler::epoll_scheduler(capy::execution_context&, int)
     167            2484 :     : epoll_fd_(-1)
     168            2484 :     , event_fd_(-1)
     169            2484 :     , timer_fd_(-1)
     170            4968 :     , event_buffer_(max_events_per_poll_)
     171                 : {
     172            2484 :     epoll_fd_ = ::epoll_create1(EPOLL_CLOEXEC);
     173            2484 :     if (epoll_fd_ < 0)
     174               3 :         detail::throw_system_error(make_err(errno), "epoll_create1");
     175                 : 
     176            2481 :     event_fd_ = ::eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
     177            2481 :     if (event_fd_ < 0)
     178                 :     {
     179               3 :         int errn = errno;
     180               3 :         ::close(epoll_fd_);
     181               3 :         detail::throw_system_error(make_err(errn), "eventfd");
     182                 :     }
     183                 : 
     184            2478 :     epoll_event ev{};
     185            2478 :     ev.events   = EPOLLIN | EPOLLET;
     186            2478 :     ev.data.ptr = nullptr;
     187            2478 :     if (::epoll_ctl(epoll_fd_, EPOLL_CTL_ADD, event_fd_, &ev) < 0)
     188                 :     {
     189               3 :         int errn = errno;
     190               3 :         ::close(event_fd_);
     191               3 :         ::close(epoll_fd_);
     192               3 :         detail::throw_system_error(make_err(errn), "epoll_ctl");
     193                 :     }
     194                 : 
     195            2475 :     completed_ops_.push(&task_op_);
     196            2502 : }
     197                 : 
     198                 : inline void
     199             534 : epoll_scheduler::on_timer_service_attached(timer_service& svc)
     200                 : {
     201                 :     // Without a timerfd the epoll_wait timeout drives the timers: the
     202                 :     // first timer is often made where a throw can only terminate.
     203             534 :     int fd = ::timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
     204             534 :     if (fd >= 0)
     205                 :     {
     206                 :         // Set before registration, which orders it for every thread
     207                 :         // the timerfd's events can wake.
     208             533 :         timer_fd_ = fd;
     209             533 :         epoll_event timer_ev{};
     210             533 :         timer_ev.events   = EPOLLIN | EPOLLERR;
     211             533 :         timer_ev.data.ptr = &timer_fd_;
     212             533 :         if (::epoll_ctl(epoll_fd_, EPOLL_CTL_ADD, fd, &timer_ev) < 0)
     213                 :         {
     214               1 :             ::close(fd);
     215               1 :             timer_fd_ = -1;
     216                 :         }
     217                 :     }
     218             534 :     svc.set_on_earliest_changed(timer_service::callback(this, [](void* p) {
     219            4650 :         auto* self = static_cast<epoll_scheduler*>(p);
     220            4650 :         self->timerfd_stale_.store(true, std::memory_order_release);
     221            4650 :         self->interrupt_reactor();
     222            4650 :     }));
     223             534 : }
     224                 : 
     225            4950 : inline epoll_scheduler::~epoll_scheduler()
     226                 : {
     227            2475 :     if (timer_fd_ >= 0)
     228             532 :         ::close(timer_fd_);
     229            2475 :     if (event_fd_ >= 0)
     230            2475 :         ::close(event_fd_);
     231            2475 :     if (epoll_fd_ >= 0)
     232            2475 :         ::close(epoll_fd_);
     233            4950 : }
     234                 : 
     235                 : inline void
     236            2475 : epoll_scheduler::shutdown()
     237                 : {
     238            2475 :     shutdown_drain();
     239                 : 
     240            2475 :     if (event_fd_ >= 0)
     241            2475 :         interrupt_reactor();
     242            2475 : }
     243                 : 
     244                 : inline void
     245              32 : epoll_scheduler::configure_reactor(
     246                 :     unsigned max_events,
     247                 :     unsigned budget_init,
     248                 :     unsigned budget_max,
     249                 :     unsigned unassisted)
     250                 : {
     251              32 :     reactor_scheduler::configure_reactor(
     252                 :         max_events, budget_init, budget_max, unassisted);
     253              30 :     event_buffer_.resize(max_events_per_poll_);
     254              30 : }
     255                 : 
     256                 : inline std::error_code
     257           13287 : epoll_scheduler::register_descriptor(
     258                 :     int fd, reactor_descriptor_state* desc) const
     259                 : {
     260           13287 :     epoll_event ev{};
     261           13287 :     ev.events   = EPOLLIN | EPOLLOUT | EPOLLET | EPOLLERR | EPOLLHUP;
     262           13287 :     ev.data.ptr = desc;
     263                 : 
     264                 :     // Initialized before the add: another run thread's reactor can
     265                 :     // dispatch an event for the descriptor as soon as it is added.
     266                 :     // A completion queued by an earlier registration of the same state
     267                 :     // may still be running, so the fields it reads change under its
     268                 :     // mutex, and the mutex mode is only written when it differs.
     269           13287 :     if (desc->mutex.enabled() != reactor_io_locking_)
     270               3 :         desc->mutex.set_enabled(reactor_io_locking_);
     271                 :     {
     272           13287 :         conditionally_enabled_mutex::scoped_lock lock(desc->mutex);
     273           13287 :         desc->registered_events = ev.events;
     274           13287 :         desc->unpollable        = false;
     275           13287 :         desc->fd                = fd;
     276           13287 :         desc->scheduler_        = this;
     277           13287 :         desc->ready_events_.store(0, std::memory_order_release);
     278                 :         // object_ref_ is not touched: a queued invocation from an
     279                 :         // earlier registration still owns the reference in it.
     280           13287 :         desc->read_ready  = false;
     281           13287 :         desc->write_ready = false;
     282           13287 :     }
     283                 : 
     284           13287 :     if (::epoll_ctl(epoll_fd_, EPOLL_CTL_ADD, fd, &ev) < 0)
     285                 :     {
     286              12 :         int const err = errno;
     287              12 :         conditionally_enabled_mutex::scoped_lock lock(desc->mutex);
     288              12 :         desc->registered_events = 0;
     289                 :         // EPERM: a file type epoll cannot watch. Its I/O does not
     290                 :         // block, so adopt it unwatched, as asio does.
     291              12 :         if (err != EPERM)
     292               7 :             return make_err(err);
     293               5 :         desc->unpollable = true;
     294              12 :     }
     295           13280 :     return {};
     296                 : }
     297                 : 
     298                 : inline void
     299           12990 : epoll_scheduler::deregister_descriptor(int fd) const
     300                 : {
     301           12990 :     ::epoll_ctl(epoll_fd_, EPOLL_CTL_DEL, fd, nullptr);
     302           12990 : }
     303                 : 
     304                 : inline void
     305           12879 : epoll_scheduler::interrupt_reactor() const
     306                 : {
     307           12879 :     bool expected = false;
     308           12879 :     if (eventfd_armed_.compare_exchange_strong(
     309                 :             expected, true, std::memory_order_release,
     310                 :             std::memory_order_relaxed))
     311                 :     {
     312           10523 :         std::uint64_t val = 1;
     313           10523 :         if (::write(event_fd_, &val, sizeof(val)) < 0)
     314                 :         {
     315                 :             // The flag is what coalesces later interrupts into a byte
     316                 :             // already in the eventfd; a write that failed put no byte
     317                 :             // there, so leaving it armed would swallow every interrupt
     318                 :             // that follows. Disarming keeps the cost to the interrupts
     319                 :             // already in flight -- the next one arms and writes again,
     320                 :             // instead of every one after this coalescing into a byte
     321                 :             // that does not exist.
     322               2 :             eventfd_armed_.store(false, std::memory_order_release);
     323                 :         }
     324                 :     }
     325           12879 : }
     326                 : 
     327                 : inline void
     328           11568 : epoll_scheduler::update_timerfd() const
     329                 : {
     330           11568 :     auto* tsvc = timers();
     331           11568 :     if (!tsvc || timer_fd_ < 0)
     332               6 :         return;
     333           11562 :     auto nearest = tsvc->nearest_expiry();
     334                 : 
     335           11562 :     itimerspec ts{};
     336           11562 :     int flags = 0;
     337                 : 
     338           11562 :     if (nearest == timer_service::time_point::max())
     339                 :     {
     340                 :         // No timers — disarm by setting to 0 (relative)
     341                 :     }
     342                 :     else
     343                 :     {
     344           10281 :         auto now = std::chrono::steady_clock::now();
     345           10281 :         if (nearest <= now)
     346                 :         {
     347                 :             // Use 1ns instead of 0 — zero disarms the timerfd
     348            1109 :             ts.it_value.tv_nsec = 1;
     349                 :         }
     350                 :         else
     351                 :         {
     352            9172 :             auto nsec = std::chrono::duration_cast<std::chrono::nanoseconds>(
     353            9172 :                             nearest - now)
     354            9172 :                             .count();
     355            9172 :             ts.it_value.tv_sec  = nsec / 1000000000;
     356            9172 :             ts.it_value.tv_nsec = nsec % 1000000000;
     357            9172 :             if (ts.it_value.tv_sec == 0 && ts.it_value.tv_nsec == 0)
     358 MIS           0 :                 ts.it_value.tv_nsec = 1;
     359                 :         }
     360                 :     }
     361                 : 
     362 HIT       11562 :     if (::timerfd_settime(timer_fd_, flags, &ts, nullptr) < 0)
     363               1 :         detail::throw_system_error(make_err(errno), "timerfd_settime");
     364                 : }
     365                 : 
     366                 : // Rounded up, so the wait never wakes before the timer is due.
     367                 : inline int
     368               4 : epoll_scheduler::timer_timeout_ms(timer_service& svc, int timeout_ms) noexcept
     369                 : {
     370               4 :     auto nearest = svc.nearest_expiry();
     371               4 :     if (nearest == timer_service::time_point::max())
     372 MIS           0 :         return timeout_ms;
     373 HIT           4 :     auto now = std::chrono::steady_clock::now();
     374               4 :     if (nearest <= now)
     375 MIS           0 :         return 0;
     376                 :     auto ms =
     377 HIT           4 :         std::chrono::ceil<std::chrono::milliseconds>(nearest - now).count();
     378               8 :     ms = (std::min)(ms,
     379               4 :                     static_cast<decltype(ms)>(
     380               4 :                         (std::numeric_limits<int>::max)()));
     381               4 :     if (timeout_ms >= 0 && timeout_ms < ms)
     382 MIS           0 :         return timeout_ms;
     383 HIT           4 :     return static_cast<int>(ms);
     384                 : }
     385                 : 
     386                 : inline void
     387          148711 : epoll_scheduler::run_task(lock_type& lock, context_type& ctx, long timeout_us)
     388                 : {
     389                 :     int timeout_ms;
     390          148711 :     if (task_interrupted_)
     391          128721 :         timeout_ms = 0;
     392           19990 :     else if (timeout_us < 0)
     393           19613 :         timeout_ms = -1;
     394                 :     else
     395             377 :         timeout_ms = static_cast<int>((timeout_us + 999) / 1000);
     396                 : 
     397          148711 :     if (lock.owns_lock())
     398           19994 :         lock.unlock();
     399                 : 
     400          148711 :     task_cleanup on_exit{this, &lock, ctx};
     401                 : 
     402                 :     // Flush deferred timerfd programming before blocking
     403          148711 :     if (timerfd_stale_.exchange(false, std::memory_order_acquire))
     404            3797 :         update_timerfd();
     405                 : 
     406                 :     // timer_fd_ is read only once the service is seen, after which it
     407                 :     // no longer changes.
     408          148710 :     auto* tsvc             = timers();
     409          148710 :     bool const poll_timers = tsvc && timer_fd_ < 0;
     410          148710 :     if (poll_timers && timeout_ms != 0)
     411               4 :         timeout_ms = timer_timeout_ms(*tsvc, timeout_ms);
     412                 : 
     413          148710 :     int nfds = ::epoll_wait(
     414          148710 :         epoll_fd_, event_buffer_.data(), static_cast<int>(event_buffer_.size()),
     415                 :         timeout_ms);
     416                 : 
     417          148710 :     if (nfds < 0 && errno != EINTR)
     418               1 :         detail::throw_system_error(make_err(errno), "epoll_wait");
     419                 : 
     420          148709 :     bool check_timers = poll_timers;
     421          148709 :     ready_queue local_ops;
     422                 : 
     423          210020 :     for (int i = 0; i < nfds; ++i)
     424                 :     {
     425           61311 :         if (event_buffer_[i].data.ptr == nullptr)
     426                 :         {
     427                 :             std::uint64_t val;
     428                 :             // NOLINTNEXTLINE(clang-analyzer-unix.BlockInCriticalSection)
     429            8046 :             [[maybe_unused]] auto r = ::read(event_fd_, &val, sizeof(val));
     430            8046 :             eventfd_armed_.store(false, std::memory_order_relaxed);
     431            8046 :             continue;
     432            8046 :         }
     433                 : 
     434           53265 :         if (event_buffer_[i].data.ptr == &timer_fd_)
     435                 :         {
     436                 :             std::uint64_t expirations;
     437                 :             // NOLINTNEXTLINE(clang-analyzer-unix.BlockInCriticalSection)
     438                 :             [[maybe_unused]] auto r =
     439            7767 :                 ::read(timer_fd_, &expirations, sizeof(expirations));
     440            7767 :             check_timers = true;
     441            7767 :             continue;
     442            7767 :         }
     443                 : 
     444                 :         auto* desc =
     445           45498 :             static_cast<reactor_descriptor_state*>(event_buffer_[i].data.ptr);
     446                 : 
     447                 :         // EPOLLHUP maps to no reactor_event_* bit, so a HUP the widening
     448                 :         // below did not translate would take no branch in
     449                 :         // invoke_deferred_io() and, the registration being
     450                 :         // edge-triggered, never get another chance.
     451                 :         //
     452                 :         // HUP without OUT means a non-socket: a pipe or tty whose peer
     453                 :         // closed, with or without data still unread (HUP alone, or
     454                 :         // IN|HUP). Treat it as readable, writable and faulted, the way
     455                 :         // poll() and io_uring report it, so a parked error wait
     456                 :         // completes too. Sockets carry at least OUT with HUP (a fresh
     457                 :         // unconnected stream socket reports HUP|OUT), so they take the
     458                 :         // plain widening, where forcing IN costs at most one spurious
     459                 :         // EAGAIN.
     460           45498 :         std::uint32_t ev = event_buffer_[i].events;
     461           45498 :         if ((ev & (EPOLLHUP | EPOLLOUT)) == EPOLLHUP)
     462               8 :             ev |= EPOLLIN | EPOLLOUT | EPOLLERR;
     463           45490 :         else if (ev & EPOLLHUP)
     464            2963 :             ev |= EPOLLIN | EPOLLOUT;
     465           45498 :         desc->add_ready_events(ev);
     466                 : 
     467           45498 :         bool expected = false;
     468           45498 :         if (desc->is_enqueued_.compare_exchange_strong(
     469                 :                 expected, true, std::memory_order_release,
     470                 :                 std::memory_order_relaxed))
     471                 :         {
     472           45476 :             local_ops.push(desc);
     473                 :         }
     474                 :     }
     475                 : 
     476          148709 :     if (check_timers)
     477                 :     {
     478            7771 :         if (auto* svc = timers())
     479            7771 :             svc->process_expired();
     480            7771 :         update_timerfd();
     481                 :     }
     482                 : 
     483          148709 :     lock.lock();
     484                 : 
     485          148709 :     completed_ops_.splice(local_ops);
     486          148711 : }
     487                 : 
     488                 : } // namespace boost::corosio::detail
     489                 : 
     490                 : #endif // BOOST_COROSIO_HAS_EPOLL
     491                 : 
     492                 : #endif // BOOST_COROSIO_NATIVE_DETAIL_EPOLL_EPOLL_SCHEDULER_HPP
        

Generated by: LCOV version 2.3