Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Operating Systems  Module 9 – I/O systems, disks and disk scheduling

Buffering, caching and zero-copy I/O

Where the kernel buffers, caches and spools data, why the buffer size sets the cost of a copy, measured, and how sendfile and splice skip copies.

  • Intermediate
  • 20 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Clang 21.0.0 and GCC 16.2.0
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: Buffers, caches and spools hold data on its way · The buffer size decides how many system calls a copy makes · Zero-copy: the kernel moves the data, the program never touches it · Practise: predict what a copy costs. With a pass: 3 in-depth parts and 3 interview answers, about 13 minutes.

What you will learn

  • Explain buffering, caching and spooling in the kernel's I/O subsystem
  • Measure how the buffer size changes the number of system calls and the speed of a copy
  • Compare blocking, non-blocking and asynchronous I/O by what a call does when data is not ready
  • Use sendfile and splice to move data between descriptors without copying it through user space

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain buffering, caching and spooling in the kernel's I/O subsystem
  • Measure how the buffer size changes the number of system calls and the speed of a copy
  • Compare blocking, non-blocking and asynchronous I/O by what a call does when data is not ready
  • Use sendfile and splice to move data between descriptors without copying it through user space

Before you start

On this page

Copying a file with a one-byte buffer makes two system calls for every byte, 2,097,152 for each mebibyte; with a buffer of 1 MiB it makes two. On the recorded Mac the large buffer copied tens of thousands of times faster. Between a program and its disk the kernel keeps data in buffers and in a cache, and it can move data from one file to another without the program touching it.

This lesson shows where the kernel’s buffering and caching happen, measures what the size of a buffer costs, and explains how zero-copy I/O with sendfile and splice avoids copies.

Buffers, caches and spools hold data on its way

A line a program prints does not go straight to the disk. The C library collects output in a buffer of its own, and POSIX says standard output is fully buffered unless it refers to an interactive device. When the buffer is written, write copies it into the kernel and returns before the disk has seen anything. Follow both directions:

The write path through the C library buffer, the page cache and write-back, and the read path from the drive through the page cache.printf, fwriteC library's bufferread(fd, buf, n)into the program's bufferwrite: one callper full bufferPage cache hitcopied from memoryPage cachepage marked dirtyPage cache missthe drive fills a pageWritten back later,or at fsyncDrivebuffer fullcopyDMADMAcopy

Where data waits between a program and the drive

Text description of the diagram

The figure has eight boxes in two columns: the write path runs down the left, the read path up the right.

  1. Top left: printf and fwrite put output in the C library's buffer, inside the program.
  2. When that buffer is full (or at the end of a line, for a terminal), the library makes one write system call, which copies the buffer into the page cache.
  3. In the page cache the written page is marked dirty. The call has returned, but the drive has nothing yet.
  4. The dirty page is written back later by the kernel's flusher threads, or when the program calls fsync, and reaches the drive by DMA.
  5. On the right, from the bottom: a read that misses the page cache makes the drive fill a page of the cache by DMA.
  6. Once the page is in the cache, the read is a hit: the data is copied from memory into the program's buffer, and later reads of the same data never touch the drive.

Three ideas share the work, and the kernel uses all of them:

  • buffer: Memory that holds data once on its way between a producer and a consumer that work at different speeds or in different sizes, such as the C library's output buffer. holds data between a producer and a consumer that work at different speeds or in different sizes. The C library’s buffering turns many small prints into one write.
  • Caching keeps a copy so that a later access can skip the slow device. The kernel’s caching of files happens in the page cache: in its own words, “whenever a file is read, the data is put into the page cache”; a write lands there too, as a dirty page: A page in memory that has been written since it was read from the drive, so the drive's copy is out of date until the page is written back., and reaches the drive later.
  • A spooling: Queuing whole jobs on disk for a device that can serve only one job at a time, as a print scheduler keeps print jobs in its spool directory. queues whole jobs on disk for a device that serves one at a time. The CUPS print scheduler keeps the files of each waiting job in its spool directory, usually /var/spool/cups.

The difference between buffering and caching is what happens to the data afterwards: a buffer’s data is used once, on its way through, and a cache’s data is kept because it may be wanted again. One page cache: The kernel's cache of file data in memory: pages read from a file stay there for later reads, and written pages wait there before they reach the drive. page can be both, since a dirty page buffers a write and meanwhile answers reads of the same data.

Checkpoint

