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 tomemcpy-into-userspace is a layered descent: the syscall lands invfs_read, which dispatches through the file’sf_op->read_iterto the per-filesystem read routine, which for nearly every disk filesystem is the genericgeneric_file_read_iter→filemap_readmachinery inmm/filemap.c. That code looks the requested byte range up in the cache’sXArray; 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_read → read_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_modeforFMODE_READ(the descriptor was opened readable) andFMODE_CAN_READ(the file type supports reading), returning-EBADF/-EINVALotherwise. - It validates the user buffer with
access_ok(buf, count)(returning-EFAULTfor a bad pointer) and runsrw_verify_area, which clamps againstMAX_RW_COUNTand 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 onlyread_iter(the iterator-based interface that takes astruct iov_iter); the older->readcallback 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:
- Calls
cond_resched()so a large read does not hog the CPU. - Bails early with
0ifki_posis already at or pasti_size(end of file) — this is how a read past EOF returns0bytes. - Calls
filemap_get_pages(iocb, iter->count, &fbatch, false)to fill a folio batch (struct folio_batch, an array of up toPAGEVEC_SIZE= 31 folio pointers, perinclude/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. - Re-reads
i_sizeafter the folios are known up-to-date, so a concurrent truncate is observed correctly and the zero-fill boundaryend_offsetis computed precisely. - Iterates the returned folios, and for each computes the in-folio
offset(iocb->ki_pos & (fsize - 1)) and the number ofbytesto copy, then callscopied = copy_folio_to_iter(folio, offset, bytes, iter). This is the actual data movement into userspace. After each copy it advances bothalready_read += copiedandiocb->ki_pos += copied— updating the file position as it goes. - Drops its references with
folio_puton 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:
-
First lookup (the hit path).
filemap_get_read_batch(mapping, index, last_index - 1, fbatch)walks theaddress_space’sXArray(mapping->i_pages) underrcu_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 infilemap_get_pagesdoes any real work. The lookup is lockless — it usesfolio_try_getand re-validates withxas_reloadto tolerate concurrent eviction, the same RCU discipline described in Address Space and the Page Cache XArray. -
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 callspage_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_readaheadallocates 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.” -
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_folioallocates exactly one folio, inserts it under the mapping’sinvalidate_lock(held shared to serialize against truncate/hole-punch), and callsfilemap_read_folio→mapping->a_ops->read_folio(file, folio)— the filesystem’s single-folio reader, which issues thebioand unlocks the folio on completion. TheAOP_TRUNCATED_PAGEreturn means “the folio was truncated mid-flight; start over,” and the codegoto retrys the whole function. -
The marker → asynchronous readahead. After the batch is non-empty, the code inspects the last folio in it:
if (folio_test_readahead(folio)). A setPG_readaheadflag 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, sofilemap_readaheadfirespage_cache_async_rato 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, thePG_readaheadflag “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_rafirst callsfolio_clear_readahead(folio)so the marker fires exactly once. -
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_pagelocks the folio and waits for the read to complete (or, underIOCB_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_NOWAITreturns-EAGAIN, not data. Ifpreadv2is called withRWF_NOWAITand the range is not fully cached,filemap_get_pagesshort-circuits to-EAGAINthe moment it would have to issue or wait on I/O. Callers that do not expect-EAGAINon 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_sizereturns the bytes available and then0; thei_size_readre-check inside the loop is what makes a concurrent truncate produce a correct short read rather than leaking stale page tail bytes. read_folioreturning success but folio not up-to-date →-EIO.filemap_read_foliowaits for the folio lock, then checksfolio_test_uptodate; if the filesystem’s reader cleared the lock but failed to mark the folio up-to-date, the read fails-EIOandshrink_readahead_size_eioshrinks 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)setsFMODE_RANDOM, which routes throughpage_cache_sync_ra’sdo_forced_rabranch and reads exactly the requested range. This is the read-path counterpart to the tuning discussed in Readahead. PG_readaheadaliasesPG_reclaim. Because the marker bit is reused for reclaim, a folio under writeback can look “marked”; the async path’sfolio_test_writebackguard is not optional. This is a classic source of subtle bugs when adding new page-flag consumers.
Alternatives and When the Path Differs
O_DIRECTskipsfilemap_readentirely via theIOCB_DIRECTfork ingeneric_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 callsfilemap_fault, which has its own readahead (do_sync_mmap_readahead) but ultimately populates the very sameaddress_space. Reads viaread()and via a mapping of the same file are coherent because they share the page cache.splice/sendfilemove data between the page cache and a pipe without copying to userspace; they reuse the same lookup-and-readahead front end (filemap_splice_readcallsfilemap_get_pages).io_uringbuffered reads funnel through the samefilemap_read, but submit via the ring and lean heavily onIOCB_NOWAITto 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
->readis fully absent from modern disk filesystems. Reason: not exhaustively audited across the v6.12 tree — some pseudo/char devices and special files still wire->readdirectly (thevfs_readdispatch triesf_op->readfirst, thenread_iter). To resolve: confirm against the relevantfile_operationstables for the filesystems of interest. (Thefolio_batchcapacity is resolved:PAGEVEC_SIZE= 31 ininclude/linux/pagevec.hat v6.12.) uncertain
See Also
- Readahead — the readahead window-growth algorithm (initial size, ramp-up, max window,
FMODE_RANDOM); this note defers all sizing heuristics there. - The Page Cache — the in-memory mirror this path reads from and populates.
- Address Space and the Page Cache XArray — the
mapping->i_pagesXArraythatfilemap_get_read_batchwalks. - Folios and the Folio Conversion — why the batch holds folios, not bare pages, and how
folio_size/folio_poswork. - Direct IO and O_DIRECT — the
O_DIRECTfork that skips this entire path. - The Page Cache and address_space · Folios and the Page Cache — sibling page-cache leaves (MOC titles).
- Dirty Pages and Writeback — the write-side counterpart to this read path.
- The Virtual File System Layer · VFS Operation Tables · VFS File Object — the
f_op->read_iterdispatch. - MOC: Linux Filesystems and VFS MOC