Skip to content

Conversation

@pandazxx
Copy link
Contributor

@pandazxx pandazxx commented Apr 21, 2025

A draft implementation of kqueue context. Not fully tested yet and with a lot of duplicated codes. Compare to poll context:

  1. The read-write event will create seperate events.
  2. One event could, in theory, has multiple outstanding operations.
  3. Canceling one operation should also deregister the coresponding read/write event in case of the operation listen to read-write event.

Copy link
Member

@dietmarkuehl dietmarkuehl left a 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).

@pandazxx
Copy link
Contributor Author

pandazxx commented Apr 24, 2025 via email

@pandazxx pandazxx marked this pull request as ready for review May 4, 2025 04:09
@pandazxx pandazxx requested a review from camio as a code owner May 4, 2025 04:09
@pandazxx pandazxx requested review from a team, DeveloperPaul123 and JeffGarland May 4, 2025 04:09
Copy link
Member

@dietmarkuehl dietmarkuehl left a 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;
Copy link
Member

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, {}};
Copy link
Member

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>
Copy link
Member

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)
Copy link
Member

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) {
Copy link
Member

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 {
Copy link
Member

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).

Comment on lines +200 to +201
while (0 < n) {
--n;
Copy link
Member

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.

Comment on lines +216 to +217
completion->work(*this, completion);
++ncompleted;
Copy link
Member

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};
Copy link
Member

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:

  1. The file descriptor is accessible via the socket_id.
  2. 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.

Copy link
Contributor Author

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.

@dietmarkuehl
Copy link
Member

I have tried the milano example with kqueue and it seems it terminates the I/O loop while the poll version doesn't. I suspect there is something not quite right (although I'm not, yet, sure in which version).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants