Skip to content

Add io_uring UDP event loop path#100

Merged
igorls merged 8 commits into
mainfrom
copilot/replace-epoll-with-io-uring
Apr 28, 2026
Merged

Add io_uring UDP event loop path#100
igorls merged 8 commits into
mainfrom
copilot/replace-epoll-with-io-uring

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 28, 2026

Replaces the Linux userspace UDP control-plane polling path with an io_uring receive/send path when available, while preserving the existing poll() + GRO fallback. TUN io_uring remains runtime-disabled pending bare-metal validation.

  • io_uring UDP ring

    • Added UdpRing with pre-submitted IORING_OP_RECVMSG slots.
    • Added IORING_OP_SENDMSG support using ring-owned send buffers.
    • Attempts IORING_SETUP_SQPOLL, falling back to normal submission if unavailable.
    • Registers fixed buffers when permitted; continues with unregistered buffers when constrained by container/kernel limits.
  • Control-plane integration

    • Linux userspace event loop now prefers UdpRing for UDP receive completions.
    • Existing GRO segmentation handling is preserved for coalesced UDP packets.
    • Existing poll() + recvmsg path remains the fallback when io_uring setup fails.
  • Runtime safety

    • Clears O_NONBLOCK for the UDP fd while the ring is active so pre-submitted receives can arm asynchronously.
    • Restores original fd flags on ring teardown.
    • Handles SQPOLL and buffer-registration availability explicitly in runtime logging.
  • Coverage

    • Added focused coverage for UDP GRO control-message segment-size parsing.

Example shape of the new receive path:

var udp_ring: lib.net.IoUring.UdpRing = undefined;
const use_udp_ring = blk: {
    udp_ring.init(udp_sock.fd) catch break :blk false;
    break :blk true;
};

if (use_udp_ring) {
    const n_cqes = udp_ring.copyCompletions(&cqe_buffer, 0) catch 0;
    for (cqe_buffer[0..n_cqes]) |cqe| {
        const recv = udp_ring.recvCompletion(cqe) orelse continue;
        // process recv.data, then resubmit the receive slot
        udp_ring.resubmitRecv(recv.slot, udp_sock.fd) catch {};
    }
}

Copilot AI linked an issue Apr 28, 2026 that may be closed by this pull request
Copilot AI and others added 6 commits April 28, 2026 08:27
Agent-Logs-Url: https://github.com/igorls/meshguard/sessions/71f00ba9-16d4-42e0-82d3-49200c7d7dcc

Co-authored-by: igorls <4753812+igorls@users.noreply.github.com>
Agent-Logs-Url: https://github.com/igorls/meshguard/sessions/71f00ba9-16d4-42e0-82d3-49200c7d7dcc

Co-authored-by: igorls <4753812+igorls@users.noreply.github.com>
Agent-Logs-Url: https://github.com/igorls/meshguard/sessions/71f00ba9-16d4-42e0-82d3-49200c7d7dcc

Co-authored-by: igorls <4753812+igorls@users.noreply.github.com>
Agent-Logs-Url: https://github.com/igorls/meshguard/sessions/71f00ba9-16d4-42e0-82d3-49200c7d7dcc

Co-authored-by: igorls <4753812+igorls@users.noreply.github.com>
Agent-Logs-Url: https://github.com/igorls/meshguard/sessions/71f00ba9-16d4-42e0-82d3-49200c7d7dcc

Co-authored-by: igorls <4753812+igorls@users.noreply.github.com>
Agent-Logs-Url: https://github.com/igorls/meshguard/sessions/71f00ba9-16d4-42e0-82d3-49200c7d7dcc

Co-authored-by: igorls <4753812+igorls@users.noreply.github.com>
Copilot AI changed the title [WIP] Replace epoll-based event loop with io_uring for zero-copy I/O Add io_uring UDP event loop path Apr 28, 2026
Copilot AI requested a review from igorls April 28, 2026 08:39
- Rename msghdr/msghdr_const fields to match Zig 0.16.0 std
  (msg_name→name, msg_namelen→namelen, etc.)
- Cast fcntl return value via @intcast to handle c_int→usize
  mismatch when libc is linked
@igorls igorls marked this pull request as ready for review April 28, 2026 10:04
Copilot AI review requested due to automatic review settings April 28, 2026 10:04
@igorls igorls merged commit f9fa3ed into main Apr 28, 2026
6 checks passed
@igorls igorls deleted the copilot/replace-epoll-with-io-uring branch April 28, 2026 10:09
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an io_uring-backed UDP receive path to the Linux userspace control-plane event loop (with fallback to the existing poll() + recvmsg GRO path), plus a small unit test covering UDP GRO cmsg segment-size parsing.

Changes:

  • Introduces UdpRing in src/net/io_uring.zig with pre-submitted recvmsg slots and optional buffer registration / SQPOLL setup.
  • Updates userspaceEventLoop() to prefer UdpRing completions for UDP receives while preserving GRO segmentation handling.
  • Adds a focused test validating UDP_GRO cmsg segment-size parsing.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
