- 
                Notifications
    You must be signed in to change notification settings 
- Fork 7
Add kqueue context #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had a brief look (I haven't looked at the actual kqueue logic).
| Thank you so much for your quick response and guidance. Will update and
work on condiction compilation this weekend.
Best Regards,
Xiangxin Zheng
Dietmar Kühl ***@***.***> 于2025年4月24日周四 07:58写道:…  ***@***.**** commented on this pull request.
 I had a brief look (I haven't looked at the actual kqueue logic).
 ------------------------------
 In include/beman/net/detail/basic_socket_acceptor.hpp
 <#22 (comment)>:
 > @@ -4,6 +4,7 @@
  #ifndef INCLUDED_BEMAN_NET_DETAIL_BASIC_SOCKET_ACCEPTOR
  #define INCLUDED_BEMAN_NET_DETAIL_BASIC_SOCKET_ACCEPTOR
 +#include "beman/net/detail/socket_base.hpp"
 I think the #include directives should consistently use <file>.
 ⬇️ Suggested change
 -#include "beman/net/detail/socket_base.hpp"
 +#include <beman/net/detail/socket_base.hpp>
 ------------------------------
 In include/beman/net/detail/io_context.hpp
 <#22 (comment)>:
 > @@ -6,6 +6,7 @@
  // ----------------------------------------------------------------------------
 +#include "beman/net/detail/kqueue_context.hpp"
 ⬇️ Suggested change
 -#include "beman/net/detail/kqueue_context.hpp"
 +#include <beman/net/detail/kqueue_context.hpp>
 ------------------------------
 In include/beman/net/detail/io_context.hpp
 <#22 (comment)>:
 > @@ -30,7 +31,8 @@ class io_context;
  class beman::net::io_context {
    private:
      ::std::unique_ptr<::beman::net::detail::context_base> d_owned{new ::beman::net::detail::poll_context()};
 -    ::beman::net::detail::context_base&                   d_context{*this->d_owned};
 +    // ::std::unique_ptr<::beman::net::detail::context_base> d_owned{new ::beman::net::detail::kqueue_context()};
 I'd think this would ask for conditional compilation: when compiled on a
 system supporting kqueue the default implementation should use just that.
 The interesting question may be how to determine if kqueue is support but
 I think this can be done by detecting this feature in the CMakeLists.txt.
 I do something like that here
 <https://github.com/dietmarkuehl/kuhllib/blob/482ddc2b910870398a9a2bcaa0a77a145e081f78/src/toy/CMakeLists.txt#L74>
 for my "toy" implementation.
 ------------------------------
 In src/beman/net/net.cpp
 <#22 (comment)>:
 > @@ -2,6 +2,7 @@
  // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
  // ----------------------------------------------------------------------------
 +#include "beman/net/net.hpp"
 ⬇️ Suggested change
 -#include "beman/net/net.hpp"
 +#include <beman/net/net.hpp>
 ------------------------------
 In include/beman/net/detail/kqueue_context.hpp
 <#22 (comment)>:
 > +#include "beman/net/detail/event_type.hpp"
 +#include "beman/net/detail/io_base.hpp"
 ⬇️ Suggested change
 -#include "beman/net/detail/event_type.hpp"
 -#include "beman/net/detail/io_base.hpp"
 +#include <beman/net/detail/event_type.hpp>
 +#include <beman/net/detail/io_base.hpp>
 ------------------------------
 In include/beman/net/detail/kqueue_context.hpp
 <#22 (comment)>:
 > +struct kqueue_record;
 +struct kqueue_context;
 +} // namespace beman::net::detail
 +
 +// ----------------------------------------------------------------------------
 +
 +struct beman::net::detail::kqueue_record final {
 +    kqueue_record(::beman::net::detail::native_handle_type h) : handle(h) {}
 +    ::beman::net::detail::native_handle_type handle;
 +    bool                                     blocking{true};
 +};
 +
 +// ----------------------------------------------------------------------------
 +
 +struct beman::net::detail::kqueue_context final : ::beman::net::detail::context_base {
 +    static constexpr size_t event_buffer_size = 10;
 There isn't a guarantee that these names are made available in the global
 namespace when including the respective C++ version of the C headers:
 ⬇️ Suggested change
 -    static constexpr size_t event_buffer_size = 10;
 +    static constexpr ::std::size_t event_buffer_size = 10;
 ------------------------------
 In include/beman/net/detail/kqueue_context.hpp
 <#22 (comment)>:
 > +
 +// ----------------------------------------------------------------------------
 +
 +struct beman::net::detail::kqueue_record final {
 +    kqueue_record(::beman::net::detail::native_handle_type h) : handle(h) {}
 +    ::beman::net::detail::native_handle_type handle;
 +    bool                                     blocking{true};
 +};
 +
 +// ----------------------------------------------------------------------------
 +
 +struct beman::net::detail::kqueue_context final : ::beman::net::detail::context_base {
 +    static constexpr size_t event_buffer_size = 10;
 +    using time_t                              = ::std::chrono::system_clock::time_point;
 +    using timer_node_t                        = ::beman::net::detail::context_base::resume_at_operation;
 +    using event_key_t                         = ::std::tuple<uintptr_t, int16_t>;
 ⬇️ Suggested change
 -    using event_key_t                         = ::std::tuple<uintptr_t, int16_t>;
 +    using event_key_t                         = ::std::tuple<::std::uintptr_t, ::std::int16_t>;
 ------------------------------
 In include/beman/net/detail/kqueue_context.hpp
 <#22 (comment)>:
 > +    constexpr auto to_native_filter(::beman::net::event_type event_type) -> std::span<const int16_t> {
 +        static constexpr std::array<int16_t, 1> read_filter      = {EVFILT_READ};
 +        static constexpr std::array<int16_t, 1> write_filter     = {EVFILT_WRITE};
 +        static constexpr std::array<int16_t, 2> readwrite_filter = {EVFILT_READ, EVFILT_WRITE};
 ⬇️ Suggested change
 -    constexpr auto to_native_filter(::beman::net::event_type event_type) -> std::span<const int16_t> {
 -        static constexpr std::array<int16_t, 1> read_filter      = {EVFILT_READ};
 -        static constexpr std::array<int16_t, 1> write_filter     = {EVFILT_WRITE};
 -        static constexpr std::array<int16_t, 2> readwrite_filter = {EVFILT_READ, EVFILT_WRITE};
 +    constexpr auto to_native_filter(::beman::net::event_type event_type) -> std::span<const ::std::int16_t> {
 +        static constexpr std::array<::std::int16_t, 1> read_filter      = {EVFILT_READ};
 +        static constexpr std::array<::std::int16_t, 1> write_filter     = {EVFILT_WRITE};
 +        static constexpr std::array<::std::int16_t, 2> readwrite_filter = {EVFILT_READ, EVFILT_WRITE};
 ------------------------------
 In include/beman/net/detail/kqueue_context.hpp
 <#22 (comment)>:
 > +            return {read_filter};
 +        case ::beman::net::event_type::out:
 +            return {write_filter};
 +        case ::beman::net::event_type::in_out:
 +            return {readwrite_filter};
 +        case ::beman::net::event_type::none:
 +            return {};
 +        }
 +    }
 +
 +    ::beman::net::detail::container<::beman::net::detail::kqueue_record>  d_sockets;
 +    ::std::map<event_key_t, std::vector<::beman::net::detail::socket_id>> d_event;
 +    ::beman::net::detail::container<::beman::net::detail::io_base*>       d_outstanding;
 +    timer_priority_t                                                      d_timeouts;
 +    ::beman::net::detail::context_base::task*                             d_tasks{};
 +    const int d_queue = kqueue(); // TODO: is this a good practise to put it here?
 I like to initialise members upon definition. It is more important for
 class with multiple constructors but I think this is fine.
 ⬇️ Suggested change
 -    const int d_queue = kqueue(); // TODO: is this a good practise to put it here?
 +    const int d_queue = ::kqueue();
 —
 Reply to this email directly, view it on GitHub
 <#22 (review)>,
 or unsubscribe
 <https://github.com/notifications/unsubscribe-auth/AAJDJAZWKQO5YHXJ6INFM5L23ASJLAVCNFSM6AAAAAB3QJKFYOVHI2DSMVQWIX3LMV43YUDVNRWFEZLROVSXG5CSMV3GSZLXHMZDOOBYHEYDMNRVG4>
 .
 You are receiving this because you authored the thread.Message ID:
 ***@***.***>
 | 
Co-authored-by: Dietmar Kühl <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the late response: I was traveling the last two weeks and I didn't manage to look at the review before or during traveling. Also, I haven't, yet, reviewed everything.
The primary issue with the current code is that it uses a map for each operation: while there may be a few allocations needed for the overall operation, e.g., to manage a vector of open file descriptors, these should be really few (I'd think reallocating a vector managed by the context to accommodate more file descriptors should be sufficient). I believe that the kqueue interface actually helps with that by providing a std::uintptr_t which can be populated with user-selected data. Storing a pointer to an operation state into that is the key idea which should make extra storage and various search unnecessary!
It is a while since I implemented my "toy" kqueue interface but I'm pretty sure it gets away without any allocators. I should also refactor the poll implementation over here to factor out the common POSIX interface (as some of your code is roughly another version instead of sharing the logic of processing once work is reported as ready by poll, kqueue, or epoll functions).
| #include <stdint.h> | ||
| int main() { | ||
| struct kevent ev{::uintptr_t(), ::int16_t(), EV_DELETE, ::uint32_t(), ::intptr_t(), nullptr, {}}; | ||
| (void)ev; | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if that should use [[maybe_unused]] kevent ev{ … }; instead. The cast to (void) does work but nothing in the standard says that it needs to suppress an unused variable warning. Also, C style casts are somewhat frowned upon.
| #include <sys/event.h> | ||
| #include <stdint.h> | ||
| int main() { | ||
| struct kevent ev{::uintptr_t(), ::int16_t(), EV_DELETE, ::uint32_t(), ::intptr_t(), nullptr, {}}; | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In C++ the struct keyword isn’t needed.
| check_cxx_source_compiles( | ||
| " | ||
| #include <sys/event.h> | ||
| #include <stdint.h> | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using nullptr the test is strictly C++: I’d use <cstdint> and ::std::-qualify the respective names.
| } | ||
|  | ||
| template <typename Record> | ||
| inline auto ::beman::net::detail::container<Record>::find(const Record& r) | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This member function can be const.
| } | ||
| return 0u; | ||
| } | ||
| auto remove_outstanding(::beman::net::detail::socket_id outstanding_id) { | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code is too complex for me to easily follow (although I will openly admit that I may write similar code - I shouldn't really and where I did it should probably be fixed). It looks like a fairly linear search for a socket which seems odd: the entire point of kqueue is to remove the linear work (in the kernel) needed when using poll(). With the kqueue interface they should also not be needed in user-space.
| } | ||
| } | ||
| } | ||
| auto to_milliseconds(auto duration) -> int { | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function can be static (and can possibly also be constexpr and/or noexcept although I don't, yet, care much for the latter).
| while (0 < n) { | ||
| --n; | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think using while (0 < n--) or while (n--) is the idiomatic way to phrase counting down loops. For the last iteration the n-- wraps around which is well-defined behaviour of unsigned integers.
| completion->work(*this, completion); | ||
| ++ncompleted; | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code seems to complete multiple operations where the run_one should really just complete one operation. The way to set this up is probably to have an array of already completed worked which is processed one at the time
for each call at the start of the function (it seems that is set up with process_task/process_timeout calls). Once all known to be ready work is exhausted kevent(...) is called to populate the array with new work (or time out).
| auto filters = to_native_filter(completion->event); | ||
| auto outstanding_id = d_outstanding.insert(completion); | ||
| for (const auto f : filters) { | ||
| const event_key_t key{native_handle, f}; | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is the place where we can do better! First off, there is an important aspect of the work being scheduled: I'm fairly certain that all work is either input or output and never both! Thus, there isn't a case where the completion is used for two potential completions at the same time. There may be outstanding read and write operations for the same file descriptor but they'd have different operation states/completion records.
With that in mind, we can store the a pointer to a io_base in the event_key_t! All relevant information should be accessible vai the io_base:
- The file descriptor is accessible via the socket_id.
- The direction is accessible via the event_type.
With that I think there isn't any map needed. Also, the key can be reconstructed from the operation state when it is necessary to cancel the work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi Diemar, my understanding is there could be more than one completion listen to the same socket. If so, I think the pointer in kevent struct should point to a linked list. Please correct me if I am wrong.
| I have tried the  | 
A draft implementation of kqueue context. Not fully tested yet and with a lot of duplicated codes. Compare to poll context: