Readahead and Read Path

When a process calls read(fd, buf, count) on an ordinary file, the bytes almost never come straight off the disk. They come from the page cache — the kernel’s in-memory mirror of file data — and the journey from the syscall to memcpy-into-userspace is a layered descent: the syscall lands in vfs_read, which dispatches through the file’s f_op->read_iter to the per-filesystem read routine, which for nearly every disk filesystem is the generic generic_file_read_iterfilemap_read machinery in mm/filemap.c. That code looks the requested byte range up in the cache’s XArray; a hit copies cached bytes straight out, while a miss kicks off readahead and a block read to fill the cache before copying. This note traces that buffered read path end to end against Linux 6.12 LTS (mm/filemap.c, v6.12), and explains how the path decides to read ahead. The window-growth heuristics themselves — how big the next readahead chunk should be — live in Readahead; this note owns the VFS-to-page-cache plumbing and the trigger.

Mental Model

Think of the buffered read path as a three-stage funnel. At the top, VFS is a thin, filesystem-agnostic shim: it validates the descriptor and the buffer, then immediately hands off to a function pointer. In the middle, filemap_read is the generic page-cache reader shared by ext4, XFS, Btrfs, F2FS and most others — it owns the loop that turns a byte range into a batch of cached folios and copies them out. At the bottom, on a cache miss, the path drops into readahead and the filesystem’s read_folio operation, which is where an actual block I/O (bio) is born and the block layer takes over.

The single most important idea is that the read path is optimistic about the cache. It is structured so that the common case — a sequential reader walking a file that is already partly cached — is almost branch-free: look up a batch of folios, copy, advance, repeat. Only when the lookup comes back empty (or finds a folio flagged for follow-up readahead) does the slow machinery engage.

flowchart TD
  A["read() / pread64()<br/>(syscall)"] --> B["ksys_read → vfs_read"]
  B -->|"f_op->read_iter"| C["generic_file_read_iter"]
  C -->|"O_DIRECT? → bypass"| DIO["direct_IO path<br/>(see Direct IO note)"]
  C -->|"buffered"| D["filemap_read"]
  D --> E["filemap_get_pages"]
  E --> F["filemap_get_read_batch<br/>(XArray lookup, RCU)"]
  F -->|"hit: folios found"| H["copy_folio_to_iter<br/>→ userspace"]
  F -->|"miss: empty batch"| G["page_cache_sync_readahead<br/>then retry lookup"]
  G -->|"still empty"| I["filemap_create_folio<br/>→ read_folio (1 folio)"]
  E -->|"last folio has<br/>PG_readahead set"| J["page_cache_async_ra<br/>(refill window ahead)"]
  H --> K["update f_pos (ki_pos),<br/>folio_mark_accessed"]

The buffered read funnel. What it shows: the syscall descends through VFS into filemap_read, whose inner engine filemap_get_pages first tries a lockless XArray lookup; a miss triggers synchronous readahead and, if even that fails to populate the cache, a single-folio read_folio; meanwhile a folio carrying the PG_readahead marker triggers asynchronous readahead for the window ahead. The insight to take: there are two distinct readahead triggers on this path — a synchronous one on an outright miss (we must wait), and an asynchronous one fired off a marker so the next window is already in flight by the time the reader reaches it.

Mechanical Walk-through

Stage 1 — the VFS shim: read()vfs_readread_iter

A read(2) syscall enters ksys_read, which resolves the file descriptor to a struct file and then calls vfs_read(file, buf, count, pos) (fs/read_write.c, v6.12). pread64(2) follows the same route but with an explicit offset and the file’s own f_pos left untouched. vfs_read does only generic, filesystem-independent work:

  • It checks file->f_mode for FMODE_READ (the descriptor was opened readable) and FMODE_CAN_READ (the file type supports reading), returning -EBADF / -EINVAL otherwise.
  • It validates the user buffer with access_ok(buf, count) (returning -EFAULT for a bad pointer) and runs rw_verify_area, which clamps against MAX_RW_COUNT and checks for mandatory locks / overflow.
  • It then dispatches: if (file->f_op->read) … else if (file->f_op->read_iter) ret = new_sync_read(...). Modern filesystems supply only read_iter (the iterator-based interface that takes a struct iov_iter); the older ->read callback is essentially gone from disk filesystems.