src/net/io_uring.zig Adds UdpRing implementation (recv/send scaffolding, fd flag management) and a UDP_GRO cmsg parsing unit test.
src/main.zig Integrates UdpRing into the Linux userspace control-plane event loop with runtime logging and poll-based fallback.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/main.zig
Comment on lines +3234 to 3236
if (use_udp_ring) {
const n_cqes = udp_ring.copyCompletions(&cqe_buffer, 0) catch 0;
var n_decrypted: usize = 0;
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

udp_ring.copyCompletions(... ) catch 0 silently suppresses io_uring errors and treats them as "no CQEs", which can leave the loop spinning/backing off with no visibility and no fallback. Consider handling the error explicitly (log once and disable use_udp_ring / fall back to poll()), so failures don't get masked.

Copilot uses AI. Check for mistakes.
Comment thread src/net/io_uring.zig
};
slot.iov = .{ .base = &slot.buf, .len = data.len };
slot.msg.namelen = @sizeOf(posix.sockaddr.in);
slot.busy = true;
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In sendTo, the slot is marked busy = true before queueing/submitting the SQE. If ring.sendmsg() or ring.submit() returns an error, the function exits early and the slot remains permanently busy, reducing send capacity. Consider setting busy only after the SQE is successfully queued (or using an errdefer to clear it on failure).

Suggested change
slot.busy = true;
slot.busy = true;
errdefer slot.busy = false;

Copilot uses AI. Check for mistakes.
Comment thread src/net/io_uring.zig
Comment on lines +237 to +246
if ((flags & O_NONBLOCK_FLAG) != 0) {
// Blocking fds let io_uring arm async socket receive instead of completing
// immediately with EAGAIN on every pre-submitted recvmsg SQE. The saved
// original flags are restored by restoreFdFlags() during cleanup.
const set_rc = posix.system.fcntl(fd, posix.F.SETFL, flags & ~O_NONBLOCK_FLAG);
switch (posix.errno(set_rc)) {
.SUCCESS => {},
else => |err| return posix.unexpectedErrno(err),
}
}
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UdpRing.init clears O_NONBLOCK on the shared UDP socket fd. The control-plane still sends responses via udp_sock.sendTo(...) (which uses sendto(..., flags=0)), so those sends can now block under socket backpressure. To preserve prior non-blocking behavior, consider sending with MSG_DONTWAIT on that socket (or routing control-plane sends through UdpRing.sendTo / io_uring instead of direct sendto).

Suggested change
if ((flags & O_NONBLOCK_FLAG) != 0) {
// Blocking fds let io_uring arm async socket receive instead of completing
// immediately with EAGAIN on every pre-submitted recvmsg SQE. The saved
// original flags are restored by restoreFdFlags() during cleanup.
const set_rc = posix.system.fcntl(fd, posix.F.SETFL, flags & ~O_NONBLOCK_FLAG);
switch (posix.errno(set_rc)) {
.SUCCESS => {},
else => |err| return posix.unexpectedErrno(err),
}
}
// Keep the socket's original status flags unchanged. This fd is shared with
// control-plane code paths that may still use direct sendto/sendTo calls, and
// clearing O_NONBLOCK here would make those sends block under backpressure.

Copilot uses AI. Check for mistakes.
Comment thread src/net/io_uring.zig
Comment on lines +284 to +288
pub fn resubmitRecv(self: *UdpRing, slot: u16, fd: posix.fd_t) !void {
try self.submitRecv(slot, fd);
_ = try self.ring.submit();
}

Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resubmitRecv calls ring.submit() for every completed packet, which can reintroduce a syscall-per-packet when SQPOLL isn't active and reduces the benefit of pre-submitted receives. Consider providing a queueResubmitRecv (no submit) and submitting once after processing a batch of CQEs (or resubmitting all drained slots and calling submit() once).

Suggested change
pub fn resubmitRecv(self: *UdpRing, slot: u16, fd: posix.fd_t) !void {
try self.submitRecv(slot, fd);
_ = try self.ring.submit();
}
pub fn queueResubmitRecv(self: *UdpRing, slot: u16, fd: posix.fd_t) !void {
try self.submitRecv(slot, fd);
}
pub fn resubmitRecv(self: *UdpRing, slot: u16, fd: posix.fd_t) !void {
try self.queueResubmitRecv(slot, fd);
}
pub fn submitRecvResubmissions(self: *UdpRing) !u32 {
return try self.ring.submit();
}

Copilot uses AI. Check for mistakes.
Comment thread src/main.zig
if (use_udp_ring) {
const sqpoll_msg = if (udp_ring.sqpoll) "SQPOLL" else "submit";
const buffer_msg = if (udp_ring.registered_buffers) "registered buffers" else "unregistered buffers";
writeFormatted(stdout, " io_uring UDP: recvmsg/sendmsg ring active ({s}, {s})\n", .{ sqpoll_msg, buffer_msg }) catch {};
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The startup log says "recvmsg/sendmsg ring active", but the control-plane still sends via udp_sock.sendTo(...) (regular sendto) rather than UdpRing.sendTo, so "sendmsg" isn't actually active here. Consider adjusting the message (e.g., "recvmsg ring active") or switching control-plane sends to the ring if intended.

Suggested change
writeFormatted(stdout, " io_uring UDP: recvmsg/sendmsg ring active ({s}, {s})\n", .{ sqpoll_msg, buffer_msg }) catch {};
writeFormatted(stdout, " io_uring UDP: recvmsg ring active ({s}, {s})\n", .{ sqpoll_msg, buffer_msg }) catch {};

Copilot uses AI. Check for mistakes.
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.

io_uring event loop

3 participants