Zero-Copy Networking on Linux: The Complete Guide to io_uring, splice(), and AF_XDP at 100GbE

In the era of 1 Gigabit and 10 Gigabit Ethernet, high-performance Linux network engineering was relatively forgiving. Standard POSIX socket API callsread(), write(), recv(), and send()worked reliably, and a modern multi-core x86 CPU could process network traffic with modest CPU utilization. But as modern enterprise cloud infrastructure, high-frequency algorithmic trading fabrics, and distributed AI storage clusters migrated to 40GbE, 100GbE, and 200GbE line rates, the classical Linux networking stack collided with an insurmountable architectural barrier: the memory copy and system call cliff.

At 100 Gigabit Ethernet line rate processing minimum-sized Ethernet frames (64 bytes), a server’s network interface card (NIC) receives an astonishing 148.8 million packets per secondmeaning a new packet arrives at the physical MAC layer every 6.72 nanoseconds. In that microscopic temporal window, an operating system using standard sockets must allocate kernel data structures (sk_buff), execute softirq interrupt service routines, transition between user space and kernel space via system call traps, and copy packet payloads across CPU cache hierarchies using copy_to_user(). The CPU spends over 80% of its execution cycles performing pure memory thrashing rather than processing business logic. To survive at 100GbE, systems architects must eliminate the kernel from the data path using Zero-Copy networking. Here is the deep engineering guide to how Linux zero-copy evolved from splice() to modern io_uring fixed buffers, and how AF_XDP (XDP Sockets) achieves true line-rate kernel-bypass packet processing at 35+ million packets per second per core.

Advertisement


1. The Anatomy of POSIX Memory Copy Stalls at 100GbE

To understand why zero-copy networking is an engineering imperative, one must first trace the tortuous journey of a network packet through the traditional Linux kernel networking stack:

  1. Hardware DMA into Kernel Ring: When an Ethernet frame hits the physical PHY and MAC layers of a 100GbE NIC (such as an NVIDIA Mellanox ConnectX-6 or Intel E810), the NIC’s DMA engine transfers the raw packet bytes across the PCIe bus into a host memory receive ring buffer (Rx Ring).
  2. Kernel sk_buff Allocation: The NIC driver issues a hardware interrupt. The kernel’s NAPI (New API) polling subsystem allocates a complex kernel data structure known as a Socket Buffer (sk_buff). An sk_buff is a heavy, 240-byte memory descriptor containing dozens of pointers, protocol flags, and network namespace tags. Allocating and freeing millions of sk_buff objects per second severely fragments the kernel’s slab allocator (SLUB).
  3. TCP/IP Protocol Stack Traversal: The kernel parses IP headers, validates checksums, manages TCP sequence windows, and routes the packet through Netfilter / iptables firewall rules.
  4. The Crushing copy_to_user() CPU Memcpy: When the user application issues a recv() or read() system call, the CPU transitions from Ring 3 (User Space) to Ring 0 (Kernel Space). The CPU execution core executes a physical memory copy, transferring the payload bytes from the kernel’s socket buffer into the application’s user-space memory buffer.

This final memory copy is where high-speed networking collapses. In modern server architecture, main system DRAM has a typical read/write bandwidth of 150 to 300 GB/s. Copying a 100 Gbps stream (12.5 Gigabytes per second) requires 12.5 GB/s of read bandwidth and 12.5 GB/s of write bandwidtheffectively consuming 25 Gigabytes per second of memory bus capacity solely moving bytes between kernel and user buffers!

Furthermore, each memory copy evicts active application data from the CPU’s Level 1 and Level 2 caches. Combined with the overhead of system call context switches (flushing translation lookaside buffers / TLBs post-Meltdown), a standard POSIX socket implementation saturates 100% of a modern CPU core at less than 2 to 3 million packets per secondleaving 98% of a 100GbE pipe unserviceable!

Standard POSIX Multi-Copy Kernel Network Flow vs. Direct Zero-Copy Ring Buffer Architecture.
Figure 1: Standard POSIX Multi-Copy Kernel Network Flow vs. Direct Zero-Copy Ring Buffer Architecture.

2. The First Evolution: Pipe Splicing via splice() and vmsplice()

To eliminate user-space memory copies in streaming workloads (such as proxying network traffic directly to disk or forwarding TCP streams between interfaces), Linux introduced the splice() system call in kernel 2.6.17.

Instead of copying data into a user-space buffer, splice() transfers data between a network socket and an operating system pipe buffer:

// Forwarding data from socket to file without user copy
#define _GNU_SOURCE
#include <fcntl.h>

int pipe_fd[2];
pipe(pipe_fd);

// 1. Splice socket into pipe (Zero user-space memory copy)
splice(socket_fd, NULL, pipe_fd[1], NULL, 65536, SPLICE_F_MOVE | SPLICE_F_MORE);

// 2. Splice pipe into disk file descriptor
splice(pipe_fd[0], NULL, disk_fd, NULL, 65536, SPLICE_F_MOVE | SPLICE_F_MORE);