new_sync_read is the adapter that turns the plain (buf, len) of a synchronous read() into the iterator world: it builds an on-stack struct kiocb (kernel I/O control block) with init_sync_kiocb, points kiocb.ki_pos at the current offset, wraps the user buffer in a single-segment iov_iter via iov_iter_ubuf(&iter, ITER_DEST, buf, len), and calls filp->f_op->read_iter(&kiocb, &iter). Because this is a synchronous read it asserts BUG_ON(ret == -EIOCBQUEUED) — a sync read() may never be left pending. On return it writes the updated kiocb.ki_pos back into *ppos, which is how the file’s offset advances. After the read completes, vfs_read calls fsnotify_access(file) (firing inotify/fanotify access events) and bumps the task’s read counters.

This is the crux of the VFS abstraction (see The Virtual File System Layer, VFS Operation Tables, and the kernel’s VFS documentation): VFS never knows it is reading ext4 or XFS. It calls through f_op->read_iter, a function pointer the filesystem installed when it built the struct file. For almost every block-backed filesystem that pointer is generic_file_read_iter.

Stage 2 — generic_file_read_iter: the buffered vs direct fork

generic_file_read_iter (mm/filemap.c, v6.12) is the documented “read_iter() routine for all filesystems that can use the page cache directly.” Its first job is to split off the O case: if iocb->ki_flags & IOCB_DIRECT (the file was opened O_DIRECT), it writes back and waits on any dirty cached pages over the range (kiocb_write_and_wait) and then calls mapping->a_ops->direct_IO(iocb, iter), bypassing the page cache entirely. Note that even a successful O_DIRECT read can fall through to buffered I/O for the remainder of the request — Btrfs, for instance, returns a short direct read on compressed extents, and the code then continues into filemap_read for the rest. For the normal buffered case it simply tail-calls return filemap_read(iocb, iter, retval).

Stage 3 — filemap_read: the copy-out loop

filemap_read(iocb, iter, already_read) is the heart of the buffered read. It pulls a few cached pointers up front — the file’s readahead state ra = &filp->f_ra, the address_space *mapping = filp->f_mapping, and the inode — then runs an outer do { … } while loop that repeats until the iterator is drained or end-of-file is reached. Each iteration:

  1. Calls cond_resched() so a large read does not hog the CPU.
  2. Bails early with 0 if ki_pos is already at or past i_size (end of file) — this is how a read past EOF returns 0 bytes.
  3. Calls filemap_get_pages(iocb, iter->count, &fbatch, false) to fill a folio batch (struct folio_batch, an array of up to PAGEVEC_SIZE = 31 folio pointers, per include/linux/pagevec.h, v6.12) covering the start of the remaining range. This is where all cache lookups, readahead, and on-demand reads happen — detailed below.
  4. Re-reads i_size after the folios are known up-to-date, so a concurrent truncate is observed correctly and the zero-fill boundary end_offset is computed precisely.
  5. Iterates the returned folios, and for each computes the in-folio offset (iocb->ki_pos & (fsize - 1)) and the number of bytes to copy, then calls copied = copy_folio_to_iter(folio, offset, bytes, iter). This is the actual data movement into userspace. After each copy it advances both already_read += copied and iocb->ki_pos += copiedupdating the file position as it goes.
  6. Drops its references with folio_put on every folio in the batch (each was pinned by the lookup) and re-inits the batch for the next pass.

