Zero-Copy
In high-throughput server systems—such as web servers, message brokers (Apache Kafka), database replication pipelines, and distributed file stores—data transfer speed is often bottlenecked not by storage or network hardware, but by CPU data copying and kernel context switching.
Zero-Copy refers to a collection of Linux kernel techniques designed to minimize or entirely eliminate intermediate CPU memory copies between the kernel page cache and userspace buffers when streaming data.
The Traditional I/O Path: 4 Copies & 4 Context Switches
Consider the classic pattern of reading a static file from disk and sending it over a TCP socket:
char buffer[8192];
while ((n = read(file_fd, buffer, sizeof(buffer))) > 0) {
write(socket_fd, buffer, n);
}
Behind this deceptively simple loop, the operating system executes a grueling sequence of steps:
- Context Switch 1 (
readcall): The application switches from userspace to kernel space. - Copy 1 (DMA): The storage controller reads data from the disk and copies it via DMA into the kernel's OS Page Cache.
- Copy 2 (CPU): The CPU copies the data from the OS Page Cache into the userspace
buffer. - Context Switch 2 (
readreturn): Control returns back to userspace with the loaded buffer. - Context Switch 3 (
writecall): The application callswrite()to send the buffer to the socket, switching back into kernel space. - Copy 3 (CPU): The CPU copies the data from the userspace
bufferinto the kernel's Socket Buffer (sk_buff). - Context Switch 4 (
writereturn): Control returns back to userspace. - Copy 4 (DMA): The network card (NIC) uses DMA to transfer the bytes from the socket buffer to the wire.
In total, serving a single file block incurs 4 context switches and 4 data copies (2 hardware DMA copies + 2 expensive CPU memory copies). The CPU copies cause heavy L1/L2/L3 cache churn, pushing other vital application data out of the cache hierarchy.
Zero-Copy Technique 1: mmap() + write()
The first optimization eliminates the CPU copy from kernel space to userspace by memory-mapping the file directly into the process's virtual address space:
void *src = mmap(NULL, stat_buf.st_size, PROT_READ, MAP_SHARED, file_fd, 0);
write(socket_fd, src, stat_buf.st_size);
How it works:
- The
mmap()system call tells the kernel to map the file's Page Cache pages directly into the process's virtual address space. - The userspace copy is bypassed. When
write()is invoked, the CPU copies data directly from the OS Page Cache to the Socket Buffer.
Trade-offs:
- Reduces copies to 3: 2 DMA copies + 1 CPU copy.
- Still incurs 4 context switches: 2 for
mmap(), 2 forwrite(). - The
SIGBUSTrap: If another process truncates or deletes the underlying file while your thread is writing the mapped memory, the kernel issues aSIGBUSsignal, terminating your process unless explicitly handled with a custom signal handler.
Zero-Copy Technique 2: sendfile()
Introduced in Linux 2.1 to optimize web servers, sendfile() transfers data directly between two file descriptors entirely inside the kernel:
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
The In-Kernel Path:
- The application invokes
sendfile(socket_fd, file_fd, &offset, count). - The DMA engine loads file blocks into the kernel Page Cache.
- In modern Linux with NIC Scatter-Gather DMA support (
NETIF_F_SG):- The CPU does not copy file bytes into the socket buffer.
- Instead, only small descriptor headers (containing file page memory pointers and lengths) are appended to the socket buffer.
- The NIC's DMA engine reads packet payloads directly from the kernel Page Cache and transmits them to the wire.
Result:
- Context switches: Reduced from 4 to 2 (one entry, one exit).
- CPU copies: Reduced from 2 to 0.
- Total copies: Exactly 2 DMA copies (Disk to Page Cache, Page Cache to NIC).
This is true hardware-assisted zero-copy.
Zero-Copy Technique 3: splice() and vmsplice()
While sendfile() is primarily designed for file-to-socket transfers, splice() generalizes zero-copy streaming between arbitrary file descriptors, provided at least one of them is a pipe:
#define _GNU_SOURCE
#include <fcntl.h>
ssize_t splice(int fd_in, loff_t *off_in, int fd_out,
loff_t *off_out, size_t len, unsigned int flags);
The Pipe Buffer Abstraction:
splice()works by transferring references to page cache pages (struct pipe_buffer) across the Linux pipe subsystem without copying the underlying memory payload.- By chaining
splice()from an incoming socket to a pipe, and from the pipe to an outgoing socket or file, an application can construct a high-performance, userspace-orchestrated proxy or pipeline with zero payload copying.
Zero-Copy Technique 4: MSG_ZEROCOPY (Socket Transmission)
Added in Linux 4.14 by Willem de Bruijn, MSG_ZEROCOPY brings zero-copy capabilities to raw userspace buffers being written to network sockets:
send(sock_fd, user_buf, buf_size, MSG_ZEROCOPY);
The Notification Mechanism:
- When calling
send()withMSG_ZEROCOPY, the kernel pins the userspace buffer pages into memory rather than copying them into socket buffers. - The network driver transmits data directly from userspace pages via DMA.
- Because the kernel is referencing the memory asynchronously, the application must not modify the buffer until the NIC has finished sending it.
- The kernel signals completion by placing a notification on the socket's error queue (
SO_EE_ORIGIN_ZEROCOPY), which the application reads viarecvmsg(..., MSG_ERRQUEUE).
MSG_ZEROCOPY delivers substantial CPU savings for large payloads (typically > 10KB), but incurs small overhead for tiny packets due to page pinning and completion notifications.
Real-World Production Implementations
Apache Kafka
Kafka's high-throughput partitioning model relies heavily on sendfile(). When a consumer requests topic partitions:
- Kafka invokes the Java NIO
FileChannel.transferTo()method, which maps directly to thesendfile()system call on Linux. - Data moves directly from the operating system's Page Cache to the network interface without ever entering the JVM heap.
- This eliminates Java garbage collection pressure entirely for data transport.
Nginx
Nginx provides the sendfile on; directive in its HTTP configuration block. Combined with tcp_nopush on; (which sets the TCP_CORK socket option), Nginx waits to accumulate full TCP packets before triggering zero-copy DMA transmission, maximizing packet utilization and minimizing system call frequency.
Go io.Copy & net.TCPConn
In the Go standard library, calling io.Copy(conn, file) automatically checks if the destination implements io.ReaderFrom. On Linux, Go's runtime internally leverages sendfile and splice system calls whenever available, falling back to userspace buffering only when descriptors are incompatible.