Under the hood, a Linux pipe is not a block of memory; it is a circular ring of pipe_buffer descriptors containing pointers to physical memory page frames (struct page*). When splice() executes with SPLICE_F_MOVE, the kernel simply transfers the page pointers from the socket buffer into the pipe buffer. The raw payload bytes remain completely untouched in physical RAM!

While splice() was revolutionary for bulk streaming utilities (like Apache Kafka and video-on-demand servers), it remains inadequate for high-frequency packet processing:

  • It still requires two synchronous system calls per batch, inducing context-switch overhead.
  • It cannot inspect or manipulate packet payloads in user space without triggering page unmapping.
  • It cannot handle UDP or raw Ethernet frames, limiting throughput to roughly 8 to 14 million packets per second.

3. The Modern Asynchronous Era: io_uring with Registered Fixed Buffers

In Linux 5.1, kernel architect Jens Axboe introduced io_uring, fundamentally transforming how Linux executes asynchronous I/O. By placing two lockless circular ring buffersthe Submission Queue (SQ) and the Completion Queue (CQ)into shared memory mapped between user space and kernel space, applications can issue thousands of I/O operations without executing a single system call!

In Linux 5.19 and 6.0+, io_uring gained native support for true zero-copy networking through two monumental features:

  • Pre-Registered Fixed Buffers (IORING_REGISTER_BUFFERS): In standard I/O, the kernel must validate user-space memory pointers, verify page permissions, and look up physical page frame numbers (PFNs) on every single transfer. With fixed buffers, the application pre-registers its memory arena with the kernel once at startup:

    io_uring_register(ring_fd, IORING_REGISTER_BUFFERS, iovecs, nr_iovecs);

    The kernel pins the physical pages in RAM and maps them permanently into kernel address space, completely eliminating runtime page table walks.

  • Zero-Copy Send (IORING_OP_SEND_ZC): When an application submits a send request via IORING_OP_SEND_ZC, the network card’s DMA engine pulls the packet bytes directly from the application’s pre-registered user buffer. The kernel emits two completion events into the CQ ring: one when the data is queued, and a notification CQE when the NIC finishes transmission, signaling that the user buffer is safe to reuse.
  • Polled Mode (IORING_SETUP_SQPOLL): By spawning a dedicated kernel thread that continuously polls the Submission Queue ring, the application deposits network requests into shared memory and reaps completions with zero system calls and zero CPU interrupt traps.

Using io_uring in zero-copy SQPOLL mode, modern network proxies (like Envoy and custom microservices) scale packet processing to 18 to 26 million packets per second per core, doubling traditional socket performance while keeping standard Linux TCP/IP semantics intact.

AF_XDP Kernel Bypass Architecture: Direct NIC DMA into User-Space UMEM Rings.
Figure 2: AF_XDP Kernel Bypass Architecture: Direct NIC DMA into User-Space UMEM Rings.

4. The Ultimate Line-Rate Weapon: AF_XDP (XDP Sockets) Kernel Bypass

When an organization needs to process raw Ethernet frames, DNS queries, financial market feeds, or DDoS mitigation at the absolute physical limit of 100GbE (148.8 Mpps), even io_uring‘s TCP/IP stack overhead is too slow.

The ultimate Linux networking technology is Address Family XDP (AF_XDP), introduced in Linux 4.18 and matured in Linux 6.x. AF_XDP is a high-speed, kernel-bypass socket that routes raw Ethernet frames directly between the network card and user-space memory, completely bypassing the kernel’s entire TCP/IP stack, sk_buff allocation, and Netfilter subsystem.

Unlike proprietary user-space drivers (such as DPDK), which seize total exclusive control of the physical NIC and blind standard Linux administration tools, AF_XDP operates inside the official Linux kernel architecture using the eBPF (Extended Berkeley Packet Filter) subsystem.

The AF_XDP architecture is anchored by a shared user-memory arena known as the UMEM, coordinated through four lockless, single-producer single-consumer circular ring queues:

  1. The UMEM Memory Arena: A large, contiguous block of physical memory (typically allocated using 2MB or 1GB HugePages) partitioned into equal-sized frame buffers (e.g., 2,048 or 4,096 bytes). Both the user application and the NIC driver map this exact same memory space.
  2. The Fill Ring (User → Kernel): The user application deposits descriptors pointing to empty UMEM frame addresses into the Fill Ring. These empty frames are consumed by the NIC driver to receive incoming packets.
  3. The Rx Ring (Kernel → User): When the NIC receives an incoming Ethernet frame, the hardware DMA engine transfers the bytes directly into one of the pre-allocated UMEM frames. The driver pushes a descriptor (containing the frame address, length, and timestamp) into the Rx Ring. The user application reads the descriptor and immediately parses the packet in-place with zero CPU memory copies!
  4. The Tx Ring (User → Kernel): When the application needs to transmit, it writes packet bytes into an empty UMEM frame and deposits the descriptor into the Tx Ring. The NIC driver commands the NIC hardware to DMA the packet directly out onto the wire.
  5. The Completion Ring (Kernel → User): Once the NIC finishes transmitting the frame, the driver deposits the frame address into the Completion Ring, notifying the application that the buffer is free to be repurposed.