Two subtleties worth flagging. First, access marking for LRU: filemap_read calls folio_mark_accessed on the first folio only the first time a given position is touched (it compares against ra->prev_pos via pos_same_folio), so a read that re-touches the same folio does not double-count it toward the active LRU list — this feeds page-cache reclaim decisions. Second, cache aliasing: if the file is also mmap-ed writably (mapping_writably_mapped), it calls flush_dcache_folio before copying, to flush CPU data caches on architectures with virtually-indexed caches so the kernel sees the latest user writes. On exit it records ra->prev_pos = last_pos (so the next read can detect sequential access) and calls file_accessed(filp) to update atime.

Stage 4 — filemap_get_pages: hit, miss, and the two readahead triggers

This function decides whether bytes are already cached and, if not, gets them there. It computes the page indices spanned by the request — index = ki_pos >> PAGE_SHIFT up to last_index = DIV_ROUND_UP(ki_pos + count, PAGE_SIZE) — and then:

  1. First lookup (the hit path). filemap_get_read_batch(mapping, index, last_index - 1, fbatch) walks the address_space’s XArray (mapping->i_pages) under rcu_read_lock(), gathering a contiguous run of present, up-to-date folios into the batch. This is the fast path; on a fully-cached sequential read it returns folios immediately and nothing else in filemap_get_pages does any real work. The lookup is lockless — it uses folio_try_get and re-validates with xas_reload to tolerate concurrent eviction, the same RCU discipline described in Address Space and the Page Cache XArray.

  2. The miss → synchronous readahead. If the batch comes back empty, the requested data is not cached. Unless the caller forbade I/O (IOCB_NOIO-EAGAIN), the code calls page_cache_sync_readahead(mapping, ra, filp, index, last_index - index) and then re-runs the batch lookup. This is the synchronous readahead trigger: it is “synchronous” only in the sense that the reader must wait for these pages — page_cache_sync_readahead allocates folios, inserts them into the cache, and submits the read I/O for the requested range plus a speculative window ahead. The size of that window is the Readahead algorithm’s job; the trigger here just says “we missed, go fetch.”

  3. Single-folio fallback. If even after readahead the batch is still empty (e.g. readahead was throttled, or the folio was truncated out from under us), and the caller allows blocking, filemap_create_folio allocates exactly one folio, inserts it under the mapping’s invalidate_lock (held shared to serialize against truncate/hole-punch), and calls filemap_read_foliomapping->a_ops->read_folio(file, folio) — the filesystem’s single-folio reader, which issues the bio and unlocks the folio on completion. The AOP_TRUNCATED_PAGE return means “the folio was truncated mid-flight; start over,” and the code goto retrys the whole function.

  4. The marker → asynchronous readahead. After the batch is non-empty, the code inspects the last folio in it: if (folio_test_readahead(folio)). A set PG_readahead flag is the readahead marker — a single folio in each readahead window is tagged when the window is created. Reaching the marked folio means the reader has consumed most of the current window and is about to run out, so filemap_readahead fires page_cache_async_ra to refill the next window now, while the reader keeps copying from the folios it already has. As Neil Brown’s readahead write-up puts it, the PG_readahead flag “indicates that the page was loaded as part of a previous readahead request and now that it has been accessed, it is time for the next read-ahead” (Brown, LWN 2022). This is the asynchronous trigger: I/O is launched, but the reader does not wait on it — by the time it walks off the end of the current batch, the next window is (ideally) already arriving. Unlike the sync path, page_cache_async_ra first calls folio_clear_readahead(folio) so the marker fires exactly once.

  5. Not-up-to-date wait. Finally, if the last folio is present but not yet Uptodate (its I/O is still in flight — perhaps a readahead we just issued), filemap_update_page locks the folio and waits for the read to complete (or, under IOCB_NOWAIT, returns -EAGAIN). This is “where we usually end up waiting for a previously submitted readahead to finish,” per the code comment.

The readahead marker, concretely

