From: Razvan Becheriu Date: Tue, 4 Nov 2025 14:29:25 +0000 (+0200) Subject: [#4141] implemented epoll event handler X-Git-Tag: Kea-3.1.4~29 X-Git-Url: http://git.ipfire.org/gitweb/?a=commitdiff_plain;h=0f478fe2bba2db1ba8d50249d3e8ed6822857779;p=thirdparty%2Fkea.git [#4141] implemented epoll event handler --- diff --git a/src/lib/dhcp/testutils/pkt_filter6_test_stub.cc b/src/lib/dhcp/testutils/pkt_filter6_test_stub.cc index 0a2cb4b742..2dd0af5c70 100644 --- a/src/lib/dhcp/testutils/pkt_filter6_test_stub.cc +++ b/src/lib/dhcp/testutils/pkt_filter6_test_stub.cc @@ -20,11 +20,29 @@ SocketInfo PktFilter6TestStub::openSocket(const Iface&, const isc::asiolink::IOAddress& addr, const uint16_t port, const bool) { - if (open_socket_callback_) { - open_socket_callback_(port); + int pipefd[2]; + + int ret = pipe(pipefd); + if (ret < 0) { + const char* errmsg = strerror(errno); + isc_throw(Unexpected, + "PktFilter6TestStub: cannot open pipe: " << errmsg); + } + + try { + if (open_socket_callback_) { + open_socket_callback_(port); + } + } catch (...) { + // Don't leak fd on simulated errors. + close(pipefd[0]); + close(pipefd[1]); + throw; } - return (SocketInfo(addr, port, 0)); + close(pipefd[1]); + + return (SocketInfo(addr, port, pipefd[0])); } Pkt6Ptr diff --git a/src/lib/dhcp/testutils/pkt_filter_test_stub.cc b/src/lib/dhcp/testutils/pkt_filter_test_stub.cc index 0c6d67bdd0..5accf92bd6 100644 --- a/src/lib/dhcp/testutils/pkt_filter_test_stub.cc +++ b/src/lib/dhcp/testutils/pkt_filter_test_stub.cc @@ -37,24 +37,29 @@ SocketInfo PktFilterTestStub::openSocket(Iface&, const isc::asiolink::IOAddress& addr, const uint16_t port, const bool, const bool) { - int fd = open("/dev/null", O_RDONLY); - if (fd < 0) { + int pipefd[2]; + + int ret = pipe(pipefd); + if (ret < 0) { const char* errmsg = strerror(errno); isc_throw(Unexpected, - "PktFilterTestStub: cannot open /dev/null:" << errmsg); + "PktFilterTestStub: cannot open pipe: " << errmsg); } try { if (open_socket_callback_) { open_socket_callback_(port); } - } catch(...) { + } catch (...) { // Don't leak fd on simulated errors. - close(fd); + close(pipefd[0]); + close(pipefd[1]); throw; } - return (SocketInfo(addr, port, fd)); + close(pipefd[1]); + + return (SocketInfo(addr, port, pipefd[0])); } Pkt4Ptr diff --git a/src/lib/util/epoll_event_handler.cc b/src/lib/util/epoll_event_handler.cc new file mode 100644 index 0000000000..ce128062b7 --- /dev/null +++ b/src/lib/util/epoll_event_handler.cc @@ -0,0 +1,138 @@ +// Copyright (C) 2025 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include + +#include +#include + +#include + +using namespace std; + +namespace isc { +namespace util { + +EPollEventHandler::EPollEventHandler() : FDEventHandler(TYPE_EPOLL), epollfd_(-1) { + epollfd_ = epoll_create1(EPOLL_CLOEXEC); + if (epollfd_ == -1) { + isc_throw(Unexpected, "error opening epoll: " << strerror(errno)); + } + if (pipe(pipefd)) { + close(epollfd_); + isc_throw(Unexpected, "error opening internal epoll pipe: " << strerror(errno)); + } + clear(); +} + +EPollEventHandler::~EPollEventHandler() { + close(epollfd_); + close(pipefd[1]); + close(pipefd[0]); +} + +void EPollEventHandler::add(int fd, bool read /* = true */, bool write /* = false */) { + if (fd < 0) { + isc_throw(BadValue, "invalid negative value for fd"); + } + struct epoll_event data; + memset(&data, 0, sizeof(data)); + data.data.fd = fd; + if (read) { + // Add this socket to read events + data.events |= EPOLLIN; + } + if (write) { + // Add this socket to write events + data.events |= EPOLLOUT; + } + data_.push_back(data); +} + +int EPollEventHandler::waitEvent(uint32_t timeout_sec, uint32_t timeout_usec /* = 0 */, + bool use_timeout /* = true */) { + // Sanity check for microsecond timeout. + if (timeout_usec >= 1000000) { + isc_throw(BadValue, "fractional timeout must be shorter than" + " one million microseconds"); + } + int timeout = -1; + if (use_timeout) { + timeout = timeout_sec * 1000 + timeout_usec / 1000; + } + map_.clear(); + used_data_.clear(); + errors_.clear(); + errno = 0; + int saved_errno = errno; + for (auto data : data_) { + if (epoll_ctl(epollfd_, EPOLL_CTL_ADD, data.data.fd, &data) == -1) { + saved_errno = errno; + errors_.insert(data.data.fd); + } else { + used_data_.push_back(data); + } + } + struct epoll_event dummy; + memset(&dummy, 0, sizeof(dummy)); + dummy.data.fd = pipefd[0]; + dummy.events |= EPOLLIN; + epoll_ctl(epollfd_, EPOLL_CTL_ADD, dummy.data.fd, &dummy); + used_data_.push_back(dummy); + int result = 0; + if (errors_.empty()) { + result = epoll_wait(epollfd_, used_data_.data(), used_data_.size(), timeout); + for (int i = 0; i < result; ++i) { + map_[used_data_[i].data.fd] = &used_data_[i]; + } + } + for (auto data : data_) { + epoll_ctl(epollfd_, EPOLL_CTL_DEL, data.data.fd, &data); + } + epoll_ctl(epollfd_, EPOLL_CTL_DEL, dummy.data.fd, &dummy); + if (result != -1) { + errno = saved_errno; + } + if (errors_.size()) { + return (errors_.size()); + } + return (result); +} + +bool EPollEventHandler::readReady(int fd) { + if (map_.find(fd) == map_.end()) { + return (false); + } + return (map_[fd]->events & EPOLLIN); +} + +bool EPollEventHandler::writeReady(int fd) { + if (map_.find(fd) == map_.end()) { + return (false); + } + return (map_[fd]->events & EPOLLOUT); +} + +bool EPollEventHandler::hasError(int fd) { + if (errors_.count(fd)) { + return (true); + } + if (map_.find(fd) == map_.end()) { + return (false); + } + return (map_[fd]->events & (EPOLLRDHUP | EPOLLERR | EPOLLHUP)); +} + +void EPollEventHandler::clear() { + data_.clear(); + used_data_.clear(); + errors_.clear(); + map_.clear(); + errno = 0; +} + +} // end of namespace isc::util +} // end of namespace isc diff --git a/src/lib/util/epoll_event_handler.h b/src/lib/util/epoll_event_handler.h new file mode 100644 index 0000000000..fdb96bc830 --- /dev/null +++ b/src/lib/util/epoll_event_handler.h @@ -0,0 +1,99 @@ +// Copyright (C) 2025 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef EPOLL_EVENT_HANDLER_H +#define EPOLL_EVENT_HANDLER_H + +#include + +#include +#include +#include + +#include + +namespace isc { +namespace util { + +/// @brief File descriptor event handler class handles events for registered +/// file descriptors. This class uses the OS select syscall for event handling. +class EPollEventHandler : public FDEventHandler { +public: + /// @brief Constructor. + EPollEventHandler(); + + /// @brief Destructor. + virtual ~EPollEventHandler(); + + /// @brief Add file descriptor to watch for events. + /// + /// @param fd The file descriptor. + /// @param read The flag indicating if the file descriptor should be + /// registered for read ready events. + /// @param write The flag indicating if the file descriptor should be + /// registered for write ready events. + void add(int fd, bool read = true, bool write = false); + + /// @brief Wait for events on registered file descriptors. + /// + /// @param timeout_sec The wait timeout in seconds. + /// @param timeout_usec The wait timeout in micro seconds. + /// @param use_timeout Flag which indicates if function should wait + /// with no timeout (wait forever). + /// @return -1 on error, 0 if no data is available (timeout expired), + /// 1 if data is ready. + int waitEvent(uint32_t timeout_sec, uint32_t timeout_usec = 0, + bool use_timeout = true); + + /// @brief Check if file descriptor is ready for read operation. + /// + /// @param fd The file descriptor. + /// + /// @return True if file descriptor is ready for reading. + bool readReady(int fd); + + /// @brief Check if file descriptor is ready for write operation. + /// + /// @param fd The file descriptor. + /// + /// @return True if file descriptor is ready for writing. + bool writeReady(int fd); + + /// @brief Check if file descriptor has error. + /// + /// @param fd The file descriptor. + /// + /// @return True if file descriptor has error. + virtual bool hasError(int fd); + + /// @brief Clear registered file descriptors. + void clear(); + +private: + /// @brief The epoll file descriptor. + int epollfd_; + + /// @brief The epoll file descriptors data. + std::vector data_; + + /// @brief The epoll file descriptors data. + std::vector used_data_; + + /// @brief The set of file descriptors with errors. + std::unordered_set errors_; + + /// @brief The map with file descriptor to data reference. + std::unordered_map map_; + + /// @brief The pipe used to permit calling @ref waitEvent with no + /// registered file descriptors. + int pipefd[2]; +}; + +} // namespace isc::util; +} // namespace isc + +#endif // EPOLL_EVENT_HANDLER_H diff --git a/src/lib/util/fd_event_handler_factory.cc b/src/lib/util/fd_event_handler_factory.cc index 2997c44f74..ba63624296 100644 --- a/src/lib/util/fd_event_handler_factory.cc +++ b/src/lib/util/fd_event_handler_factory.cc @@ -6,6 +6,7 @@ #include +#include #include #include #include @@ -28,10 +29,15 @@ FDEventHandlerPtr FDEventHandlerFactory::factoryFDEventHandler() { if (string(env_type) == string("poll")) { type = FDEventHandler::TYPE_POLL; } + if (string(env_type) == string("epoll")) { + type = FDEventHandler::TYPE_EPOLL; + } } switch(type) { case FDEventHandler::TYPE_SELECT: return (FDEventHandlerPtr(new SelectEventHandler())); + case FDEventHandler::TYPE_EPOLL: + return (FDEventHandlerPtr(new EPollEventHandler())); case FDEventHandler::TYPE_POLL: return (FDEventHandlerPtr(new PollEventHandler())); default: diff --git a/src/lib/util/meson.build b/src/lib/util/meson.build index 8131a7f13f..1e7196e0f2 100644 --- a/src/lib/util/meson.build +++ b/src/lib/util/meson.build @@ -6,6 +6,7 @@ kea_util_lib = shared_library( 'dhcp_space.cc', 'encode/encode.cc', 'encode/utf8.cc', + 'epoll_event_handler.cc', 'fd_event_handler.cc', 'fd_event_handler_factory.cc', 'filesystem.cc', @@ -47,6 +48,7 @@ kea_util_headers = [ 'doubles.h', 'encode/encode.h', 'encode/utf8.h', + 'epoll_event_handler.h', 'fd_event_handler.h', 'fd_event_handler_factory.h', 'filesystem.h', diff --git a/src/lib/util/poll_event_handler.h b/src/lib/util/poll_event_handler.h index 9332843613..f102a0fc09 100644 --- a/src/lib/util/poll_event_handler.h +++ b/src/lib/util/poll_event_handler.h @@ -10,6 +10,7 @@ #include #include +#include #include #include diff --git a/src/lib/util/tests/epoll_event_handler_unittests.cc b/src/lib/util/tests/epoll_event_handler_unittests.cc new file mode 100644 index 0000000000..05173c9022 --- /dev/null +++ b/src/lib/util/tests/epoll_event_handler_unittests.cc @@ -0,0 +1,12 @@ +// Copyright (C) 2025 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include + +#define FDEventHandlerType EPollEventHandler +#define FDEventHandlerTest EPollEventHandlerTest + +#include diff --git a/src/lib/util/tests/fd_event_handler_unittests.h b/src/lib/util/tests/fd_event_handler_unittests.h index 5037186e05..5b5367b781 100644 --- a/src/lib/util/tests/fd_event_handler_unittests.h +++ b/src/lib/util/tests/fd_event_handler_unittests.h @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -35,20 +36,20 @@ public: /// @brief Constructor. FDEventHandlerTest() { handler_.reset(new FDEventHandlerType); - pipe(pipefd); + pipe(pipefd_); } /// @brief Destructor. ~FDEventHandlerTest() { - close(pipefd[1]); - close(pipefd[0]); + close(pipefd_[1]); + close(pipefd_[0]); } /// @brief The tested fd event handler. FDEventHandlerPtr handler_; /// @brief The pipe used for testing read and write operations. - int pipefd[2]; + int pipefd_[2]; }; TEST_F(FDEventHandlerTest, events) { @@ -58,58 +59,58 @@ TEST_F(FDEventHandlerTest, events) { EXPECT_THROW(handler_->add(-1), BadValue); - EXPECT_NO_THROW(handler_->add(pipefd[0], true, false)); - EXPECT_NO_THROW(handler_->add(pipefd[1], false, true)); + EXPECT_NO_THROW(handler_->add(pipefd_[0], true, false)); + EXPECT_NO_THROW(handler_->add(pipefd_[1], false, true)); - EXPECT_FALSE(handler_->readReady(pipefd[0])); - EXPECT_FALSE(handler_->writeReady(pipefd[0])); - EXPECT_FALSE(handler_->readReady(pipefd[1])); - EXPECT_FALSE(handler_->writeReady(pipefd[1])); + EXPECT_FALSE(handler_->readReady(pipefd_[0])); + EXPECT_FALSE(handler_->writeReady(pipefd_[0])); + EXPECT_FALSE(handler_->readReady(pipefd_[1])); + EXPECT_FALSE(handler_->writeReady(pipefd_[1])); EXPECT_EQ(1, handler_->waitEvent(0, 1000)); - EXPECT_FALSE(handler_->readReady(pipefd[0])); - EXPECT_FALSE(handler_->writeReady(pipefd[0])); - EXPECT_FALSE(handler_->readReady(pipefd[1])); - EXPECT_TRUE(handler_->writeReady(pipefd[1])); + EXPECT_FALSE(handler_->readReady(pipefd_[0])); + EXPECT_FALSE(handler_->writeReady(pipefd_[0])); + EXPECT_FALSE(handler_->readReady(pipefd_[1])); + EXPECT_TRUE(handler_->writeReady(pipefd_[1])); - EXPECT_EQ(1, write(pipefd[1], &MARKER, sizeof(MARKER))); + EXPECT_EQ(1, write(pipefd_[1], &MARKER, sizeof(MARKER))); EXPECT_EQ(2, handler_->waitEvent(0, 1000)); - EXPECT_TRUE(handler_->readReady(pipefd[0])); - EXPECT_FALSE(handler_->writeReady(pipefd[0])); - EXPECT_FALSE(handler_->readReady(pipefd[1])); - EXPECT_TRUE(handler_->writeReady(pipefd[1])); + EXPECT_TRUE(handler_->readReady(pipefd_[0])); + EXPECT_FALSE(handler_->writeReady(pipefd_[0])); + EXPECT_FALSE(handler_->readReady(pipefd_[1])); + EXPECT_TRUE(handler_->writeReady(pipefd_[1])); - EXPECT_EQ(1, write(pipefd[1], &MARKER, sizeof(MARKER))); + EXPECT_EQ(1, write(pipefd_[1], &MARKER, sizeof(MARKER))); EXPECT_EQ(2, handler_->waitEvent(0, 1000)); - EXPECT_TRUE(handler_->readReady(pipefd[0])); - EXPECT_FALSE(handler_->writeReady(pipefd[0])); - EXPECT_FALSE(handler_->readReady(pipefd[1])); - EXPECT_TRUE(handler_->writeReady(pipefd[1])); + EXPECT_TRUE(handler_->readReady(pipefd_[0])); + EXPECT_FALSE(handler_->writeReady(pipefd_[0])); + EXPECT_FALSE(handler_->readReady(pipefd_[1])); + EXPECT_TRUE(handler_->writeReady(pipefd_[1])); unsigned char data; - EXPECT_EQ(1, read(pipefd[0], &data, sizeof(data))); + EXPECT_EQ(1, read(pipefd_[0], &data, sizeof(data))); EXPECT_EQ(2, handler_->waitEvent(0, 1000)); - EXPECT_TRUE(handler_->readReady(pipefd[0])); - EXPECT_FALSE(handler_->writeReady(pipefd[0])); - EXPECT_FALSE(handler_->readReady(pipefd[1])); - EXPECT_TRUE(handler_->writeReady(pipefd[1])); + EXPECT_TRUE(handler_->readReady(pipefd_[0])); + EXPECT_FALSE(handler_->writeReady(pipefd_[0])); + EXPECT_FALSE(handler_->readReady(pipefd_[1])); + EXPECT_TRUE(handler_->writeReady(pipefd_[1])); - EXPECT_EQ(1, read(pipefd[0], &data, sizeof(data))); + EXPECT_EQ(1, read(pipefd_[0], &data, sizeof(data))); EXPECT_EQ(1, handler_->waitEvent(0, 1000)); - EXPECT_FALSE(handler_->readReady(pipefd[0])); - EXPECT_FALSE(handler_->writeReady(pipefd[0])); - EXPECT_FALSE(handler_->readReady(pipefd[1])); - EXPECT_TRUE(handler_->writeReady(pipefd[1])); + EXPECT_FALSE(handler_->readReady(pipefd_[0])); + EXPECT_FALSE(handler_->writeReady(pipefd_[0])); + EXPECT_FALSE(handler_->readReady(pipefd_[1])); + EXPECT_TRUE(handler_->writeReady(pipefd_[1])); EXPECT_NO_THROW(handler_->clear()); @@ -143,7 +144,16 @@ TEST_F(FDEventHandlerTest, events) { } TEST_F(FDEventHandlerTest, badFD) { - int fd = open("/dev/zero", O_RDONLY, 0); + errno = 0; + int fd; + + if (handler_->type() == FDEventHandler::TYPE_EPOLL) { + // epoll does not allow add of /dev/zero to registered events. + fd = pipefd_[0]; + EXPECT_EQ(1, write(pipefd_[1], &MARKER, sizeof(MARKER))); + } else { + fd = open("/dev/zero", O_RDONLY, 0); + } ASSERT_GE(fd, 0); @@ -171,11 +181,21 @@ TEST_F(FDEventHandlerTest, badFD) { EXPECT_TRUE(handler_->readReady(fd)); EXPECT_FALSE(handler_->hasError(fd)); EXPECT_EQ(EBADF, errno); - } else { + } else if (handler_->type() == FDEventHandler::TYPE_POLL) { EXPECT_EQ(1, handler_->waitEvent(0, 1000)); EXPECT_FALSE(handler_->readReady(fd)); EXPECT_TRUE(handler_->hasError(fd)); EXPECT_EQ(0, errno); + } else { + EXPECT_EQ(1, handler_->waitEvent(0, 1000)); + EXPECT_FALSE(handler_->readReady(fd)); + EXPECT_TRUE(handler_->hasError(fd)); + EXPECT_EQ(EBADF, errno); + } + + if (handler_->type() == FDEventHandler::TYPE_EPOLL) { + close(pipefd_[1]); + pipe(pipefd_); } } diff --git a/src/lib/util/tests/meson.build b/src/lib/util/tests/meson.build index 964dafc945..8e48a3a5ac 100644 --- a/src/lib/util/tests/meson.build +++ b/src/lib/util/tests/meson.build @@ -12,6 +12,7 @@ kea_util_tests = executable( 'dhcp_space_unittest.cc', 'doubles_unittest.cc', 'encode_unittest.cc', + 'epoll_event_handler_unittests.cc', 'fd_event_handler_factory_unittests.cc', 'fd_tests.cc', 'filesystem_unittests.cc', diff --git a/src/lib/util/tests/watch_socket_unittests.cc b/src/lib/util/tests/watch_socket_unittests.cc index 375252d574..4346d738c6 100644 --- a/src/lib/util/tests/watch_socket_unittests.cc +++ b/src/lib/util/tests/watch_socket_unittests.cc @@ -119,6 +119,12 @@ TEST(WatchSocketTest, closedWhileReady) { EXPECT_EQ(1, selectCheck(select_fd)); EXPECT_TRUE(watch->isReady()); + // The epoll event handler must be created before closing the socket. + // It creates an internal pipe which will match the closed fd and the + // check for bad file descriptor will fail. + FDEventHandlerPtr handler = FDEventHandlerFactory::factoryFDEventHandler(); + bool use_select = FDEventHandlerFactory::factoryFDEventHandler()->type() == FDEventHandler::TYPE_SELECT; + // Interfere by closing the fd. ASSERT_EQ(0, close(select_fd)); @@ -132,13 +138,12 @@ TEST(WatchSocketTest, closedWhileReady) { ASSERT_NO_THROW(watch->clearReady()); // Verify the select_fd fails as socket is invalid/closed. - if (FDEventHandlerFactory::factoryFDEventHandler()->type() == FDEventHandler::TYPE_SELECT) { - EXPECT_EQ(-1, selectCheck(select_fd)); + if (use_select) { + ASSERT_EQ(-1, selectCheck(select_fd)); } else { - FDEventHandlerPtr handler = FDEventHandlerFactory::factoryFDEventHandler(); handler->add(select_fd); EXPECT_EQ(1, handler->waitEvent(0, 0)); - EXPECT_TRUE(handler->hasError(select_fd)); + ASSERT_TRUE(handler->hasError(select_fd)); } // Verify that subsequent attempts to mark it will fail. @@ -198,6 +203,12 @@ TEST(WatchSocketTest, badReadOnClear) { EXPECT_TRUE(watch->isReady()); EXPECT_EQ(1, selectCheck(select_fd)); + // The epoll event handler must be created before closing the socket. + // It creates an internal pipe which will match the closed fd and the + // check for bad file descriptor will fail. + FDEventHandlerPtr handler = FDEventHandlerFactory::factoryFDEventHandler(); + bool use_select = FDEventHandlerFactory::factoryFDEventHandler()->type() == FDEventHandler::TYPE_SELECT; + // Interfere by reading the fd. This should empty the read pipe. uint32_t buf = 0; ASSERT_EQ((read (select_fd, &buf, 1)), 1); @@ -210,10 +221,9 @@ TEST(WatchSocketTest, badReadOnClear) { // Verify the select_fd does not evaluate to ready. EXPECT_FALSE(watch->isReady()); - if (FDEventHandlerFactory::factoryFDEventHandler()->type() == FDEventHandler::TYPE_SELECT) { + if (use_select) { EXPECT_EQ(-1, selectCheck(select_fd)); } else { - FDEventHandlerPtr handler = FDEventHandlerFactory::factoryFDEventHandler(); handler->add(select_fd); EXPECT_EQ(1, handler->waitEvent(0, 0)); EXPECT_TRUE(handler->hasError(select_fd));