A static-file server reads a 1 GB object from a file and writes it to a socket. The application does not inspect or modify the payload, yet a conventional loop copies every byte through a temporary user-space buffer.
The buffer exists only to move unchanged bytes from one kernel-managed object to another. For large transfers, those copies consume CPU time, memory bandwidth, and cache capacity that could serve request processing.
Zero-copy interfaces let the kernel move or reference data without routing the payload through an application buffer. The term does not mean that data never moves. Storage devices, memory, kernel metadata, and network devices still participate. It means that one or more CPU-mediated copies have been removed from a particular path.
Buffered file data normally enters kernel-managed pages. A read() copies it into an application buffer, and write() copies it into socket-managed memory:
The application performs at least two system calls per chunk and temporarily fills CPU caches with bytes it never examines. Small buffers add more system calls. Large buffers use more application memory without removing the copies.
This path remains appropriate when the application must parse, compress, encrypt, transform, or validate the bytes. In those cases, the user-space copy carries data that the application needs to access.
Copy avoidance matters most when the data is already in a kernel-managed object and the application only needs to direct it elsewhere.
The phrase describes several different optimizations:
One API may satisfy some of these descriptions but not all of them. An implementation can also fall back to copying when a filesystem, socket type, alignment, or device lacks the required support.
The accurate claim is therefore scoped:
A zero-copy interface removes specified copies from one transfer path. It does not guarantee zero data movement or zero CPU work.
The kernel still validates descriptors, updates offsets and socket state, manages page lifetimes, creates protocol metadata, handles errors, and wakes blocked processes.
sendfile(): File to Socket in One Kernel OperationLinux sendfile() transfers bytes between two descriptors:
For static file serving, the input is a regular file and the output is a connected stream socket. The application supplies the descriptors, offset, and maximum byte count without supplying a payload buffer.
The optimized path can let the socket refer to the kernel pages that already contain file data:
The exact implementation depends on the kernel, filesystem, protocol, and network device. Small metadata copies remain, while the large payload avoids the round trip through user space.
sendfile() returns the number of bytes transferred. That value can be smaller than count, so callers must loop. A return of -1 with EINTR means the call was interrupted before completing the requested work. Nonblocking output can produce EAGAIN.
The offset argument changes file-position behavior:
The second form is useful when several independent transfers share one open file description or when the application wants explicit progress tracking.
read()/write() with sendfile()The following Linux program transfers one file through a Unix stream socket pair. One mode uses a user-space buffer. The other uses sendfile().
The receiving child still calls read(), so the comparison isolates copy avoidance on the sending side. It does not represent an end-to-end network benchmark.
Compile it on Linux:
Create a test file, then compare the two modes:
Run each mode several times. The first run can include storage latency, while later runs may use cached file pages. Compare CPU time as well as elapsed time.
This program assumes the file's contents and size remain stable during transfer. A concurrent truncate or rewrite can shorten the transfer or change the bytes observed by the receiver.
Loading simulation...
sendfile() Is Not Always FasterFor a small response, setup and bookkeeping can cost more than copying a short buffer. The application may already have the data in user memory, leaving no file-backed pages for sendfile() to reference.
Transformations also require access to the payload. User-space compression, content rewriting, and user-space TLS encryption must examine or produce new bytes. The conventional application buffer then has a real purpose.
Kernel TLS can preserve optimized file-transfer paths on supported configurations, but the result depends on the cipher, device capabilities, and kernel setup. It should be measured rather than assumed.
sendfile() also has descriptor restrictions and implementation-specific fast paths. Unsupported combinations can fail, and supported combinations may still copy internally. Correctness must not depend on the optimization being zero-copy.
splice(): Moving Data Through a Kernel PipeLinux splice() transfers data between descriptors without exposing the payload through a user-space address. At least one endpoint must be a pipe.
A file-to-socket path therefore uses the pipe as a kernel conduit:
The program calls splice() once to fill the pipe and again to drain it:
SPLICE_F_MOVE is a hint that pages should be moved instead of copied. SPLICE_F_MORE tells the kernel that more data is expected. Neither flag changes the need to handle partial results and errors.
The pipe has bounded capacity, so blocking and backpressure still apply. Nonblocking code must handle EAGAIN on both the input and output stages.
splice() is useful when a pipeline must connect descriptor types that sendfile() does not handle directly. For regular file-to-socket transfer, sendfile() expresses the operation with fewer calls and less plumbing.
tee() and vmsplice()Linux provides two related pipe operations.
tee() duplicates references from one pipe to another without consuming the source pipe. It can support fan-out paths in which the same stream goes to two destinations. Each destination still needs backpressure and failure handling.
vmsplice() connects user memory to a pipe. Some aligned, page-backed buffers can be referenced instead of copied, but the application must obey page-lifetime and mutation rules. Other cases can copy internally.
These APIs expose more control than sendfile(), along with more ways to violate buffer ownership. They are best reserved for measured transfer paths with strict lifetime management.
mmap() Fitsmmap() maps file-backed pages into a process's virtual address space:
The application can then inspect file data through ordinary loads without first copying it into a buffer with read(). Pages become accessible as the process touches them.
Sending that mapping with write() or ordinary send() still normally copies bytes into socket-managed memory. mmap() removes the file-to-user copy, while the user-to-socket copy remains.
This makes mapping useful when an application must parse or transform file contents. It is not the shortest path for forwarding an unchanged file to a socket.
The mapping also introduces lifetime and fault rules. The process must stay within the mapped range, and access beyond a file that another process truncated can raise SIGBUS. Those virtual-memory mechanics are separate from the copy-avoidance decision.
MSG_ZEROCOPY for User-Owned Bufferssendfile() begins with kernel-managed file pages. Some servers instead generate large payloads in user memory.
Linux supports a zero-copy socket mode:
The kernel can pin and reference the application's pages rather than copying the payload into separate socket memory.
The application cannot reuse or modify those pages immediately after send() returns. It must read completion notifications from the socket error queue and wait until the kernel reports that the relevant byte ranges are safe to reuse.
Page pinning, completion processing, and fallbacks add overhead. Small messages often cost less to copy. MSG_ZEROCOPY is intended for large writes from buffers whose ownership can remain with the kernel until asynchronous completion.
This API changes buffer-lifetime semantics substantially. A missed completion can leak pinned memory or cause the application to corrupt data still being transmitted.
Linux copy_file_range() copies between two file descriptors without routing payload through user space. A filesystem may implement the operation through page-cache work, storage offload, or another optimized mechanism.
It targets file-to-file copies rather than file-to-socket serving. Unsupported filesystems or descriptor combinations can fail, and cross-filesystem behavior depends on kernel and filesystem support.
The important distinction is the endpoint contract:
| Interface | Source | Destination | User sees payload |
|---|---|---|---|
sendfile() | Usually a regular file | Commonly a stream socket | No |
splice() | Descriptor with a pipe endpoint | Descriptor with a pipe endpoint | No |
mmap() plus send() | Mapped file pages | Socket | Yes |
MSG_ZEROCOPY | User-owned pages | Socket | Yes, with deferred reuse |
copy_file_range() | File | File | No |
An interface name does not guarantee a particular hardware path. Kernel, filesystem, and device support determine whether the implementation references pages, offloads work, or copies internally.
Wall-clock time alone can hide the resource being saved. Compare:
strace -c shows the syscall difference:
perf stat can compare CPU work:
File-cache state, CPU frequency, storage speed, socket type, and receiver speed can dominate one run. Use the same file, receiver, chunk policy, and concurrency for both paths.
The optimized path should also be tested under error conditions: a receiver that closes early, a nonblocking socket that fills, a file that changes, and a transfer interrupted by a signal. Fast success-path measurements do not validate partial-result handling.
Copy avoidance is well-suited to large, unchanged payloads that already live in files or stable page-backed buffers. Static assets, backups, local data pipelines, and proxying unchanged byte streams can benefit.
It offers less value when messages are small or the application must transform every byte. The extra lifetime rules of pinned pages or pipe-based transfer may cost more than one memory copy.
API portability also matters. sendfile() exists on several operating systems but has different signatures and capabilities. splice() and MSG_ZEROCOPY are Linux-specific. A portable application often needs a correct read()/write() fallback.
Loading simulation...
Zero-copy interfaces remove payload copies from a specific transfer path. They still require kernel bookkeeping, memory references, device transfers, system calls, and correct handling of partial progress.
sendfile() is the direct Linux interface for forwarding unchanged file data to a socket. splice() connects descriptors through a kernel pipe, while mmap() gives the application direct access to file-backed pages but leaves an ordinary socket-copy step. MSG_ZEROCOPY can reference large user buffers with deferred reuse and completion handling.
Copy avoidance is valuable when large payloads pass through unchanged. Measurement must include CPU time, cache effects, syscall count, receiver behavior, fallbacks, and the added ownership rules.
5 quizzes