Check: Buffering, caching and spooling

  1. Question 1 of 2 Which of these is a cache rather than a buffer?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The page cache keeping a file's pages after a read, so that later reads skip the drive

    A buffer holds data on its way between two parts that work at different speeds or sizes; a cache keeps a copy for the next access. A dirty page in the page cache is both at once.

  2. Question 2 of 2 A program prints 100 lines of 30 bytes each to a file, and the C library's buffer holds 4,096 bytes. When does the kernel first see any of the lines?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Only when the program calls fflush or exits, because 3,000 bytes never fill the buffer

    100 × 30 = 3,000 bytes fit in one 4,096-byte buffer, so the library makes a single write when the stream is flushed or closed. POSIX says standard output is fully buffered unless it refers to an interactive device.

The buffer size decides how many system calls a copy makes

A copy with read and write makes two system calls per buffer, so the buffer’s size sets the number of calls. The program copies a file with buffers from 1 byte to 1 MiB, giving each size enough data to time. It uses Python’s os.read and os.write, low-level functions that make one system call each:

One file copied with six buffer sizes Python · copy_sizes.py
# Copies a file with os.read and os.write, which make one system call each,
# one buffer size at a time, and reports the calls per mebibyte and the speed.
import os
import time

MIB = 1024 * 1024
TOTAL = 16 * MIB
SIZES = [1, 16, 256, 4096, 65536, MIB]


def copy(src: int, dst: int, size: int, nbytes: int) -> int:
    """Copies nbytes from src to dst in pieces of size; returns the calls made."""
    calls = 0
    os.lseek(src, 0, os.SEEK_SET)
    os.lseek(dst, 0, os.SEEK_SET)
    done = 0
    while done < nbytes:
        piece = os.read(src, size)
        os.write(dst, piece)
        calls += 2
        done += len(piece)
    return calls


def label(size: int) -> str:
    return f"{size // 1024:4} KiB" if size >= 1024 else f"{size:6} B"


