The Page Cache
The page cache is the kernel’s in-RAM cache of file contents — the layer that makes a second
read()of a file return instantly instead of going to disk, and that lets awrite()return the moment the data is in memory rather than after it reaches the platter. It is memory management’s great connector: it sits between the VFS above and the block layer below, so that “whenever a file is read, the data is put into the page cache to avoid expensive disk access on the subsequent reads” and “when one writes to a file, the data is placed in the page cache and eventually gets into the backing storage device” (kernel MM concepts, v6.12). Almost every byte of buffered file I/O on a Linux system passes through it. This note is the memory-management view: what is and is not cached, how the read and write paths populate it, how dirty data is accounted and throttled, how reclaim takes memory back, what/proc/meminfois really telling you, and when to bypass the cache withO_DIRECTor steer it withposix_fadvise. It deliberately does not explain how a filesystem is wired into the cache — thestruct address_spaceobject, thei_mappingbinding, and theaddress_space_operationsvtable are the subject of its sibling The Page Cache and address_space.
This note pins every claim to Linux 6.12, which is a maintained long-term-support (LTS) release (6.12 was released 2024-11-17; the 6.12.y stable series is still shipping). Mainline has moved on — the v7.0, v7.1 and v7.2 tags all exist in Torvalds’ tree and were checked while writing this — so where something material changed after 6.12 it is called out with the version that changed it rather than silently folded in. All source was read at the v6.12 tag via raw.githubusercontent.com; line numbers cited are line numbers in those files.
Scope — This Note, and Its Three Siblings
Four notes in this vault describe the page cache, and they are deliberately distinct rather than merged. The split is by question asked, not by topic, and the fastest way to pick one is to notice what kind of answer you want.
flowchart TB Q{"What are you<br/>actually asking?"} Q -->|"'How much memory is this using,<br/>why is it there, when does it go away?'"| A["<b>The Page Cache</b><br/>(this note — MM view)<br/>meminfo · dirty limits · reclaim<br/>readahead policy · O_DIRECT · fadvise"] Q -->|"'Which function does my filesystem<br/>implement, and what must it guarantee?'"| B["<b>The Page Cache and address_space</b><br/>(VFS view)<br/>i_mapping/i_data · a_ops vtable<br/>method contracts · locking matrix"] Q -->|"'How is a cached offset found,<br/>and where do the dirty bits live?'"| C["<b>Address Space and<br/>the Page Cache XArray</b><br/>(data-structure view)<br/>i_pages · marks · shadow entries"] Q -->|"'What exactly is the unit<br/>the cache stores?'"| D["<b>Folios and the Page Cache</b><br/>(unit-of-I/O view)<br/>filemap_get_folio · FGP flags<br/>large-folio sizing"]
Which of the four page-cache notes answers which question. What it shows: the same subsystem cut four ways — by memory behaviour (this note), by the filesystem-facing interface, by the indexing structure, and by the unit of storage. The insight to take: the boundary between this note and The Page Cache and address_space is the one people trip over, and it is simply memory versus code. If the noun in your question is a number of kilobytes, a threshold, an eviction or a stall, you are in this note. If the noun is a function pointer, a lock, or a filesystem method, you are in the other one. Neither note re-derives the other; they cross-link.
| If you want to know… | Read |
|---|---|
Why Cached is 40 GiB and whether that is a problem | this note, Reading /proc/meminfo |
Why your writer process is being paused mid-write() | this note, Dirty thresholds |
Whether O_DIRECT really ignores the cache | this note, Bypassing the cache |
What ext4 has to implement to participate | The Page Cache and address_space |
Which lock is held when ->read_folio is called | The Page Cache and address_space |
| How offset 8192 of a file is looked up in O(log n) | Address Space and the Page Cache XArray |
| Why a 64 KiB folio is cheaper than sixteen 4 KiB ones | Folios and the Page Cache |
Mental Model — Free RAM Is Wasted RAM (Honestly Stated)
The single most important idea is the kernel’s posture toward unused memory: it does not leave RAM idle, it fills it with cached file data, and reclaims that cache on demand when something needs the memory more. Cached file pages are reclaimable — the kernel documentation defines the term precisely: pages “that can be freed at any time, either because they cache the data available elsewhere, for instance, on a hard disk, or because they can be swapped out” are reclaimable, and “the most notable categories of the reclaimable pages are page cache and anonymous memory” (MM concepts, v6.12). A clean cached folio duplicates bytes that also exist on disk, so it can be dropped at any instant for free; a dirty one must be written back first. This is why a freshly-booted Linux box quickly shows most of its RAM accounted to Cached: that is not memory pressure, it is the cache doing its job.
The slogan is true and it is also routinely over-applied, so state it precisely: free RAM is wasted RAM, but not all Cached is free-on-demand. Three concrete exceptions, each developed later in this note: dirty folios cost a disk write before they can be freed; tmpfs/shmem folios count under Cached but have no filesystem backing at all, so they can only be swapped, never dropped; and mlocked or otherwise pinned file pages are counted as file-backed but are unevictable. A dashboard that renders “used = MemTotal − MemFree” panics at idle; a dashboard that renders “available = MemFree + Cached” under-reports pressure on a tmpfs-heavy or dirty-heavy box. The kernel already computes the honest number for you, and it is MemAvailable.
flowchart TB app["Application: read() / write() / mmap()"] vfs["VFS layer<br/>(generic_file_read_iter, write_iter)"] pc["PAGE CACHE<br/>per-inode address_space → i_pages<br/>(folios keyed by file offset)"] hit{"folio<br/>present and<br/>uptodate?"} block["Block layer<br/>(submit bio to device)"] reclaim["Reclaim / LRU<br/>drops clean, defers dirty"] wb["Writeback<br/>(bdi flusher threads)"] app --> vfs --> pc --> hit hit -->|"HIT: copy from RAM, no I/O"| app hit -->|"MISS: readahead or read_folio"| block --> pc pc -->|"dirty folios, deferred"| wb --> block reclaim -->|"reclaim under pressure"| pc
The page cache as the hinge of buffered file I/O. What it shows: reads and writes from userspace go through the VFS into the page cache; a read hit is a pure RAM copy, a miss triggers a block-layer read that fills the cache, and writes dirty cached folios that the writeback machinery flushes later. Reclaim pulls folios out of the cache under memory pressure. The insight to take: the page cache is the only place in the system where the VFS, the block layer, and the memory-reclaim machinery all meet — virtually every I/O path threads through this one cache, which is why getting it right is so high-leverage, and why a page-cache problem can present as a filesystem problem, a disk problem, or a memory problem depending on where you are standing.
What the Page Cache Caches — and What It Does Not
The page cache holds the contents of file-backed memory: regular files on disk filesystems, tmpfs/shmem (memory-only filesystems whose “backing store” is swap), and the raw contents of block devices (the historical buffer cache, now unified into the page cache). What it does not hold is anonymous memory — heap, stack, MAP_ANONYMOUS mappings — which has no file behind it. The kernel documentation’s definition is the one to keep: anonymous memory “is not backed by a filesystem… implicitly created for program’s stack and heap or by explicit calls to mmap(2)”, and when the kernel repurposes a dirty anonymous page, “the dirty page will be swapped out” rather than written back to a file (MM concepts, v6.12). This file-versus-anonymous fork is the fundamental split in reclaim policy; see Anonymous vs File-Backed Memory.
flowchart TB RAM["All physical RAM<br/>(MemTotal)"] RAM --> FILE["<b>File-backed</b> — has a backing<br/>address_space; reclaim = write back<br/>if dirty, then drop"] RAM --> ANON["<b>Anonymous</b> — no file behind it;<br/>reclaim = swap out"] RAM --> UNREC["<b>Unreclaimable</b> — pinned kernel<br/>data, DMA buffers, page tables"] FILE --> F1["disk files<br/>→ Cached"] FILE --> F2["block devices<br/>→ Buffers"] FILE --> F3["tmpfs / shmem<br/>→ Cached AND Shmem<br/>(dropping impossible: swap only)"] ANON --> A1["heap / stack / MAP_ANONYMOUS<br/>→ AnonPages"] ANON --> A2["already written to swap,<br/>still resident → SwapCached<br/>(NOT page cache)"] UNREC --> U1["slab, page tables,<br/>mlocked → Unevictable"]
The three-way classification of physical memory, and which /proc/meminfo counter reports each. What it shows: the page cache is exactly the “file-backed” branch — disk files, block devices and tmpfs — while anonymous memory and pinned kernel memory sit outside it. The insight to take: two counters are commonly misfiled. SwapCached sounds like part of the page cache but is anonymous memory that happens to have a swap copy; and Shmem sits inside Cached yet behaves like anonymous memory under reclaim. Reading Cached as “memory I can have back for free” over-counts by exactly Shmem plus Dirty.
The /proc/meminfo documentation defines the two headline counters precisely (Documentation/filesystems/proc.rst, v6.12): Cached is “In-memory cache for files read from the disk (the pagecache) as well as tmpfs & shmem. Doesn’t include SwapCached”, and Buffers is “Relatively temporary storage for raw disk blocks[;] shouldn’t get tremendously large (20MB or so)”.
The historical “buffer cache versus page cache” split is gone, and has been for a quarter of a century: the two were unified in 1999, as recorded in the file header of mm/filemap.c — “finished ‘unifying’ the page and buffer cache … 21.05.1999, Ingo Molnar”. Today Buffers is simply the page cache belonging to block-device inodes (which is why si_meminfo() in mm/show_mem.c fills bufferram from nr_blockdev_pages()), and Cached is filesystem page cache. The remaining buffer_head machinery describes how a folio’s bytes map to disk blocks; the bytes themselves live in page-cache folios.
The Modern Shape: Folios and the XArray
If you learned the page cache from a book written before 2019, two things you know are now wrong, and both matter enough that stating them as current would be a correctness bug rather than a stylistic quibble.
The index is an XArray, not a radix tree. Each file’s cached data hangs off a struct address_space whose i_pages member is a struct xarray — visible directly in include/linux/fs.h, v6.12 line 467. The XArray is “an abstract data type which behaves like a very large array of pointers”, and the kernel’s own documentation names the page cache as “the most important user” of it (core-api XArray docs). It replaced the radix tree in the 4.20 merge window. The indexing mechanics live in Address Space and the Page Cache XArray.
The stored object is a folio, not a page. A struct folio is a struct page that is guaranteed never to be a tail page of a compound page, so a function taking one is unambiguous about whether it operates on PAGE_SIZE bytes or on the whole compound allocation. Matthew Wilcox’s framing, quoted by LWN, is the clearest statement of the problem folios solve: “A function which has a struct page argument might be expecting a head or base page and will BUG if given a tail page. It might work with any kind of page and operate on PAGE_SIZE bytes. It might work with any kind of page and operate on page_size() bytes if given a head page but PAGE_SIZE bytes if given a base or tail page… We have examples of all of these today” (Corbet, LWN, March 2021). The page cache was the first major subsystem converted: Wilcox’s 46-patch “Folio-enabling the page cache” series (posted June 2021, archived at LWN) introduced filemap_add_folio, filemap_get_folio, filemap_dirty_folio, folio_start_writeback and the rest, and his own summary of it names the real cost centre: “The biggest chunk of this is teaching the writeback code that folios may be larger than a single page.” The series was contentious enough that Linus initially declined the pull request, a dispute LWN covered in detail (LWN, September 2021); it merged for 5.16 after that round.
At v6.12 the consequence is concrete and observable: the page cache does not merely tolerate large folios, it allocates them on purpose. page_cache_ra_order() in mm/readahead.c raises the folio order by two on each successive readahead round (if (new_order < mapping_max_folio_order(mapping)) new_order += 2;), clamped by the mapping’s configured maximum, by ilog2(ra->size), and by the file’s end. The ceiling is MAX_PAGECACHE_ORDER, defined in include/linux/pagemap.h as min(MAX_XAS_ORDER, PREFERRED_MAX_PAGECACHE_ORDER), where the preferred value is the PMD order when transparent huge pages are configured and order 8 (1 MiB on x86-64) otherwise.
timeline title Structural evolution of the Linux page cache 1999 : Buffer cache and page cache unified<br/>(Ingo Molnar, per the mm/filemap.c header) 2018 : Radix tree replaced by the XArray<br/>(4.20 merge window; i_pages becomes struct xarray) 2021 : Folio type introduced; 46-patch<br/>"Folio-enabling the page cache" series 2021 : Folios merge for 5.16 after the<br/>pull-request dispute 2022 : a_ops methods renamed to folio forms<br/>(5.18: dirty_folio; 5.19: read_folio) 2024 : v6.12 LTS — readahead allocates large folios,<br/>mappings carry min/max folio order 2025 : ->writepage removed from<br/>address_space_operations (v6.16)
How the page cache’s internal shape changed over 25 years. What it shows: three structural transitions — the 1999 unification that ended the separate buffer cache, the 2018 swap of the radix tree for the XArray, and the 2021–2022 folio conversion that changed the unit of caching from a page to a variable-order folio. The insight to take: descriptions of the page cache age badly in a specific way. Anything that says “the radix tree maps page offsets to struct page” is describing a kernel older than 4.20, and anything that says the cache entry is one 4 KiB page is describing a kernel older than 5.16. On v6.12 both statements are wrong; the entry is a folio of some power-of-two size, indexed in an XArray. The last row is dated after this note’s pin and is flagged as such throughout.
Mechanical Walk-through — The Buffered Read Path
When a process calls read() on a file opened without O_DIRECT, the filesystem’s read_iter method — for most filesystems, generic_file_read_iter in mm/filemap.c, v6.12 line 2792 — hands off to filemap_read() (line 2610), which is where the page cache is actually consulted. filemap_read reaches the cache through filp->f_mapping and loops over the requested range, and each iteration does exactly two things: obtain a batch of up-to-date folios, then copy out of them.
Obtaining the batch is filemap_get_pages() (line 2523), and it is a three-tier fallback:
- Lockless lookup.
filemap_get_read_batchwalks thei_pagesXArray under RCU and collects whatever is already cached and uptodate. No spinlock is taken on this path, which is what allows many CPUs to read a shared file concurrently without contending on the mapping. This is the common case, and on a warm cache the wholeread()is this step plus amemcpy. - Readahead. If the batch comes up empty,
page_cache_sync_readaheadruns (line 2547), which allocates folios and asks the filesystem to fill a run of them. On a sequential workload this is where the data actually arrives; the folios it populates satisfy not only this read but the next several. - Synchronous single-folio fill. If readahead still produced nothing,
filemap_create_folio(line 2463) allocates one folio and callsfilemap_read_folio, which invokes the filesystem’s single-folio read method and waits for it (line 2498). The calling task sleeps here; this is a blocking I/O wait, accounted to PSI as memory/IO pressure and, in themmapcase, presenting as a major fault.
Once folios are uptodate, the copy is done by generic code with copy_folio_to_iter, and each folio touched is passed to folio_mark_accessed() (lines 2674 and 2687) so that reclaim’s LRU treats it as recently used. There is a deliberate asymmetry worth noticing: filemap_read marks the first folio of a batch accessed unconditionally, and marks subsequent folios accessed as it copies them, so a large sequential read does not artificially promote every folio it streams through.
flowchart TB s["read(fd, buf, n)"] --> gfri["generic_file_read_iter"] gfri --> od{"O_DIRECT<br/>(IOCB_DIRECT)?"} od -->|yes| dio["flush + wait on overlapping dirty cache,<br/>then direct_IO — see 'Bypassing the cache'"] od -->|no| fr["filemap_read()"] fr --> fgp["filemap_get_pages()"] fgp --> l1{"cached and<br/>uptodate?<br/>(RCU lookup)"} l1 -->|"yes — the fast path"| copy l1 -->|no| ra["page_cache_sync_readahead<br/>(allocate a run of folios,<br/>ask the filesystem to fill them)"] ra --> l2{"folios now<br/>present?"} l2 -->|yes| copy l2 -->|no| one["filemap_create_folio →<br/>single-folio read, task SLEEPS"] one --> copy["copy_folio_to_iter → user buffer<br/>+ folio_mark_accessed"] copy --> ret["return bytes copied"] dio --> ret
The buffered read path in mm/filemap.c, v6.12. What it shows: three tiers of increasing cost — a lockless cache hit, a batched readahead fill, and a synchronous single-folio read that blocks the caller. The insight to take: the two branches that matter for performance are invisible from userspace. A read() that hits tier 1 costs a memcpy; a read() that falls to tier 3 costs a disk round-trip while holding up the calling thread. Everything readahead does is an effort to keep workloads in tier 1 or 2, and every O_DIRECT decision is a bet that you can manage that better yourself.
Readahead: the heuristic that decides how much to guess
Readahead is what turns a sequential scan of a large file into a stream of cache hits punctuated by occasional bulk reads. The kernel’s own overview in mm/readahead.c, v6.12 states the trigger and the split cleanly: “Readahead is triggered when an application read request (whether a system call or a page fault) finds that the requested folio is not in the page cache, or that it is in the page cache and has the readahead flag set”, and “Each readahead request is partly synchronous read, and partly async readahead.”
That two-part structure is the whole design. A readahead window is described by struct file_ra_state with a size and an async_size, and the first folio of the async tail is stamped with the PG_readahead flag. When the application eventually reads that folio — a cache hit, no I/O — touching it triggers the next readahead round. The steady state is therefore fully asynchronous: the application never waits, because the window is always refilled one window ahead of where it is reading.
file offsets ──────────────────────────────────────────────►
ra->start end of window
| |
v v
|============================#===============|
|<--------------- ra->size ------------------>|
|<- async_size ->|
^
folio stamped PG_readahead:
reading THIS folio (a cache hit)
kicks off the next readahead round
The readahead window, redrawn from the ASCII diagram in mm/readahead.c’s own “On-demand readahead design” comment. What it shows: the window the kernel has already populated, and the marker folio inside it whose first access triggers the next round. The insight to take: readahead is not “read N pages ahead on every read” — it is a self-clocking pipeline, driven by the application catching up to a marker the kernel left behind. This is an ASCII box diagram rather than mermaid because what is being depicted is a linear offset range with a marker inside it; mermaid has no diagram type for a one-dimensional interval with an annotated interior point.
The window sizes come from two small functions, and both are worth reading because their behaviour is frequently misattributed to tunables that do not exist. On the first read of a stream, get_init_ra_size() rounds the request up to a power of two, then multiplies by 4 if that is at most max/32, by 2 if it is at most max/4, and otherwise clamps to max. On each subsequent round, get_next_ra_size() grows the window: 4× if the current size is below max/16, 2× if it is at most max/2, otherwise max. So a sequential reader ramps aggressively at first and then saturates.
Situation (v6.12 mm/readahead.c) | Window behaviour |
|---|---|
Default maximum (max) | bdi->ra_pages, initialised to VM_READAHEAD_PAGES = 128 KiB / PAGE_SIZE; tunable per device via read_ahead_kb in sysfs |
| Oversized single request | max is raised to min(req_size, bdi->io_pages) — a huge read() is allowed up to the device’s optimal I/O size |
Start of file, or index - prev_index <= 1 | treated as sequential: ra->size = get_init_ra_size(req_count, max) |
| Small standalone random read | do_page_cache_ra(req_count) only — and deliberately does not update file_ra_state, so one random read cannot poison a sequential stream’s window |
| Interleaved streams | page_cache_prev_miss() counts contiguous cached folios behind the read to infer a stream the file_ra_state has lost track of |
| Whole file already cached from offset 0 | contig_count *= 2 — “a strong indication of long-run stream (or whole-file-read)” |
PG_readahead folio hit at the expected index | window advances by ra->size, then ra->size = get_next_ra_size(...) |
FMODE_RANDOM set (via posix_fadvise(POSIX_FADV_RANDOM)) | do_forced_ra — “be dumb”: read exactly what was asked, no window at all |
ra_pages == 0, or the block cgroup is congested | forced readahead of a single page; effectively readahead off |
Readahead decision table, v6.12. What it shows: every branch in page_cache_sync_ra() and page_cache_async_ra() that changes how much is read speculatively. The insight to take: two rows are the ones that bite in production. The “small standalone random read” row means a random-access workload gets no readahead amplification and needs no tuning — the kernel detects it. The FMODE_RANDOM row means POSIX_FADV_RANDOM is not a hint that nudges a heuristic; it hard-disables the window. Reaching for read_ahead_kb before checking which of these branches you are in is guessing.
Two further v6.12 details that older descriptions get wrong. First, readahead allocates large folios: page_cache_ra_order() bumps the order by two per round, so a long sequential read escalates from base pages toward MAX_PAGECACHE_ORDER, aligning each folio’s index and shrinking the order near end-of-file so nothing is allocated past EOF. Second, readahead() is allowed to fail silently: “→readahead() should normally initiate reads on all folios, but may fail to read any or all folios without causing an I/O error. The page cache reading code will issue a →read_folio() request for any folio which →readahead() did not read, and only an error from this will be final.” The algorithm is developed further in Readahead and Readahead and Read Path; the point here is that the page cache’s population rate is set by this heuristic, not by the application’s request size.
Mechanical Walk-through — The Buffered Write Path and Dirty Accounting
A buffered write() is write-back, not write-through. The data is copied into a page-cache folio, the folio is marked dirty, and the call returns — the data has not reached disk, and will not until writeback runs or the application calls fsync(). The generic loop is generic_perform_write() in mm/filemap.c; the mechanics of which filesystem functions it calls belong to The Page Cache and address_space. What matters here is the accounting, because that is what determines whether your writer runs at memory speed or gets paused.
Marking a folio dirty does four things at once, and each one shows up somewhere an operator can see:
- Sets the folio’s
PG_dirtyflag. - Sets the searchable
PAGECACHE_TAG_DIRTYmark on that index in the mapping’s XArray —XA_MARK_0, perinclude/linux/fs.hline 493. This is what makes “find this file’s dirty folios” cheap for the flusher threads. - Increments
NR_FILE_DIRTYon the node and on the owning memcg (__lruvec_stat_mod_folio(folio, NR_FILE_DIRTY, nr)inmm/page-writeback.c) — this is/proc/meminfo’sDirty. - Charges the dirtying against the backing device’s writeback statistics, which is what the throttling algorithm later reads.
stateDiagram-v2 [*] --> Allocated: filemap_add_folio<br/>(read miss or write to<br/>an uncached offset) Allocated --> Uptodate: read completes,<br/>folio marked uptodate Uptodate --> Dirty: write_end / mmap store<br/>PG_dirty + TAG_DIRTY<br/>NR_FILE_DIRTY++ Dirty --> ToWrite: flusher snapshots the range<br/>TAG_TOWRITE set ToWrite --> Writeback: I/O submitted<br/>PG_writeback + TAG_WRITEBACK<br/>Dirty-- , Writeback++ Writeback --> Uptodate: I/O completes<br/>all marks cleared, folio CLEAN Dirty --> Dirty: re-dirtied before flush<br/>(TOWRITE snapshot prevents livelock) Uptodate --> Evicted: reclaim: clean folio<br/>removed from i_pages, freed Dirty --> Deferred: reclaim meets a DIRTY folio:<br/>set PG_reclaim, put back on LRU,<br/>NR_VMSCAN_IMMEDIATE++ Deferred --> Writeback: flusher threads write it Writeback --> Evicted: folio freed as soon as<br/>writeback completes Evicted --> [*]: a shadow entry may remain<br/>for refault detection
The lifecycle of one page-cache folio, v6.12. What it shows: the full state machine — allocation, fill, dirty, the TOWRITE snapshot, writeback, and the two exits (clean eviction, or the deferred path reclaim takes when it meets a dirty folio). The insight to take: the Dirty → Deferred → Writeback path, not the Dirty → Writeback path, is what reclaim normally does. Direct reclaim does not write your dirty file data itself; it marks the folio and hands the problem to the flusher threads. That single design decision is why “low memory plus lots of dirty data” produces stalls rather than throughput, and it is developed in Reclaim below.
The TOWRITE mark deserves its own sentence because it is the answer to an obvious question: how does the flusher avoid livelocking on a process that re-dirties pages as fast as they are written? The answer is a two-mark snapshot. Before an integrity sync, the flusher walks the DIRTY-marked indices and copies the mark to PAGECACHE_TAG_TOWRITE (XA_MARK_2); it then writes back only the TOWRITE set. Folios dirtied after the snapshot get DIRTY but not TOWRITE, so they are not in this pass’s work set and fsync() can terminate. The mark-level mechanics are in Address Space and the Page Cache XArray.
Dirty Thresholds — When Your Writer Gets Paused
This is where most production surprises live, and where the documentation’s phrasing is precise in a way that is easy to skim past. Both dirty_ratio and dirty_background_ratio are percentages “of total available memory that contains free pages and reclaimable pages”, and the kernel documentation adds the warning explicitly: “The total available memory is not equal to total system memory” (Documentation/admin-guide/sysctl/vm.rst, v6.12).
The actual denominator is computed by global_dirtyable_memory() in mm/page-writeback.c:
static unsigned long global_dirtyable_memory(void)
{
unsigned long x;
x = global_zone_page_state(NR_FREE_PAGES);
/* Pages reserved for the kernel should not be considered
* dirtyable, to prevent a situation where reclaim has to
* clean pages in order to balance the zones. */
x -= min(x, totalreserve_pages);
x += global_node_page_state(NR_INACTIVE_FILE);
x += global_node_page_state(NR_ACTIVE_FILE);
if (!vm_highmem_is_dirtyable)
x -= highmem_dirtyable_memory(x);
return x + 1; /* Ensure that we never return 0 */
}Read symbol by symbol: NR_FREE_PAGES is currently-free memory; totalreserve_pages is subtracted so that reclaim is never forced to clean pages merely to satisfy zone watermarks; NR_INACTIVE_FILE + NR_ACTIVE_FILE is the file LRU, i.e. the page cache itself. Anonymous memory is absent. The practical consequence: on a 64 GiB machine running a process with 50 GiB of anonymous memory and a small cache, “20% dirty_ratio” is not 12.8 GiB — the dirtyable pool might be 8 GiB, so the real ceiling is around 1.6 GiB. The threshold moves as the workload’s anonymous footprint changes.
The second surprise is that throttling does not begin at dirty_ratio. dirty_freerun_ceiling() is a two-line function that sets the point below which a writer is never paused:
static unsigned long dirty_freerun_ceiling(unsigned long thresh,
unsigned long bg_thresh)
{
return (thresh + bg_thresh) / 2;
}With the v6.12 defaults — vm_dirty_ratio = 20 and dirty_background_ratio = 10, both declared as static initialisers in mm/page-writeback.c — the freerun ceiling is 15% of dirtyable memory, not 20%. Below 15% a writer never pauses. Between 15% and 20% it is progressively slowed by a computed sleep. Above 20% it is hard-throttled.
flowchart TB W["Process dirties pages<br/>via write() or an mmap store"] --> RL{"32 pages dirtied<br/>since last check?<br/>(ratelimit_pages)"} RL -->|no| CHEAP["return immediately —<br/>the common case costs nothing"] RL -->|yes| BDP["balance_dirty_pages()"] BDP --> T1{"dirty ≤ 15% of dirtyable?<br/>(freerun ceiling =<br/>(thresh + bg_thresh)/2)"} T1 -->|yes| FREE["<b>freerun</b> — no pause at all"] T1 -->|no| T2{"dirty ≥ 10%?<br/>(dirty_background_ratio)"} T2 -->|yes| BG["wake the bdi flusher threads<br/>(asynchronous, writer not blocked)"] T2 --> T3{"dirty ≥ 20%?<br/>(dirty_ratio)"} T3 -->|"approaching"| PAUSE["compute a pause from the<br/>device's measured write bandwidth<br/>and sleep — I/O-less throttling"] T3 -->|"at or above"| HARD["hard throttle: the writer<br/>cannot outrun the disk"] BG --> DISK["dirty folios written to the device"] PAUSE --> DISK HARD --> DISK
The dirty-throttling ladder in v6.12. What it shows: four regimes separated by three thresholds, plus the 32-page ratelimit that keeps the common case free. The insight to take: the throttle is not a cliff at dirty_ratio — it is a ramp that starts at the midpoint between the background and foreground thresholds, and the pause is computed from measured device write bandwidth, so a slow disk throttles a writer sooner and harder than a fast one at the same dirty percentage. Raising dirty_ratio to “fix” write stalls usually just moves the cliff and makes the eventual stall longer.
Three more details that change how you tune this:
- The ratelimit.
balance_dirty_pages_ratelimited()is called on every buffered write, but it only does real work everyratelimit_pagesdirtied pages, initialised to 32 — the comment says plainly “After a CPU has dirtied this many pages,balance_dirty_pages_ratelimitedwill look to see if it needs to force writeback or throttling.” The interval is then scaled near-square-root against the safety margin bydirty_poll_interval(), so a machine far from its limit checks rarely and one near its limit checks often. - Realtime tasks get headroom. In
domain_dirty_limits(), a task with a realtime or deadline scheduling policy gets both thresholds raised by 25% plusglobal_wb_domain.dirty_limit / 32. A realtime writer is therefore harder to throttle than a normal one on the same machine — occasionally the explanation for “the same code stalls in one service and not another”. bg_threshis clamped. If a misconfiguration putsdirty_background_ratioat or abovedirty_ratio, the kernel silently setsbg_thresh = thresh / 2rather than honouring it. Setting them equal does not do what it looks like it does.
The tunables, with their v6.12 defaults read from the source rather than from folklore:
Tunable (/proc/sys/vm/) | v6.12 default | Meaning |
|---|---|---|
dirty_background_ratio | 10 (%) | Background flushers start writing at this fraction of dirtyable memory |
dirty_ratio | 20 (%) | The writing process itself starts writing out / being throttled |
dirty_background_bytes | 0 (disabled) | Absolute-byte counterpart; setting one zeroes the other |
dirty_bytes | 0 (disabled) | Absolute-byte counterpart of dirty_ratio; minimum accepted value is two pages |
dirty_writeback_centisecs | 500 (5 s) | Interval between periodic “kupdate”-style writeback wakeups |
dirty_expire_centisecs | 3000 (30 s) | Longest a dirty folio may sit before the next flusher wakeup must write it |
| (derived) freerun ceiling | (20 + 10) / 2 = 15% | Below this, a writer is never paused. Not a sysctl; computed in the kernel |
Dirty-writeback tunables at v6.12, defaults taken from the static initialisers in mm/page-writeback.c and the descriptions from vm.rst. What it shows: the four sysctls that set the thresholds, the two that set the timers, and the derived threshold that has no sysctl at all. The insight to take: the last row is the one that is not in any tuning guide, because it is not user-visible. If you measure “at what dirty percentage does my writer slow down” you will get 15, not 20, and concluding that your dirty_ratio setting is being ignored is the wrong inference.
The policy and the pause-computation algorithm — how balance_dirty_pages() converts a position within the ramp into a sleep duration — are the subject of Dirty Page Balancing and Throttling and Dirty Pages and Writeback; the flusher-thread machinery is Dirty Page Writeback and Flusher Threads. The point for this note is the subsystem behaviour: writes are cheap and deferred, the deferral has a hard ceiling that is lower than the documented one, and durability requires fsync().
Interaction with Reclaim — Why “Low Memory + Dirty Data” Stalls
The page cache, writeback, and reclaim form a triangle, and confusing their roles is the most common source of misdiagnosis:
- Writeback is about durability and dirty-memory limits — getting dirty page-cache data onto disk so it survives a crash and so dirty memory does not grow unbounded. It runs continuously on timers and dirty thresholds, independent of memory pressure. It does not free the folio; after writeback a clean folio stays cached.
- Reclaim is about freeing physical memory under pressure. It removes folios from the cache. Page cache is the cheapest thing to reclaim (a clean file folio costs nothing to drop), which is why under modest pressure the kernel sheds cache before touching anonymous memory — the bias controlled by swappiness.
The subtle part is what reclaim does when it meets a dirty folio, and the widespread belief — “reclaim writes it back, then frees it” — is wrong on v6.12 in the case that matters. In shrink_folio_list() in mm/vmscan.c, a dirty file folio is only written by reclaim if all of the following hold: the caller is kswapd, the folio already carries PG_reclaim (meaning reclaim has seen it once before and come round again), and PGDAT_DIRTY is set on the node. The comment states the reasoning: “Only kswapd can writeback filesystem folios to avoid risk of stack overflow. But avoid injecting inefficient single-folio I/O into flusher writeback as much as possible: only write folios when we’ve encountered many dirty folios, and when we’ve already scanned the rest of the LRU for clean folios and see the same dirty folios again.”
Otherwise reclaim sets PG_reclaim, bumps NR_VMSCAN_IMMEDIATE, and puts the folio back — a deferral, not a write. The folio will be freed the moment the flusher threads finish writing it. And when an entire isolated batch turns out to be dirty-and-not-yet-queued (stat.nr_unqueued_dirty == nr_taken), reclaim wakes the flushers with WB_REASON_VMSCAN and, on cgroup-v1 setups, throttles itself with VMSCAN_THROTTLE_WRITEBACK.
flowchart TB subgraph normal["Healthy: the two loops are independent"] n1["writeback: timers + dirty thresholds<br/>→ dirty folios become clean"] n2["reclaim: LRU scan<br/>→ clean folios dropped, memory freed"] end subgraph stall["Pathological: they collide"] p1["memory pressure"] --> p2["reclaim scans the file LRU"] p2 --> p3{"folio dirty?"} p3 -->|"clean"| p4["drop it — free, instant"] p3 -->|"dirty"| p5["cannot free it.<br/>set PG_reclaim, wake flushers,<br/>put it back on the LRU"] p5 --> p6["flushers compete for the<br/>same saturated disk"] p6 --> p7["reclaim rescans, finds the<br/>same folios still dirty"] p7 --> p8["allocation latency climbs;<br/>PSI memory + io pressure rises"] p8 --> p2 end
The reclaim/writeback interaction, healthy and pathological. What it shows: in normal operation the two loops never meet, because reclaim finds clean folios to drop; under simultaneous memory pressure and heavy dirtying, reclaim keeps encountering folios it is not allowed to free and cannot itself write, and cycles. The insight to take: the stall is not caused by the page cache being “too big” — it is caused by the dirty fraction of it being too big relative to how fast the device drains. The fix is on the writeback side (lower dirty_ratio, faster storage, fsync earlier, or per-cgroup writeback limits), not on the cache side. PSI exists precisely to make this loop visible before it becomes a timeout.
Reclaim may also leave a shadow entry behind: a small value stored in the XArray slot the evicted folio occupied, recording eviction recency, so that a later refault at the same offset can distinguish a thrashing working set from a genuinely cold miss. That is the workingset mechanism, detailed in Address Space and the Page Cache XArray; the aging machinery is The LRU Lists and its modern replacement Multi-Generational LRU.
Reading the Page Cache in /proc/meminfo
The relevant fields, with the kernel’s own definitions from Documentation/filesystems/proc.rst at v6.12:
MemTotal: 32521456 kB # physical RAM minus reserved bits and the kernel image
MemFree: 412356 kB # genuinely unused — a LOW value here is normal and healthy
MemAvailable: 27214312 kB # the honest headroom number (see the formula below)
Buffers: 581092 kB # block-device page cache; "shouldn't get tremendously large (20MB or so)"
Cached: 5587612 kB # filesystem page cache + tmpfs/shmem; "Doesn't include SwapCached"
SwapCached: 0 kB # ANONYMOUS pages resident and also present in swap — not page cache
Dirty: 128 kB # "Memory which is waiting to get written back to the disk"
Writeback: 0 kB # "Memory which is actively being written back to the disk"
AnonPages: 4210044 kB # "Non-file backed pages mapped into userspace page tables"
Mapped: 842116 kB # "files which have been mmapped, such as libraries" — a subset of Cached
Shmem: 120484 kB # tmpfs + shared memory — a subset of Cached that cannot simply be dropped
MemAvailable is the number to watch, not MemFree. The documentation describes it as “an estimate of how much memory is available for starting new applications, without swapping”, calculated from MemFree, SReclaimable, the file LRU sizes and the per-zone low watermarks, and notes that “the estimate takes into account that the system needs some page cache to function well”. The implementation, si_mem_available() in mm/show_mem.c, v6.12, is worth walking because it shows exactly how conservative the kernel is about its own cache:
available = global_zone_page_state(NR_FREE_PAGES) - totalreserve_pages;
pagecache = global_node_page_state(NR_ACTIVE_FILE) +
global_node_page_state(NR_INACTIVE_FILE);
pagecache -= min(pagecache / 2, wmark_low); /* keep at least half, or the low watermark */
available += pagecache;
reclaimable = global_node_page_state_pages(NR_SLAB_RECLAIMABLE_B) +
global_node_page_state(NR_KERNEL_MISC_RECLAIMABLE);
reclaimable -= min(reclaimable / 2, wmark_low);
available += reclaimable;Symbol by symbol: start from free pages minus the reserves that must never be handed to userspace; add the file LRU (the page cache) but withhold the smaller of half of it and one low-watermark’s worth, because — per the comment — “Not all the page cache can be freed, otherwise the system will start swapping or thrashing”; add reclaimable slab under the same discount. So MemAvailable already assumes you cannot get all your cache back. This is precisely the arithmetic that a dashboard computing MemFree + Cached gets wrong in the optimistic direction.
flowchart LR subgraph pc["Page cache (this note's subject)"] C["Cached"] --- S["Shmem<br/>(subset: swap-backed)"] C --- M["Mapped<br/>(subset: currently mmap'd)"] B["Buffers"] D["Dirty"] --> WB["Writeback"] end subgraph anon["Anonymous memory (NOT page cache)"] A["AnonPages"] --- SC["SwapCached"] end subgraph derived["Derived headline numbers"] MA["MemAvailable = free − reserves<br/>+ ~half the file LRU<br/>+ ~half reclaimable slab"] MF["MemFree"] end C -.->|"counted, discounted by ~50%"| MA B -.->|"counted (part of file LRU)"| MA A -.->|"NOT counted"| MA
How the /proc/meminfo page-cache fields relate to each other and to MemAvailable. What it shows: Shmem and Mapped are subsets of Cached, not additions to it; Dirty and Writeback are states of cached folios, not separate pools; SwapCached belongs to the anonymous side entirely. The insight to take: the fields are not disjoint, so summing them double-counts. The only field designed to be read as a single answer to “how much memory do I have” is MemAvailable, and it deliberately assumes it can only recover about half the cache.
The operational reads. A persistently large Dirty under load means applications are producing data faster than the device absorbs it; combined with non-zero Writeback and IO pressure in PSI, that is the write-throttling signature from the previous section. Buffers staying small is normal; if it grows large, something is doing heavy raw block-device I/O (a dd to /dev/sdX, a backup agent, an unmounted-filesystem scan). A large Shmem inside Cached is the case where “cache is free memory” is most wrong.
For per-file rather than system-wide visibility, mincore(2) and the fincore utility report which pages of a specific file are resident. For cache churn, v6.12 exposes the tracepoints mm_filemap_add_to_page_cache, mm_filemap_delete_from_page_cache, mm_filemap_get_pages, mm_filemap_map_pages and mm_filemap_fault, declared in include/trace/events/filemap.h — note that mm_filemap_get_pages and mm_filemap_map_pages are range tracepoints, which is the folio-era shape.
echo 1 > /proc/sys/vm/drop_caches drops clean page cache (2 drops reclaimable slab — dentries and inodes; 3 does both). The documentation is unusually direct about this being a diagnostic rather than a tuning knob: “This is a non-destructive operation and will not free any dirty objects”, “This file is not a means to control the growth of the various kernel caches”, and “use outside of a testing or debugging environment is not recommended” (vm.rst, v6.12). Note the first clause: because dirty folios are skipped, drop_caches without a preceding sync frees less than people expect — which the documentation also says.
Bypassing the Cache — O_DIRECT and What It Actually Does
O_DIRECT is described everywhere as “bypassing the page cache”, and the kernel documentation’s own phrasing for the underlying operation is “IO requests which bypass the page cache and transfer data directly between the storage and the application’s address space” (vfs.rst, v6.12). That is true of the transfer. It is not true of the interaction — and the difference is where correctness bugs live.
Reading generic_file_read_iter at v6.12, an O_DIRECT read does not ignore the cache. It first calls kiocb_write_and_wait(), which is filemap_write_and_wait_range(mapping, pos, end): it flushes any overlapping dirty page-cache folios and waits for them, so the direct read cannot return stale on-disk bytes for data that is still only in RAM. An O_DIRECT write is even more involved — generic_file_direct_write calls kiocb_invalidate_pages(), which writes back and then invalidates the overlapping clean cached folios before the write, and then tries to invalidate again afterwards, because readahead or a get_user_pages() fault could have repopulated the range mid-flight.
sequenceDiagram autonumber participant App as Application participant VFS as generic_file_read_iter /<br/>generic_file_direct_write participant PC as Page cache participant FS as Filesystem (direct_IO) participant Dev as Block device Note over App,Dev: O_DIRECT READ App->>VFS: read(fd, buf, n) with IOCB_DIRECT VFS->>PC: kiocb_write_and_wait(pos..end) PC->>Dev: write back overlapping DIRTY folios Dev-->>PC: completion PC-->>VFS: range is now clean on disk VFS->>FS: a_ops->direct_IO(iocb, iter) FS->>Dev: DMA straight into the user buffer Dev-->>App: data (no kernel copy) VFS-->>App: bytes read — may fall back to buffered<br/>for any remainder (unless DAX) Note over App,Dev: O_DIRECT WRITE App->>VFS: write(fd, buf, n) with IOCB_DIRECT VFS->>PC: kiocb_invalidate_pages(): write back,<br/>then invalidate_inode_pages2_range() PC-->>VFS: -EBUSY if a folio cannot be invalidated<br/>→ silently falls back to a BUFFERED write VFS->>FS: a_ops->direct_IO(iocb, iter) FS->>Dev: DMA from the user buffer VFS->>PC: invalidate again after completion PC-->>VFS: on failure: dio_warn_stale_pagecache()
What O_DIRECT really does with the page cache, traced from mm/filemap.c at v6.12. What it shows: both directions synchronise with the cache before touching the device, and the write path invalidates twice and can silently degrade to a buffered write. The insight to take: O_DIRECT does not mean “the page cache is not involved”; it means “no data is cached, but the cache is flushed and invalidated around every operation.” The kernel’s own comment about the post-write invalidation is refreshingly blunt about the limits of this: mixing O_DIRECT with buffered or mmap access to the same range is “a pretty crazy thing to do, so we don’t support it 100%”, and a failed invalidation only produces dio_warn_stale_pagecache() in the log. The -EBUSY fallback is the sharper trap: an application that believes it is doing direct I/O can be doing buffered I/O with no error returned.
When bypassing is right: the application maintains its own cache with better knowledge than the kernel’s LRU (databases are the canonical case — a buffer pool that knows which pages are hot beats an approximate LRU that does not), or the data is genuinely single-use at a volume that would evict everything else. When it is wrong: as a general “make I/O faster” setting. Giving up the cache also gives up readahead, gives up write combining, and forces every access to pay device latency; and the alignment requirements (buffer, offset and length aligned to the device’s logical block size) are a frequent source of EINVAL in code that “worked in testing” on a different device.
Post-6.12 note: the ecosystem alternative to O_DIRECT for avoiding cache pollution while keeping buffered semantics is RWF_DONTCACHE on preadv2/pwritev2, which landed in 6.14 — after this note’s pin, and therefore not available on a 6.12 LTS kernel.
Steering the Cache — posix_fadvise and madvise
Between “use the page cache as-is” and “bypass it entirely” sits a set of hints that steer the same cache. generic_fadvise() in mm/fadvise.c, v6.12 is short enough to read completely, and doing so replaces a lot of folklore with fact:
posix_fadvise advice | What v6.12 actually does |
|---|---|
POSIX_FADV_NORMAL | file->f_ra.ra_pages = bdi->ra_pages; clears FMODE_RANDOM and FMODE_NOREUSE. Resets to defaults. |
POSIX_FADV_SEQUENTIAL | file->f_ra.ra_pages = bdi->ra_pages * 2 and clears FMODE_RANDOM. Literally doubles the readahead window. |
POSIX_FADV_RANDOM | Sets FMODE_RANDOM, which makes page_cache_sync_ra() take its “be dumb” branch: readahead is disabled, not reduced. |
POSIX_FADV_WILLNEED | force_page_cache_readahead() over the range — populates the cache now, asynchronously. This is also exactly what the readahead(2) syscall does: ksys_readahead calls vfs_fadvise(..., POSIX_FADV_WILLNEED). |
POSIX_FADV_DONTNEED | Starts WB_SYNC_NONE writeback over the range, then invalidates whole pages only — “Partial pages are deliberately preserved on the expectation that it is better to preserve needed memory than to discard unneeded memory”. Drains the per-CPU LRU caches (lru_add_drain), and on failure retries with lru_add_drain_all(). |
POSIX_FADV_NOREUSE | Sets FMODE_NOREUSE. On v6.12 this is a real signal to MGLRU rather than the historical no-op. |
Any advice on a DAX file or a noop_backing_dev_info mapping | Validated and then ignored, returning 0. Your hint silently does nothing. |
What each posix_fadvise advice does in v6.12, read from mm/fadvise.c. What it shows: the mapping from POSIX advice to concrete kernel state changes — readahead window size, two f_mode bits, and range operations on the cache. The insight to take: DONTNEED is the one that surprises people, in three ways. It only starts writeback (WB_SYNC_NONE) rather than waiting, so dirty data in the range may not be evicted at all; it skips partial pages at both ends of the range; and it can fail silently when a folio is still held on another CPU’s LRU batch. The correct idiom for “stream this file without polluting the cache” is fdatasync() on the range first, then FADV_DONTNEED — and even then, page-aligned ranges only.
The madvise(2) counterparts apply to mapped ranges rather than file ranges: MADV_WILLNEED and MADV_DONTNEED for population and release, MADV_SEQUENTIAL/MADV_RANDOM for the mapping’s readahead behaviour, and MADV_COLD/MADV_PAGEOUT to demote or evict without unmapping. Because an mmap’d file’s pages are page-cache folios, these hints steer the same cache from the other side; the fault path that populates them is The Page Fault Handler.
Failure Modes and Misunderstandings
Almost every page-cache incident is a reading error rather than a tuning error: a number is interpreted as meaning something it does not mean, and the remediation that follows makes things worse. The eight below are the ones that recur, each traced to the code that produces the behaviour.
flowchart TB S{"What is the<br/>observed symptom?"} S -->|"'Memory is nearly full',<br/>MemFree tiny"| M1["<b>Non-issue.</b> Check MemAvailable,<br/>not MemFree. Cached is doing its job."] S -->|"Writers pause for<br/>hundreds of ms"| M2["Check Dirty vs the dirtyable pool.<br/>Throttling starts at 15%, not dirty_ratio.<br/>Root cause is device drain rate."] S -->|"Allocation latency + high<br/>PSI memory pressure"| M3["Dirty fraction of the cache is too large:<br/>reclaim keeps meeting folios it may not free.<br/>Fix on the writeback side."] S -->|"fsync() returned 0<br/>but data was lost"| M4["Writeback error was already consumed<br/>by an earlier fsync on another fd.<br/>errseq_t is a per-file cursor."] S -->|"O_DIRECT app is slow /<br/>sees stale data"| M5["Silent fallback to buffered:<br/>kiocb_invalidate_pages returned -EBUSY<br/>→ generic_file_direct_write returns 0."] S -->|"FADV_DONTNEED freed<br/>nothing"| M6["Range was dirty, partial, or on a<br/>remote CPU's LRU batch.<br/>fdatasync the range first."] S -->|"mmap'd random reads<br/>do 10x the I/O"| M7["mmap read-around: ra_pages centred on<br/>the fault, for the first 100 misses.<br/>Use MADV_RANDOM."] S -->|"drop_caches 'fixed' it<br/>for five minutes"| M8["You deleted a warm cache.<br/>The metric moved; the workload got slower."]
A symptom-to-cause map for page-cache incidents. What it shows: the eight recurring presentations and the mechanism behind each, all of which are developed in prose below. The insight to take: notice that only two of the eight (M2, M3) are genuinely page-cache problems, and both of them are really writeback problems. Three are misread metrics and three are API contracts that differ from their reputation. Reaching for a vm. sysctl before identifying which box you are in is how a two-minute diagnosis becomes a week of tuning folklore.
“Memory is full” — reading MemFree instead of MemAvailable
This is the most common false alarm in Linux monitoring, and it is expected behaviour rather than a fault. A healthy long-lived server has a small MemFree because the kernel has filled otherwise-idle RAM with cached file data that it will give back the instant anything needs it. An alert on MemFree < 5% fires permanently on every healthy machine, and — worse — trains operators to ignore it, so the one time it means something it is dismissed.
The correct signal is MemAvailable, whose implementation (walked in Reading the page cache in /proc/meminfo above) already withholds roughly half the file LRU from its own estimate. The naive replacement, MemFree + Cached, is wrong in the optimistic direction for exactly the reasons the kernel’s own arithmetic guards against: it counts dirty folios that require a disk write before they can be freed, it counts Shmem/tmpfs folios that can never be dropped at all (only swapped), and it counts Mapped and mlocked file pages that are pinned by live users. On a machine with a large tmpfs — a container host with /dev/shm in use, or a build machine using a tmpfs work directory — MemFree + Cached can over-report available memory by tens of gigabytes right up until the OOM killer fires.
“Just drop the caches”
echo 3 > /proc/sys/vm/drop_caches is the folk remedy, and reading its 79-line implementation in fs/drop_caches.c, v6.12 is enough to stop reaching for it. drop_pagecache_sb() is called on every mounted superblock via iterate_supers(), and for each one it walks the whole sb->s_inodes list under s_inode_list_lock, taking a reference on each inode and calling invalidate_mapping_pages(inode->i_mapping, 0, -1). There are three consequences worth stating plainly.
First, it does not free what people think it frees. invalidate_mapping_pages() is documented in mm/truncate.c, v6.12 as removing “pages that are clean, unmapped and unlocked, as well as shadow entries”, and it “will not block on IO activity”. The per-folio gate is mapping_evict_folio(), which returns 0 — refusing to evict — if the folio is dirty, if it is under writeback, or if its refcount exceeds folio_nr_pages(folio) + folio_has_private(folio) + 1, the check whose comment says “The refcount will be elevated if any page in the folio is mapped”. So dirty data, in-flight data, and every mapped page of every running binary and shared library stays. The vm.rst documentation says the same thing from the other direction: “This is a non-destructive operation and will not free any dirty objects.”
Second, it is global and unbounded. There is no way to drop one file’s cache with this interface; the walk covers every inode of every filesystem, and its only concession to latency is a cond_resched() per inode. On a machine with millions of cached inodes this is a visible pause.
Third, the thing it does successfully do is delete a warm cache. Every subsequent access refaults from disk, so the machine is measurably slower for as long as it takes to re-warm — which on a database or a Kafka broker is minutes to hours. The kernel documentation is unusually blunt: “This file is not a means to control the growth of the various kernel caches… use outside of a testing or debugging environment is not recommended” (vm.rst, v6.12). The handler even logs every use — pr_info("%s (%d): drop_caches: %d\n", current->comm, task_pid_nr(current), sysctl_drop_caches) — precisely so that a mystery latency regression can be traced back to whoever ran it. (Setting bit 2, i.e. writing 4, sets the file-static stfu flag and suppresses that log line permanently until reboot; if you find drop_caches messages that stopped appearing, someone wrote a value with bit 2 set.)
The legitimate uses are benchmarking a cold-cache path and reproducing a refault-driven bug. Neither is production tuning. If the goal is to stop one workload from polluting the cache, the tool is posix_fadvise(POSIX_FADV_DONTNEED) on that file, or on 6.14+ kernels RWF_DONTCACHE — both scoped, neither global.
“fsync() succeeded, so the data is safe”
A buffered write() that returns success has put bytes in a folio, nothing more. Durability requires fsync() or fdatasync() — that much is widely known. What is not widely known is that a successful fsync() on one file descriptor can consume a writeback error that another descriptor needed to see.
The kernel records writeback failures in the address_space itself: struct address_space carries errseq_t wb_err (include/linux/fs.h, v6.12 line 480) and struct file carries a cursor errseq_t f_wb_err into it (line 1057). __filemap_set_wb_err() in mm/filemap.c stamps an error into the mapping; file_check_and_advance_wb_err() compares the file’s cursor against the mapping’s current value, reports the error once per file descriptor, and advances the cursor. The design is what makes each open()ed descriptor able to see an error that occurred before it existed — but an error is still delivered to a given descriptor exactly once, and the legacy AS_EIO/AS_ENOSPC flag bits that filemap_check_errors() reads are test_and_clear operations, i.e. destructive reads.
This is the mechanism behind the incident that made the behaviour famous. PostgreSQL performs writes from many backend processes but calls fsync() from a single checkpointer process that often has to open() the file first, and as LWN reported, “even in 4.13 and later kernels, the checkpointer will not see any errors that happened before it opened the file” (Corbet, LWN, April 2018). Worse, the data is not merely un-synced but gone: on a write failure “filesystems will respond differently, but that behavior usually includes discarding the data in the affected pages and marking them as being clean”, so a re-read returns the old contents with no error. Ted Ts’o’s justification, quoted in the same article, is that the alternative is worse — “the most common cause of I/O errors, by far, is a user pulling out a USB drive at the wrong time”, and pinning dirty pages forever would exhaust memory. The operational rule that falls out: treat an EIO from fsync() as unrecoverable for that file, keep the descriptor open across the whole write-then-sync sequence, and never assume a retry of fsync() re-attempts the write — the pages are already clean, so there is nothing left to retry.
O_DIRECT that quietly is not
Covered mechanically above, but it belongs in the failure list because the failure is silent in both directions. In generic_file_direct_write() (mm/filemap.c, v6.12 line 3960), the pre-write invalidation is the first thing that happens, and its failure path is three lines:
/*
* If a page can not be invalidated, return 0 to fall back
* to buffered write.
*/
written = kiocb_invalidate_pages(iocb, write_len);
if (written) {
if (written == -EBUSY)
return 0;
return written;
}Returning 0 means “no bytes written by the direct path”, and the caller then performs an ordinary buffered write. No error, no counter, no log line. An application that believes it has bypassed the cache is dirtying it, and its carefully tuned dirty_ratio assumptions no longer hold.
The other direction is the post-write invalidation. If a range cannot be invalidated after a direct write — typically because something mmaped it, or readahead repopulated it mid-flight — the kernel emits pr_crit("Page cache invalidation failure on direct I/O. Possible data corruption due to collision with buffered I/O!\n") from dio_warn_stale_pagecache() and sets -EIO into the mapping’s wb_err. Note the rate limit on that warning: DEFINE_RATELIMIT_STATE(_rs, 86400 * HZ, DEFAULT_RATELIMIT_BURST) — one burst per 86,400 seconds, i.e. per day. A host that is corrupting data continuously will log about it once a day. Absence of the message is not evidence of absence.
POSIX_FADV_DONTNEED that frees nothing
The advice is a request, not a command, and it fails quietly in three distinct ways, all visible in generic_fadvise() and mapping_try_invalidate(). The range is written back with WB_SYNC_NONE, which starts writeback rather than waiting for it, so dirty folios in the range are typically still dirty when the invalidation walk reaches them, and mapping_evict_folio() refuses them. Partial folios at both ends of the range are deliberately skipped — the comment says it “is better to preserve needed memory than to discard unneeded memory”. And a folio sitting in another CPU’s per-CPU LRU batch is not evictable; mapping_try_invalidate() counts these into nr_failed, and generic_fadvise() retries once with lru_add_drain_all() before giving up. The working idiom is therefore: page-align the range, sync_file_range() or fdatasync() it first, then FADV_DONTNEED — and verify with mincore(2)/fincore rather than assuming.
mmap read-around, and why random mmap access does surprising I/O
mmap does not use the same readahead policy as read(). do_sync_mmap_readahead() in mm/filemap.c, v6.12 line 3142 implements read-around: on a major fault it sets ra->start = max_t(long, 0, vmf->pgoff - ra->ra_pages / 2) and ra->size = ra->ra_pages, i.e. it reads a full readahead window centred on the faulting page, not forward from it. With the default 128 KiB window, one random 8-byte read through an mmap can pull 128 KiB from disk.
There is an adaptive brake, and its threshold is worth knowing: ra->mmap_miss is incremented on each miss, and once mmap_miss > MMAP_LOTSAMISS — with #define MMAP_LOTSAMISS (100) — read-around is switched off for that file, with the comment “Do we miss much more than hit in this file? If so, stop bothering with read-ahead. It will only hurt.” So a purely random mmap workload pays the amplification for roughly its first hundred faults and then stops. A mixed workload that keeps generating hits never trips the brake and pays it indefinitely. madvise(MADV_RANDOM) sets VM_RAND_READ, which returns from do_sync_mmap_readahead() before any of this — the same “be dumb” posture POSIX_FADV_RANDOM gives the read() path.
Tuning readahead that is already off
read_ahead_kb is the first knob people reach for on a random-I/O workload, and on a genuinely random workload it usually changes nothing, because the kernel already detected the pattern. Per the decision table above, a small standalone random read takes the do_page_cache_ra(req_count) branch and deliberately does not update file_ra_state, and an FMODE_RANDOM file takes do_forced_ra. Conversely, raising read_ahead_kb on a sequential workload that is already saturating the device raises memory pressure without raising throughput, because the window was never the bottleneck. Measure which branch you are in — the mm_filemap_get_pages tracepoint and the ratio of pgpgin to bytes actually consumed will tell you — before changing the number.
Double caching
An application with its own cache that also does buffered I/O stores every hot byte twice: once in its own buffer and once in a page-cache folio. The Kafka design documentation states the problem exactly: the unified cache “cannot easily be turned off without using direct I/O, so even if a process maintains an in-process cache of the data, this data will likely be duplicated in OS pagecache, effectively storing everything twice” (Apache Kafka design docs). There are two coherent resolutions — lean on the page cache and keep your own cache small (Kafka’s choice), or take O_DIRECT and own the caching entirely (the classic database choice) — and one incoherent one, which is a large application cache on top of buffered I/O. PostgreSQL’s documentation is explicit that it deliberately sits in the middle: “because PostgreSQL also relies on the operating system cache, it is unlikely that an allocation of more than 40% of RAM to shared_buffers will work better than a smaller amount” (PostgreSQL 17 docs, resource configuration). See PostgreSQL Shared Buffers and the Buffer Manager and The Buffer Pool.
Alternatives and When to Bypass the Page Cache
“Use the page cache or not” is a false binary. There are six distinct postures a program can take toward the cache, and the interesting engineering question is not whether to bypass it but which property you are trying to buy — lower memory footprint, predictable latency, exclusive control of caching policy, or simply not evicting somebody else’s working set.
flowchart TB Q0{"Do you have better knowledge<br/>than the kernel about which<br/>bytes will be re-read?"} Q0 -->|"No — normal case"| BUF["<b>Buffered I/O, untouched.</b><br/>Readahead + writeback + LRU.<br/>The right answer for almost everything."] Q0 -->|"Only about the ACCESS PATTERN"| HINT["<b>posix_fadvise / madvise.</b><br/>SEQUENTIAL doubles the window,<br/>RANDOM disables it, WILLNEED prefetches.<br/>Same cache, steered."] Q0 -->|"Yes — this data is single-use<br/>and must not evict others"| Q1{"Kernel ≥ 6.14 AND the<br/>filesystem sets FOP_DONTCACHE?"} Q1 -->|yes| DC["<b>RWF_DONTCACHE</b> on preadv2/pwritev2.<br/>Buffered semantics, no alignment rules,<br/>folio dropped on I/O completion."] Q1 -->|no| DN["<b>Buffered + FADV_DONTNEED</b><br/>after fdatasync, page-aligned.<br/>Best-effort, verify with fincore."] Q0 -->|"Yes — you maintain a real<br/>buffer pool with its own policy"| Q2{"Can you meet the alignment<br/>and error-handling contract?"} Q2 -->|yes| OD["<b>O_DIRECT</b> (usually + io_uring).<br/>No double caching, no readahead,<br/>every access pays device latency."] Q2 -->|no| BUF2["Stay buffered and shrink<br/>the application cache instead."]
Choosing a posture toward the page cache. What it shows: the decision is driven by what you know that the kernel does not — nothing, the access pattern, the reuse distance, or the whole caching policy — and each answer has a different mechanism attached. The insight to take: the two middle branches are the ones that get skipped. Teams jump straight from “buffered is evicting my working set” to O_DIRECT, inheriting alignment rules, the loss of readahead, and a silent-fallback failure mode, when a posix_fadvise hint or RWF_DONTCACHE would have solved the actual problem with no change to the I/O contract.
Buffered I/O, unmodified, is the default for a reason: it is the only posture that gets readahead, write combining, cross-process sharing of the same cached bytes, and free crash-recovery of the cache across process restarts. That last property is easy to undervalue. As the Kafka design documentation puts it, “this cache will stay warm even if the service is restarted, whereas the in-process cache will need to be rebuilt in memory (which for a 10GB cache may take 10 minutes) or else it will need to start with a completely cold cache” (Apache Kafka design docs).
Hints (posix_fadvise, madvise) keep every one of those properties and change only policy. They are the cheapest intervention available and the most under-used: POSIX_FADV_SEQUENTIAL literally doubles ra_pages for that file, POSIX_FADV_WILLNEED is a prefetch that costs nothing if the guess is wrong, and POSIX_FADV_RANDOM turns readahead off for one file without touching a system-wide sysctl. Full semantics are in Steering the cache above.
RWF_DONTCACHE is the option that did not exist when most of the folklore was written, and it is the right answer to “stream a large file without evicting the working set”. It is a per-I/O flag on preadv2()/pwritev2(), #define RWF_DONTCACHE ((__force __kernel_rwf_t)0x00000080) in include/uapi/linux/fs.h at v6.14, described there as “buffered IO that drops the cache after reading or writing data”. Mechanically it is ordinary buffered I/O plus one folio flag: PG_dropbehind, “drop pages on IO completion”, added to include/linux/page-flags.h in the same release. Because it is buffered, none of O_DIRECT’s alignment requirements apply.
Two caveats matter for anyone planning to depend on it, and both were pinned by checking the source at successive tags rather than by reading release notes:
| Kernel tag | RWF_DONTCACHE in the uapi header | FOP_DONTCACHE set by ext4 | by XFS | by btrfs |
|---|---|---|---|---|
| v6.12 (this note’s pin) | absent | — | — | — |
| v6.13 | absent | — | — | — |
| v6.14 | present | no | no | no |
| v6.15 | present | no | yes | no |
| v6.16 | present | no | yes | no |
| v6.17 | present | yes | yes | no |
Availability of RWF_DONTCACHE, established by fetching include/uapi/linux/fs.h, fs/ext4/file.c, fs/xfs/xfs_file.c and fs/btrfs/file.c at each tag and grepping. What it shows: the flag and the per-filesystem opt-in landed in different releases — the interface in 6.14, XFS in 6.15, ext4 not until 6.17, and btrfs not at all as of 6.17. The insight to take: “supported since 6.14” is true of the flag and false of any particular filesystem. The kernel is explicit about this: kiocb_set_rw_flags() returns -EOPNOTSUPP if !(ki->ki_filp->f_op->fop_flags & FOP_DONTCACHE), and also refuses DAX mappings. Feature-detect at runtime by attempting the call and handling EOPNOTSUPP; do not gate on a kernel version.
O_DIRECT buys exclusive control and gives up everything else. It is the right choice when the application genuinely has a better replacement policy than an approximate LRU — a database buffer pool that knows which index pages are hot, a storage engine that knows its own access graph — and the wrong choice as a generic “make I/O fast” setting. Beyond the alignment rules and the silent buffered fallback documented above, the costs are structural: no readahead means every sequential access pays a full device round-trip unless the application pipelines its own; no write combining means the I/O scheduler sees exactly the requests you issue; and no cross-process sharing means two processes reading the same file read it twice. In practice O_DIRECT is nearly always paired with io_uring or asynchronous I/O, because synchronous O_DIRECT serialises the application on device latency — see Direct IO and O_DIRECT and io_uring and the Block Layer.
mmap is sometimes proposed as an alternative to the page cache; it is not one. A mapped file’s pages are page-cache folios — the mapping merely installs page-table entries pointing at them, which is why Mapped is a subset of Cached. What changes is the fault policy (read-around rather than readahead, per the failure-modes section), the absence of a copy into a user buffer, and the fact that a store to a shared mapping dirties a folio without ever entering the write syscall path. It is a different access method to the same cache, with its own trade-offs; see Shared Memory via mmap and The Page Fault Handler.
tmpfs inverts the relationship: the page cache is not a cache of the filesystem, it is the filesystem. tmpfs folios are accounted under both Cached and Shmem, have no backing file to be written to, and can only leave RAM via swap. Sizing a tmpfs is therefore sizing a permanent, unreclaimable-without-swap deduction from available memory, not a cache. See tmpfs In-Memory Filesystem.
| Posture | Cache populated? | Readahead | Alignment rules | Shared between processes | Survives process restart | Typical user |
|---|---|---|---|---|---|---|
| Buffered (default) | yes | yes | none | yes | yes | almost everything |
Buffered + fadvise hints | yes, steered | tuned per file | none | yes | yes | backup agents, log scanners |
RWF_DONTCACHE (≥6.14, per-fs) | transiently | yes | none | briefly | no | bulk copy, one-pass scans |
Buffered + FADV_DONTNEED | yes then evicted | yes | page-aligned range | yes | no | pre-6.14 equivalent of the above |
O_DIRECT | no | no | buffer, offset, length | no | n/a | database engines |
mmap | yes (same folios) | read-around | page-granular | yes | yes | index/lookup workloads |
Six postures compared across the properties that actually differ. What it shows: the columns are the real trade space — not “fast versus slow” but who holds the cached copy, who pays for prefetching, and what the API demands of the caller. The insight to take: RWF_DONTCACHE occupies a cell that used to be empty. Before 6.14 the only way to avoid polluting the cache was O_DIRECT (with its alignment contract) or FADV_DONTNEED (best-effort, and useless for dirty data); now there is an option with buffered ergonomics and bounded residency, which is why bulk-copy and backup tooling is the first thing expected to adopt it.
Finally, two things that are frequently proposed as alternatives but are really writeback controls rather than cache controls: lowering dirty_ratio/dirty_bytes to bound stall length, and the cgroup v2 io controller to bound a workload’s share of the device. Neither reduces caching; both reduce the amount of dirty cache in flight, which is the quantity that actually causes the stalls people blame the cache for. See The cgroup io Controller and Writeback Throttling and wbt.
Production Notes
The page cache is charged to a cgroup, and the charge is sticky
On any container platform the page cache is not a global pool — every folio is charged to a memory cgroup, and cgroup v2’s documentation states the rule and its consequence in the same breath: “A memory area is charged to the cgroup which instantiated it and stays charged to the cgroup until the area is released. Migrating a process to a different cgroup doesn’t move the memory usages that it instantiated while in the previous cgroup” (Documentation/admin-guide/cgroup-v2.rst, v6.12). For a shared file the doc is franker still: “A memory area may be used by processes belonging to different cgroups. To which cgroup the area will be charged is in-deterministic.”
Two operational consequences follow, and both surprise people.
The first is that whichever container touches a shared file first pays for it. A base-image library, a shared dataset on a bind mount, a common model file — the first reader’s cgroup is charged, and every later reader gets the cache for free while the first one’s memory.current carries it. When that first container is the one with the tight memory.max, it is the one that thrashes. The kernel documentation proposes the remedy directly: “If a cgroup sweeps a considerable amount of memory which is expected to be accessed repeatedly by other cgroups, it may make sense to use POSIX_FADV_DONTNEED to relinquish the ownership of memory areas belonging to the affected files to ensure correct memory ownership.”
The second is that memory.current is a bad proxy for “how much memory this workload needs”, precisely because most of it is cache. The doc’s own example is the archetype: “a workload which writes data received from network to a file can use all available memory but can also operate as performant with a small amount of memory”. The counters that do answer the question are workingset_refault_file in memory.stat — “Number of refaults of previously evicted file pages” — and the cgroup’s PSI memory pressure. A container whose memory.current is pinned at its limit but whose workingset_refault_file is flat is fine; the same container with a climbing refault rate is thrashing its cache and needs either more memory or a smaller working set.
flowchart TB subgraph inode["One inode's dirty folios"] f1["folio charged to<br/>cgroup A"] f2["folio charged to<br/>cgroup A"] f3["folio charged to<br/>cgroup B<br/>(a 'foreign page')"] end inode --> own["Writeback ownership is<br/>per-INODE, not per-folio"] own --> attr["All write I/O for this inode<br/>is attributed to the inode's<br/>currently-owning cgroup"] attr --> sw{"Does a foreign cgroup<br/>become the majority<br/>over time?"} sw -->|yes| flip["Inode ownership switches<br/>to that cgroup"] sw -->|no| keep["Ownership stays put"] attr --> warn["Two cgroups dirtying the same<br/>inode simultaneously:<br/>'a significant portion of IOs are<br/>likely to be attributed incorrectly'"]
How cgroup writeback attributes I/O, per the cgroup v2 documentation. What it shows: memory is charged per folio but writeback is accounted per inode, so an inode dirtied by two cgroups produces “foreign pages” whose I/O is billed to whichever cgroup currently owns the inode; the kernel watches for a foreign majority and flips ownership. The insight to take: the mismatch is structural, not a bug, and the documentation’s advice is to design around it — “It’s recommended to avoid such usage patterns.” If your io.stat numbers do not match your intuition for who is writing, a shared inode is the first thing to check. Also note the prerequisite: “cgroup writeback requires explicit support from the underlying filesystem. Currently, cgroup writeback is implemented on ext2, ext4, btrfs, f2fs, and xfs. On other filesystems, all writeback IOs are attributed to the root cgroup” — so on overlayfs-over-something-else, or on a network filesystem, your per-container I/O accounting may silently be zero.
Dirty limits are cgroup-aware too, and in the conservative direction: “Both system-wide and per-cgroup dirty memory states are examined and the more restrictive of the two is enforced”, with vm.dirty_ratio applied “with the amount of available memory capped by limits imposed by the memory controller”. A container with a 2 GiB memory.max therefore starts throttling its writer at a couple of hundred megabytes of dirty data, not at 20% of host RAM — which is the usual explanation for “the same binary writes at 500 MB/s on the host and 60 MB/s in the container”.
Designing for the page cache: Kafka
Kafka is the clearest published example of a system that treats the page cache as its primary cache rather than as an obstacle. Its design documentation argues the case explicitly: because the OS “will happily divert all free memory to disk caching with little performance penalty when the memory is reclaimed”, and because a JVM in-heap cache both doubles object overhead and makes garbage collection “increasingly fiddly and slow as the in-heap data increases”, “using the filesystem and relying on pagecache is superior to maintaining an in-memory cache or other structure — we at least double the available cache by having automatic access to all free memory, and likely double again by storing a compact byte structure rather than individual objects. Doing so will result in a cache of up to 28-30GB on a 32GB machine without GC penalties” (Apache Kafka design docs).
The design that falls out is exactly the one this note has been describing from the kernel side: “All data is immediately written to a persistent log on the filesystem without necessarily flushing to disk. In effect this just means that it is transferred into the kernel’s pagecache.” A Kafka broker’s consumers reading near the log head are serviced entirely from Cached; the writeback machinery drains the dirty tail on its own schedule. The two operational rules that follow are (a) do not size the JVM heap large — every gigabyte given to the heap is a gigabyte taken from the cache — and (b) do not run drop_caches on a broker, because a cold broker serves historical reads from disk at a fraction of the rate.
Designing around the page cache: PostgreSQL
PostgreSQL takes the opposite half of the trade and lands in the middle. It maintains its own buffer pool but deliberately keeps it small enough that the kernel cache remains a useful second tier: “If you have a dedicated database server with 1GB or more of RAM, a reasonable starting value for shared_buffers is 25% of the memory in your system… because PostgreSQL also relies on the operating system cache, it is unlikely that an allocation of more than 40% of RAM to shared_buffers will work better than a smaller amount” (PostgreSQL 17 documentation).
The planner is told about the kernel’s cache explicitly. effective_cache_size “sets the planner’s assumption about the effective size of the disk cache that is available to a single query”, and the documentation instructs the administrator to “consider both PostgreSQL’s shared buffers and the portion of the kernel’s disk cache that will be used for PostgreSQL data files, though some data might exist in both places”, while warning that the setting “has no effect on the size of shared memory allocated by PostgreSQL, nor does it reserve kernel disk cache; it is used only for estimation purposes” (PostgreSQL 17 documentation). This is a rare, explicit case of an application’s query planner reasoning about the page cache’s expected size — and the reason drop_caches on a Postgres host degrades plan quality as well as I/O latency. The engine side is PostgreSQL Shared Buffers and the Buffer Manager; the general theory is The Buffer Pool and Page Replacement Policies for Buffer Pools.
A monitoring runbook
| Signal | Where | Healthy | What a bad value means |
|---|---|---|---|
MemAvailable | /proc/meminfo | comfortably above the largest expected allocation burst | genuine memory pressure — unlike a low MemFree, this one is real |
MemFree | /proc/meminfo | low is normal | alerting on this produces permanent false positives |
Dirty, Writeback | /proc/meminfo | Dirty well under the freerun ceiling (15% of dirtyable) | writers are outrunning the device; expect balance_dirty_pages stalls |
Shmem | /proc/meminfo | small, or a known tmpfs budget | this fraction of Cached is not reclaimable without swap |
workingset_refault_file | cgroup memory.stat, /proc/vmstat | flat | the cache is thrashing — evicted pages are being read straight back in |
pgscan_* / pgsteal_* | /proc/vmstat | proportionate | high scan-to-steal ratio means reclaim is working hard for little return |
nr_vmscan_immediate_reclaim | /proc/vmstat | near zero | reclaim is repeatedly hitting dirty folios it cannot free (the stall loop) |
PSI memory some/full | /proc/pressure/memory, cgroup memory.pressure | near zero full | time actually lost to memory stalls — the best single leading indicator |
mm_filemap_add_to_page_cache rate | tracepoints | steady | a spike means the cache is being repopulated, i.e. something evicted it |
What to watch, and what each signal means for the page cache specifically. What it shows: the split between counters that describe the cache’s size (which are mostly not actionable) and counters that describe its stress (which are). The insight to take: every actionable row is a rate, not a level. Cached being large tells you nothing; workingset_refault_file climbing tells you the working set no longer fits, and PSI tells you what that is costing in wall-clock time. Build dashboards from the bottom half of this table.
The workingset counters deserve emphasis because they are the mechanism that makes “is the cache big enough?” answerable at all. When reclaim evicts a folio it leaves a shadow entry in the mapping’s XArray recording eviction recency; a later fault at the same index consults it and can distinguish “this page was evicted long ago and is genuinely cold” from “this page was evicted moments ago and we are thrashing”, incrementing workingset_refault_file in the latter case. The details are in Address Space and the Page Cache XArray and Memory Reclaim Overview.
Cold starts and cache warming
Because the page cache is the difference between microsecond and millisecond reads, a freshly-started or freshly-migrated instance is slow in a way that has nothing to do with the application. The honest options are to (a) accept it and keep the instance out of rotation until refault rates settle, (b) warm deliberately with posix_fadvise(POSIX_FADV_WILLNEED) or readahead(2) over the files that matter — recall from Steering the cache that readahead(2) is literally implemented as vfs_fadvise(..., POSIX_FADV_WILLNEED) — or (c) hold the working set in an application-level cache that is populated from a known-hot list. What does not work is cat file > /dev/null on a machine under memory pressure: it warms the cache and simultaneously evicts whatever else was there, because a sequential scan is exactly the access pattern the LRU is worst at. If you must warm one file without evicting others on a 6.14+ kernel, RWF_DONTCACHE on everything else is the modern lever.
Uncertain
Verify: the LWN write-ups of the uncached-buffered-I/O series (
https://lwn.net/Articles/997548/, “Uncached buffered IO”) and Matthew Wilcox’s original folio postings onlore.kernel.orgcould not be read while writing this note. Reason: LWN returned HTTP 429 (Too Many Requests,Retry-After: 300) to bothcurland WebFetch under fleet load, andlore.kernel.orgis now behind an Anubis proof-of-work challenge that returns HTTP 403 to plaincurland an “Access Denied” interstitial to WebFetch, so neither the mailing-list threads nor those two articles were consulted. To resolve: retry both when not under fleet load, or read the lore archives through a mirror that does not require JavaScript. Every claim aboutRWF_DONTCACHEabove was instead verified directly against kernel source at successive tags, so none of it rests on the blocked sources; the gap is design-discussion context, not fact. uncertain
See Also
The three sibling page-cache notes — read the one that matches your question (the split is drawn in Scope at the top of this note):
- The Page Cache and address_space — the VFS view. The
struct address_spaceobject itself, thei_mappingversusi_databinding, theaddress_space_operationsvtable and what each method must guarantee, and how a filesystem plugs itself into the cache. Read it when your question names a function pointer, a lock, or a filesystem method. - Address Space and the Page Cache XArray — the data-structure view. How
i_pagesindexes folios by file offset, the DIRTY / WRITEBACK / TOWRITE marks, shadow entries and workingset detection. - Folios and the Page Cache — the unit-of-I/O view.
filemap_get_folio, theFGP_*flags, and how large-folio sizing is chosen.
Upstream and downstream of the cache:
- Readahead and Readahead and Read Path — the prefetch heuristic in full
- Dirty Pages and Writeback, Dirty Page Balancing and Throttling, Dirty Page Writeback and Flusher Threads — the writeback side of the triangle
- Writeback Throttling and wbt and The Multi-Queue Block Layer blk-mq — where the I/O actually goes
- The iomap Library — the modern buffered-I/O implementation most filesystems now share
- fsync fdatasync and Durability — turning “in the cache” into “on the disk”
Memory management around it:
- Anonymous vs File-Backed Memory — the split this whole note rests on
- Memory Reclaim Overview, Direct Reclaim, kswapd and Background Reclaim — who takes the cache back, and when
- The LRU Lists and Multi-Generational LRU — the aging machinery that decides which folios go
- Swappiness and Reclaim Balance — the file-versus-anon reclaim bias
- The Unevictable LRU and mlock — cached file pages that cannot be reclaimed
- Compound Pages and Large Folios and Folios and the Folio Conversion — the allocation unit and the transition that introduced it
- The Swap Cache and Linux Swap Subsystem — the anonymous-side analogue, and why
SwapCachedis not page cache - The Memory Cgroup memcg, memcg Charging and Limits, Per-cgroup Reclaim and Memory Pressure — per-container accounting of cached folios
- Pressure Stall Information — the metric that makes cache thrashing visible
- The OOM Killer — what happens when reclaim runs out of cache to shed
Access methods and bypasses:
- Direct IO and O_DIRECT — the bypass, in detail
- Shared Memory via mmap and The Page Fault Handler — reaching the same folios through page tables
- tmpfs In-Memory Filesystem — when the page cache is the storage
- The io_uring Submission and Completion Queues and io_uring and the Block Layer — asynchronous submission over either path
Application-side counterparts:
- The Buffer Pool and Page Replacement Policies for Buffer Pools — the same problem solved in userspace
- PostgreSQL Shared Buffers and the Buffer Manager — a real engine that deliberately shares the job with the kernel
MOCs: Linux Memory Management MOC · Linux Filesystems and VFS MOC · Linux Block Layer and Storage MOC · Linux MOC