The marker mechanism is the seam between this note and Readahead. When page_cache_sync_ra / page_cache_ra_order build a window of ra->size folios with an async_size lookahead tail, they call folio_set_readahead on the folio sitting at offset ra->size - ra->async_size into the window (mm/readahead.c, v6.12, ra_alloc_folio: if (index == mark) folio_set_readahead(folio)). PG_readahead shares a page-flag bit with PG_reclaim, which is why page_cache_async_ra checks folio_test_writeback and bails if the folio is actually under writeback — the same bit means different things depending on context. When the read path later trips that marker, page_cache_async_ra advances the window (ra->start += ra->size, ra->size = get_next_ra_size(...)) and lays down a new marker further ahead. The values of get_init_ra_size / get_next_ra_size — the ramp-up from the first 4 KiB miss to a multi-megabyte window — are the Readahead note’s territory.

Code Walk: the inner lookup-and-fetch loop

The condensed shape of filemap_get_pages (mm/filemap.c, v6.12), annotated:

static int filemap_get_pages(struct kiocb *iocb, size_t count,
                struct folio_batch *fbatch, bool need_uptodate)
{
    struct file *filp = iocb->ki_filp;
    struct address_space *mapping = filp->f_mapping;
    struct file_ra_state *ra = &filp->f_ra;
    pgoff_t index = iocb->ki_pos >> PAGE_SHIFT;          // first page index
    pgoff_t last_index = DIV_ROUND_UP(iocb->ki_pos + count, PAGE_SIZE);
    struct folio *folio;
retry:
    if (fatal_signal_pending(current))                    // killable read
        return -EINTR;
 
    filemap_get_read_batch(mapping, index, last_index - 1, fbatch); // (1) hit path
    if (!folio_batch_count(fbatch)) {                     // MISS
        if (iocb->ki_flags & IOCB_NOIO)                   // RWF_NOWAIT-ish: refuse I/O
            return -EAGAIN;
        page_cache_sync_readahead(mapping, ra, filp,      // (2) SYNC readahead
                                  index, last_index - index);
        filemap_get_read_batch(mapping, index, last_index - 1, fbatch); // re-lookup
    }
    if (!folio_batch_count(fbatch)) {                     // STILL empty
        if (iocb->ki_flags & (IOCB_NOWAIT | IOCB_WAITQ))
            return -EAGAIN;
        err = filemap_create_folio(filp, mapping,         // (3) one-folio read_folio
                                   iocb->ki_pos, fbatch);
        if (err == AOP_TRUNCATED_PAGE)
            goto retry;                                   // truncated mid-flight
        return err;
    }
 
    folio = fbatch->folios[folio_batch_count(fbatch) - 1]; // last folio in batch
    if (folio_test_readahead(folio)) {                    // (4) ASYNC marker tripped
        err = filemap_readahead(iocb, filp, mapping, folio, last_index);
        if (err) goto err;
    }
    if (!folio_test_uptodate(folio)) {                    // (5) wait for in-flight I/O
        err = filemap_update_page(iocb, mapping, count, folio, need_uptodate);
        if (err) goto err;
    }
    return 0;
}

Line (1) is the hot path: one RCU-protected XArray walk. Line (2) is the only place a missing range triggers I/O on a normal read, and it is synchronous from the reader’s point of view. Line (3) is the rare degenerate single-folio fallback. Line (4) is the lookahead engine that keeps a sequential reader fed. Line (5) is the block where the reader actually sleeps on disk I/O.

How f_pos advances

There is no single “update the offset” statement. For a plain read(), new_sync_read seeds kiocb.ki_pos from *ppos; filemap_read advances iocb->ki_pos += copied after every copy_folio_to_iter; and on return new_sync_read writes the final kiocb.ki_pos back into *ppos, which ksys_read stores into the file’s f_pos. For pread64() the kernel uses a temporary offset and never touches f_pos — that is the entire difference between read and pread at this layer. Because the offset lives in the shared struct file (the open file description), two descriptors that share it via dup see each other’s advances.