In zero-copy mode (enabled via the flag XDP_ZEROCOPY on drivers supporting modern NICs like Mellanox mlx5 or Intel ice), AF_XDP achieves a staggering 35 to 50+ million packets per second per CPU coresaturating 100GbE line rate with tiny 64-byte packets while consuming less than 40% CPU utilization.

5. Comprehensive Linux Networking Paradigm Comparison Table

To understand how these four paradigms compare across latency, throughput, and programming complexity, review the detailed engineering specifications:

Networking Technology Memory Copies per Pkt Syscall Overhead Peak Throughput (Single Core) Kernel TCP/IP Support
Standard POSIX (`read/write`) 2 Copies (DMA + CPU memcpy) 1 Syscall per Packet/Batch 1.52.5 Mpps (Bottleneck) Full (Native Linux Stack)
Pipe Splicing (`splice()`) 0 User Copies (Page Swap) 2 Syscalls per Transfer 8.014.0 Mpps Full (TCP Stream Only)
io_uring (Fixed Buffers ZC) 0 Copies (Pre-registered DMA) 0 Syscalls (SQPOLL Polled) 18.026.0 Mpps Full (TCP & UDP Sockets)
AF_XDP (Kernel Bypass) 0 Copies (Direct UMEM DMA) 0 Syscalls (Lockless Rings) 35.050.0+ Mpps Bypassed (Raw L2/L3 Packets)

6. Master Production Sysctl & Driver Hardening Guide

To extract maximum line-rate throughput from 100GbE network interfaces on Linux production hosts, configure the following kernel parameters in /etc/sysctl.d/99-100gbe-networking.conf:

# /etc/sysctl.d/99-100gbe-networking.conf

# 1. Expand TCP socket buffer maximums for 100Gbps BDP (Bandwidth-Delay Product)
net.core.rmem_max = 67108864 # 64MB receive window
net.core.wmem_max = 67108864 # 64MB send window
net.ipv4.tcp_rmem = 4096 87380 67108864
net.ipv4.tcp_wmem = 4096 65536 67108864

# 2. Expand network device backlog queue to prevent drop on microbursts
net.core.netdev_max_backlog = 250000

# 3. Enable low-latency busy polling to bypass softirq dispatch
net.core.busy_poll = 50
net.core.busy_read = 50

# 4. Maximize hardware NIC ring buffer descriptors via ethtool
# Run in startup script: sudo ethtool -G eth0 rx 4096 tx 4096

Official Engineering Whitepaper

Linux Zero-Copy High-Throughput Networking Engineering Handbook

Deep Implementation Guide: io_uring Fixed Buffers, splice() Rings, and AF_XDP at 100GbE Line Rate (Complete architectural audit with telemetry tables and schematic breakdowns).

Reference File: Linux_Zero_Copy_Networking_io_uring_AF_XDP_Handbook.pdf
Format: PDF / KaTeX Telemetry

Frequently Asked Questions (FAQ)

1. How does AF_XDP differ from the Data Plane Development Kit (DPDK)?

DPDK completely unbinds the physical network card from the Linux kernel, requiring proprietary user-space drivers (UIO or VFIO). Once DPDK takes over, standard Linux tools (such as tcpdump, ethtool, and iptables) cease functioning, and the dedicated CPU core is 100% pegged in an infinite polling loop. AF_XDP, by contrast, operates inside the Linux ecosystem via eBPF. The NIC remains fully visible to Linux, standard interfaces function normally, and packets that the application does not care about can be passed back into the kernel network stack seamlessly.

2. Can AF_XDP process standard TCP connections like an HTTP web server?

Not without a user-space TCP stack. Because AF_XDP intercepts raw Ethernet frames before the kernel TCP/IP stack touches them, it does not provide automatic TCP three-way handshakes, sequence tracking, or packet reassembly. To build a TCP web server on AF_XDP, you must integrate a user-space TCP implementation (such as F-Stack, mTCP, or Seastar). For standard TCP microservices, io_uring in zero-copy mode is preferred because it delivers zero-copy acceleration while retaining the kernel’s battle-tested TCP state machine.

3. Do all network interface cards support AF_XDP Zero-Copy mode?

AF_XDP runs in two modes: Generic (SKB) mode and Native Zero-Copy mode. Generic mode runs on any NIC driver but involves an internal sk_buff copy, capping throughput at ~10 Mpps. Native Zero-Copy mode requires driver support for the XDP_ZEROCOPY flag, which maps the UMEM directly to the NIC hardware descriptors. Enterprise 25GbE, 100GbE, and 200GbE NICs from NVIDIA/Mellanox (mlx5), Intel (ice, i40e, igc), and Broadcom (bnxt) fully support native Zero-Copy mode in upstream Linux kernels.

S
Editorial Review by salamo — Senior Systems & Hardware Analyst
Independent analysis grounded in technical architecture, lab measurements, and verified vendor specifications. Our content adheres to strict editorial standards: zero AI fluff, independent evaluation, and transparent methodology.

Add Comment