src = os.open("source.bin", os.O_RDWR | os.O_CREAT | os.O_TRUNC, 0o600)
dst = os.open("copy.bin", os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
block = bytes(i % 26 + 97 for i in range(256)) * (MIB // 256)
for _ in range(TOTAL // MIB):
    os.write(src, block)
copy(src, dst, MIB, TOTAL)  # untimed: gives the copy its pages first

print("buffer   calls per MiB       MB/s")
speeds = []
for size in SIZES:
    # Small buffers copy less data, so that no size takes much over a second.
    nbytes = {1: 64 * 1024, 16: MIB}.get(size, TOTAL)
    start = time.perf_counter()
    calls = copy(src, dst, size, nbytes)
    speed = nbytes / 1e6 / (time.perf_counter() - start)
    speeds.append(speed)
    print(f"{label(size)} {calls * MIB // nbytes:15} {speed:10.2f}")
print(f"the fastest copy ran {max(speeds) / speeds[0]:.0f} times as fast as the 1-byte copy")
os.close(src)
os.close(dst)
os.unlink("source.bin")
os.unlink("copy.bin")

Output

buffer   calls per MiB       MB/s
     1 B         2097152       0.17
    16 B          131072       2.98
   256 B            8192      53.76
   4 KiB             512     620.56
  64 KiB              32    2573.42
1024 KiB               2    5030.02
the fastest copy ran 29582 times as fast as the 1-byte copy

This output changes from run to run: the speeds depend on the machine and on what else it is running; the counts of system calls do not

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 copy_sizes.py

The calls fall exactly as the buffer grows, from 2,097,152 per mebibyte to 2, and the speed rises as they fall, from well under 1 MB a second to several thousand. With tiny buffers almost all the time is the fixed cost of each call, however few bytes it moves. With large ones copying the bytes dominates: from 64 KiB to 1 MiB the calls fell sixteenfold, the speed far less. This is why the C library buffers: a thousand short printf calls become a few writes.

A call can also differ in how it waits when no data is ready. A blocking call sleeps until it can do its work. A non-blocking call returns at once with the error EAGAIN. An asynchronous call starts the work and reports its completion afterwards.

Key idea

Every read or write pays a fixed cost to enter the kernel, so the buffer size decides how often a copy pays it: two calls per byte with a one-byte buffer, two per mebibyte with a 1 MiB one.

Checkpoint

Check: System calls: the buffer size and how a call waits

  1. Question 1 of 2 How many system calls does copying 1 MiB with read and write and a 4 KiB buffer make?

    Type a number.

    A hint

    Count the buffer-fulls, then remember that each one takes two calls.

    Show the answer: Question 1 of 2

    Answer: 512 calls

    1 MiB / 4 KiB = 256 buffer-fulls, each one read and one write: 512 calls, the number the copy program printed for 4 KiB.

  2. Question 2 of 2 From the 64 KiB buffer to the 1 MiB buffer the calls fall from 32 to 2 per mebibyte, yet the speed grows far less than sixteenfold. Why?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Most of the time now goes on copying the bytes themselves, which no buffer size removes

    Time per mebibyte is the calls times the cost of a call plus the bytes times the cost of copying them. Large buffers shrink the first term until only the second is left.

Zero-copy: the kernel moves the data, the program never touches it

A file server that answers with read and write copies every block twice through its own memory: read copies it from the page cache into the program’s buffer, and write copies it back into the kernel, into a socket’s buffer. The program never looks at the bytes, so both copies are waste. zero-copy: Moving data from one descriptor to another inside the kernel, as sendfile and splice do, so that the bytes never pass through the program's memory. I/O leaves the data in the kernel instead. Step through one block both ways:

Interactive · message by message One block sent to a socket, with read and write, then with sendfile Step through one block of a file on its way to a socket. Count the copies the CPU makes and the system calls.
One block sent to a socket, with read and write, then with sendfile, step 5 of 5. Messages so far: Drive to Kernel: block by DMA; Kernel to Program: read: copy 1; Program to Kernel: write: copy 2; Program to Kernel: sendfile(file, socket).DriveKernelpage cache and socketProgramits own bufferblock by DMAread: copy 1write: copy 2sendfile(file, socket)never touched the bytes

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The block reaches memory. The program calls read. The block is not in the page cache yet, so the drive puts it there by DMA; so far the CPU has copied nothing.
  2. Copy 1: into the program. read then copies the block from the page cache into the program's buffer. This is the first copy the CPU makes, across the line between kernel memory and the program's memory.
  3. Copy 2: back into the kernel. To send the block, the program calls write on the socket, and the CPU copies the same bytes back into the kernel, into the socket's buffer. Two system calls and two copies, and the program never looked at the data.
  4. sendfile instead. With sendfile the program makes one call that names the file and the socket. The kernel moves the block from the page cache to the socket itself; the copying, the manual page says, is done within the kernel.
  5. What it saved. The block never entered the program's memory, so both copies through user space are gone, and one call covers what took two. splice does the same between any two descriptors, as long as one of them is a pipe.

Two calls give zero-copy I/O on Linux. sendfile copies between two descriptors inside the kernel, which its manual page calls “more efficient than the combination of read(2) and write(2)”. splice moves data between two descriptors without copying it between kernel and user space, provided that one of them is a pipe. Sending a file to a socket with splice therefore takes two calls, into a pipe and out of it.

Checkpoint

Check: What zero-copy saves

  1. One question A server sends a file that is already in the page cache to a socket. How often does the CPU copy each byte through the program's memory with read and write, and how often with sendfile?

    Choose one answer.

    Show the answer: One question

    Answer: Twice with read and write, not at all with sendfile

    Zero-copy calls remove the trip through user space. The kernel itself may still move the data from the page cache into the socket's buffer, but the program's memory never holds it.

Practise: predict what a copy costs

Turn the measurements into a model that predicts the calls and the time of a copy. The sample tests include a one-byte buffer and a file that does not fill its last buffer:

Exercise · Easy · Python

Predict the system calls and the time of a copy

A program copies a file with read and write through a buffer of its own: each read fills the buffer (the last one may fill it only partly) and each write writes what the read returned. Write predict(file_bytes, buffer_bytes, call_us, byte_ns) in copy_cost.py. It returns a pair (calls, seconds):

  • calls is the number of system calls: one read and one write for each buffer-full, counting a last, partly filled buffer as a full one. A file of 0 bytes needs none.
  • seconds is the predicted time: every call costs call_us microseconds, and every byte is copied twice, into the program's buffer and back out, at byte_ns nanoseconds a copy.

For example, predict(1_048_576, 1_048_576, 2.0, 0.1) copies 1 MiB in one buffer: 2 calls, which cost 4 µs, and 2 × 1,048,576 copied bytes, which cost about 210 µs, so it returns (2, 0.0002137152).

Starter code · copy_cost.py

def predict(file_bytes, buffer_bytes, call_us, byte_ns):
    """(system calls, seconds) for copying file_bytes with a buffer of buffer_bytes."""
    # Replace this line with your code.
    return 0, 0.0
The sample tests · test_copy_cost.py
import math

from copy_cost import predict


def test_one_buffer_for_the_whole_file():
    """makes one read and one write, and spends most of the time copying"""
    calls, seconds = predict(1_048_576, 1_048_576, 2.0, 0.1)
    assert calls == 2
    assert math.isclose(seconds, 0.0002137152)


def test_a_one_byte_buffer():
    """makes two calls for every byte, so the calls cost far more than the copying"""
    calls, seconds = predict(1_048_576, 1, 2.0, 0.1)
    assert calls == 2_097_152
    assert math.isclose(seconds, 4.1945137152)


def test_a_last_buffer_that_is_not_full():
    """counts a partly filled last buffer as one more read and write"""
    calls, seconds = predict(10_000, 4096, 1.0, 0.0)
    assert calls == 6
    assert math.isclose(seconds, 6e-06)
    calls, seconds = predict(8192, 4096, 1.0, 0.0)
    assert calls == 4
    assert math.isclose(seconds, 4e-06)


def test_an_empty_file():
    """needs no system call and no time"""
    calls, seconds = predict(0, 4096, 5.0, 1.0)
    assert calls == 0
    assert seconds == 0


def test_the_copies_cost_the_same_whatever_the_buffer():
    """charges every byte twice, so with free calls the time does not depend on the buffer"""
    small = predict(1_000_000, 100, 0.0, 0.5)[1]
    large = predict(1_000_000, 1_000_000, 0.0, 0.5)[1]
    assert math.isclose(small, 0.001)
    assert math.isclose(large, 0.001)
A hint

The number of buffers is math.ceil(file_bytes / buffer_bytes), or with whole numbers only (file_bytes + buffer_bytes - 1) // buffer_bytes; each one takes a read and a write. For the time, add calls * call_us / 1e6 and 2 * file_bytes * byte_ns / 1e9.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Then answer the questions below, weakest idea first.

Practice that adapts to you

Practise this lesson

10 questions about 9 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 10 What does a spooler do?

    Choose one answer.

    Show the answer: Question 1 of 10

    Answer: It queues whole jobs on disk for a device that can serve only one job at a time, such as a printer

    A spool lets many programs hand their output to one slow, exclusive device at once, without waiting for it.

  2. Question 2 of 10 A program writes a page of a file and reads it back a second later, before the page has been written to the drive. Where does the read come from?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: The page cache, where the dirty page holds the new data

    This is why one page cache page can be both a buffer and a cache: it holds a write on its way to the drive and answers reads meanwhile.

  3. Question 3 of 10 A program prints one line a second. On a terminal each line appears at once, but redirected to a file the lines arrive in bursts. Why?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: Standard output is fully buffered unless it refers to an interactive device, so with a file the library waits for a full buffer

    A program that must show progress in a file calls fflush after each line, or switches the stream to line buffering with setvbuf.

  4. Question 4 of 10 How many system calls does copying 1 MiB with read, write and a 16-byte buffer make?

    Type a number.

    Show the answer: Question 4 of 10

    Answer: 131072 calls

    1,048,576 / 16 = 65,536 buffer-fulls, each one read and one write: 131,072 calls, as the copy program printed.

  5. Question 5 of 10 Each system call costs 5 µs, and every byte is copied twice at 0.1 ns a copy. How many seconds does copying 1 MiB with a one-byte buffer take?

    Type a number.

    A hint

    Count the calls first; the copying hardly matters here.

    Show the answer: Question 5 of 10

    Answer: 10.49 seconds (anything from 10.44 to 10.54 counts)

    2,097,152 calls × 5 µs = 10.49 s, while copying 2 × 1,048,576 bytes at 0.1 ns adds only 0.0002 s. With a 1 MiB buffer the same model gives 2 × 5 µs plus the same 0.0002 s.

  6. Question 6 of 10 Why not use a 1 GiB buffer for every copy?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: Past a mebibyte or so the calls are already rare, so a larger buffer saves almost nothing and costs memory

    The measured speeds rose steeply up to tens of kilobytes and then far more slowly than the calls fell; buffers of 64 KiB to 1 MiB get nearly all of the gain.

  7. Question 7 of 10 Which kind of call returns at once with the error EAGAIN when it would have to wait?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: A non-blocking call

    Blocking calls wait, non-blocking calls refuse with EAGAIN, and asynchronous calls accept the work and report completion later.

  8. Question 8 of 10 What does sendfile do?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: It copies data from one descriptor to another inside the kernel

    A file server that only passes a file on to a socket gains most: two copies and nearly all the system calls go away.

  9. Question 9 of 10 A server sends a 1 MiB file through a 64 KiB buffer with read and write. Counting the last read, which returns 0, how many system calls does it make?

    Type a number.

    A hint

    Count the pieces first.

    Show the answer: Question 9 of 10

    Answer: 33 calls

    1 MiB / 64 KiB = 16 pieces, each a read and a write, plus the final read that returns 0: 33 calls. With sendfile, one call can send the whole file.

  10. Question 10 of 10 Why does sending a file to a socket with splice take two calls?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: One of splice's two descriptors must be a pipe, so the data goes from the file into a pipe and then from the pipe into the socket

    splice works between any two descriptors as long as one is a pipe, which is how it generalises sendfile's zero-copy to sockets on both sides.

The page cache, modelled and measured Pro: opens with a Pro, Premium or Ultimate pass

Reads that find their pages in memory skip the drive, and writes return before the drive has them. This part builds a page cache with write-back in code, measures cached reads and the price of making a write durable on the recorded Mac, and shows the calls that keep data out of the cache.

In this part:

  • A page cache in code: hits, dirty pages and write-back
  • Cached reads and the price of fsync, measured

2 runnable examples (Python) · about 10 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Blocking, non-blocking and asynchronous calls compared Pro: opens with a Pro, Premium or Ultimate pass

A call that cannot be served at once has three choices: wait, refuse, or accept the job and report later. This part runs the refusal on a real pipe, compares the three, and shows why the asynchronous interfaces POSIX defined gave way to newer ones.

In this part:

  • What each call does when the data is not ready
  • Asynchronous I/O: POSIX AIO, then a ring shared with the kernel

1 runnable example (C) · about 5 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

sendfile measured against read and write, and splice Pro: opens with a Pro, Premium or Ultimate pass

Saving two copies sounds small until a server sends files all day. This part measures sendfile against read and write on the recorded Mac, counts what each saves, and shows how splice gets the same result through a pipe on Linux.

In this part:

  • sendfile against read and write, measured
  • splice: zero-copy through a pipe

1 diagram · 1 runnable example (Python) · about 6 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Interview questions

  1. Fresher · Warm-up

    What is the difference between buffering and caching in an operating system's I/O?

    A buffer holds data once, on its way between two parts that work at different speeds or in different sizes; a cache keeps a copy of data because it may be needed again. The C library's output buffer is a buffer: it collects small prints and hands them to the kernel in one write, and the data is gone from it afterwards. The kernel's page cache is a cache: pages read from a file stay in memory, so a second read never touches the drive. The two often meet in one place, since a dirty page in the page cache buffers a write on its way to the drive and answers reads of the same data meanwhile. A third idea, spooling, queues whole jobs on disk for a device that serves one at a time, as a print scheduler does.

  2. Fresher to mid

    A backup tool copies files with read and write 512 bytes at a time, and a 10 GB file takes far longer than the drive's speed allows. A colleague proposes a 1 MiB buffer. Predict the effect, explain where the time goes, and say when a larger buffer stops helping.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    A service appends each order to a log file and replies "saved" as soon as write returns. After a power cut, the last few seconds of orders are missing. Explain why, and how you would make every order durable without making each request wait a full trip to the drive.

    Model answer, Pro. First step and follow-ups

  4. Mid to senior

    A file server sends large static files with read and write, and runs out of CPU long before its network link is full. How would you cut the CPU it spends per byte, what exactly does sendfile save, and what can stop zero-copy from applying?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A buffer holds data once on its way between two parts that work at different speeds or sizes, a cache keeps a copy for the next access, and a spool queues whole jobs for a device that serves one at a time.
  • Every read or write pays a fixed cost, so the buffer size sets the cost of a copy: 2,097,152 calls per mebibyte with a one-byte buffer against 2 with a 1 MiB buffer, and tens of thousands of times the speed.
  • Zero-copy calls such as sendfile and splice move data between descriptors inside the kernel, so the program’s memory never holds a copy of it.

Terms in this lesson

buffer
Memory that holds data once on its way between a producer and a consumer that work at different speeds or in different sizes, such as the C library's output buffer.
dirty page
A page in memory that has been written since it was read from the drive, so the drive's copy is out of date until the page is written back.
page cache
The kernel's cache of file data in memory: pages read from a file stay there for later reads, and written pages wait there before they reach the drive.
spooling
Queuing whole jobs on disk for a device that can serve only one job at a time, as a print scheduler keeps print jobs in its spool directory.
zero-copy
Moving data from one descriptor to another inside the kernel, as sendfile and splice do, so that the bytes never pass through the program's memory.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.