Failure Modes and Gotchas

  • RWF_NOWAIT / IOCB_NOWAIT returns -EAGAIN, not data. If preadv2 is called with RWF_NOWAIT and the range is not fully cached, filemap_get_pages short-circuits to -EAGAIN the moment it would have to issue or wait on I/O. Callers that do not expect -EAGAIN on a regular file are surprised by this; it is by design for latency-sensitive event loops. See Asynchronous IO Models in Linux.
  • Short reads at EOF, not errors. Reading past i_size returns the bytes available and then 0; the i_size_read re-check inside the loop is what makes a concurrent truncate produce a correct short read rather than leaking stale page tail bytes.
  • read_folio returning success but folio not up-to-date → -EIO. filemap_read_folio waits for the folio lock, then checks folio_test_uptodate; if the filesystem’s reader cleared the lock but failed to mark the folio up-to-date, the read fails -EIO and shrink_readahead_size_eio shrinks the window so the kernel does not keep over-reading a flaky device.
  • Readahead thrashing on random reads. If a workload is actually random but the heuristic mistakes it for sequential, every readahead window is wasted I/O and cache pressure. posix_fadvise(POSIX_FADV_RANDOM) sets FMODE_RANDOM, which routes through page_cache_sync_ra’s do_forced_ra branch and reads exactly the requested range. This is the read-path counterpart to the tuning discussed in Readahead.
  • PG_readahead aliases PG_reclaim. Because the marker bit is reused for reclaim, a folio under writeback can look “marked”; the async path’s folio_test_writeback guard is not optional. This is a classic source of subtle bugs when adding new page-flag consumers.

Alternatives and When the Path Differs

  • O_DIRECT skips filemap_read entirely via the IOCB_DIRECT fork in generic_file_read_iter, trading the page cache (and readahead) for DMA straight into the user buffer — see Direct IO and O_DIRECT.
  • mmap + page fault reaches the same folios through a different door: a fault calls filemap_fault, which has its own readahead (do_sync_mmap_readahead) but ultimately populates the very same address_space. Reads via read() and via a mapping of the same file are coherent because they share the page cache.
  • splice / sendfile move data between the page cache and a pipe without copying to userspace; they reuse the same lookup-and-readahead front end (filemap_splice_read calls filemap_get_pages).
  • io_uring buffered reads funnel through the same filemap_read, but submit via the ring and lean heavily on IOCB_NOWAIT to stay non-blocking on the cache-hit fast path, punting misses to a worker — see io_uring and the File Path.

Production Notes

The read path’s design assumptions show up directly in observable behavior. The reason a grep -r over a warm tree is near-instant is that filemap_get_read_batch is serving entirely from the XArray with zero block I/O; the reason the first pass over a cold large file ramps from slow to fast is the readahead window growing from the initial 4 KiB miss toward the device-limited maximum as the async marker keeps tripping. Tools like vmtouch and /proc/<pid>/io (the rchar/read_bytes split — add_rchar counts bytes copied to userspace, while read_bytes counts bytes actually fetched from the block device) let you see the cache-hit ratio: a sequential warm read shows rchar rising with read_bytes flat at zero. The mm_filemap_get_pages tracepoint (emitted at the end of filemap_get_pages) and readahead/mm_filemap_add_to_page_cache tracepoints let you watch the trigger logic live with trace-cmd or bpftrace.

Uncertain

Verify: the claim that ->read is fully absent from modern disk filesystems. Reason: not exhaustively audited across the v6.12 tree — some pseudo/char devices and special files still wire ->read directly (the vfs_read dispatch tries f_op->read first, then read_iter). To resolve: confirm against the relevant file_operations tables for the filesystems of interest. (The folio_batch capacity is resolved: PAGEVEC_SIZE = 31 in include/linux/pagevec.h at v6.12.) uncertain

See Also