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 a write() 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 treats the page cache as a subsystem — what it caches, the buffered read/write path, how pages are inserted, looked up, and evicted, how it interacts with writeback and reclaim, and how it shows up in /proc/meminfo. Its internal indexing data structure is dissected in the companion note Address Space and the Page Cache XArray.

Mental Model — Free RAM Is Wasted RAM

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 — they merely duplicate data that also lives on disk, so the kernel can drop a clean cached page at any instant with zero cost, and write back a dirty one before dropping it. This is why a freshly-booted Linux box quickly shows most of its RAM “used” by Cached: that is not memory pressure, it is the page cache doing its job. The cache is keyed per file: each file’s struct inode owns a struct address_space whose i_pages array maps page-aligned file offsets to the folios holding that file’s data.

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?"}
  block["Block layer<br/>(submit bio to device)"]
  reclaim["Reclaim / LRU<br/>drops clean, writes back dirty"]
  wb["Writeback<br/>(bdi flusher threads)"]
  app --> vfs --> pc --> hit
  hit -->|"HIT: copy from RAM"| app
  hit -->|"MISS: read I/O"| 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.

What the Page Cache Caches

The page cache holds the contents of file-backed memory: regular files on disk filesystems, plus 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). The /proc/meminfo documentation defines its two visible counters precisely (proc.rst, v6.12):

  • Cached — “In-memory cache for files read from the disk (the pagecache) as well as tmpfs & shmem. Doesn’t include SwapCached.” This is the bulk of the page cache.
  • Buffers — “Relatively temporary storage for raw disk blocks[;] shouldn’t get tremendously large (20MB or so).” Buffers is the small remnant for block-device metadata I/O (e.g. filesystem superblocks, bitmaps) cached against the block device’s own address_space.

The historical “buffer cache vs page cache” split is gone: the two were unified in 1999 (per the comment at the top of mm/filemap.c: “finished ‘unifying’ the page and buffer cache … 21.05.1999, Ingo Molnar”). Today Buffers is just block-device page cache; Cached is filesystem page cache. The remaining buffer_head machinery describes how a folio’s bytes map to disk blocks, but the bytes themselves live in page-cache folios.

What the page cache does not hold is anonymous memory — heap, stack, MAP_ANONYMOUS mappings — which has no file backing. Anonymous pages are reclaimed to swap, not written back to a file, and are accounted under AnonPages, not Cached. The distinction (file-backed vs anonymous) is the fundamental fork in reclaim policy; see Anonymous vs File-Backed Memory.

Mechanical Walk-through — Buffered Read and Write

A buffered read()

When a process calls read() on a file opened without O_DIRECT, the VFS routes it (via generic_file_read_iter and friends in mm/filemap.c) into the page cache. For each page-sized chunk of the requested range, the kernel computes the file’s page offset (pos / PAGE_SIZE) and looks it up in that inode’s i_pages:

  1. Hit. A folio is present and up-to-date. The kernel copies the bytes from the folio into the user buffer — a pure memory-to-memory copy, no I/O. The folio is marked accessed (folio_mark_accessed) so reclaim’s LRU treats it as recently used.
  2. Miss. No folio (or a value/shadow entry — see Address Space and the Page Cache XArray). The kernel allocates a folio, inserts it into i_pages (via filemap_add_folio), and issues a block-layer read (->read_folio or ->readahead from the filesystem’s address_space_operations) to fill it from disk. The reading task sleeps until the I/O completes (this is a major fault in the mmap case; for read() it is a blocking I/O wait), then copies out the now-up-to-date folio. This is a synchronous wait recorded against PSI memory/IO stall accounting.

Crucially, the kernel rarely reads just the one missing page: the Readahead machinery detects sequential access and speculatively reads ahead, populating consecutive i_pages slots so that subsequent reads hit. Readahead is what turns a sequential scan of a large file into a stream of cache hits punctuated by occasional bulk reads.

A buffered write()

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 yet reached disk. The filesystem’s ->write_begin obtains (and possibly reads-for-modify) the target folio, the kernel copies the user data in, and ->write_end marks the folio dirty. Marking dirty sets the folio’s PG_dirty flag and the searchable PAGECACHE_TAG_DIRTY mark in i_pages, and bumps the system-wide and per-memcg dirty-page counters that feed /proc/meminfo’s Dirty. The actual flush to disk happens later and asynchronously, driven by:

  • a time threshold — the dirty_expire_centisecs tunable bounds how long a dirty page may sit;
  • a memory threshold — once dirty memory exceeds dirty_background_ratio the background flusher threads start, and once it exceeds dirty_ratio the writing process itself is throttled until it falls back (this throttling is the subject of Dirty Page Balancing and Throttling);
  • an explicit fsync() / sync(), which forces writeback of a file’s (or the whole system’s) dirty pages and waits for completion.

The asynchronous flush is performed by per-backing-device (bdi) writeback threads (“flusher” threads). They walk a mapping’s dirty folios — found cheaply by searching the PAGECACHE_TAG_DIRTY mark — snapshot them to PAGECACHE_TAG_TOWRITE to avoid livelocking on a process that keeps re-dirtying, flip each to PAGECACHE_TAG_WRITEBACK (counted as /proc/meminfo Writeback), submit the block I/O, and clear all the marks on completion. The mark-level mechanics live in Address Space and the Page Cache XArray; the policy and throttling live in Dirty Pages and Writeback and Dirty Page Balancing and Throttling. The point here is the subsystem behaviour: writes are cheap and deferred; durability requires fsync.

Insertion, lookup, and eviction at the subsystem level

  • Insertion happens on a read miss or a write to an uncached offset: a folio is allocated (subject to the mapping’s gfp_mask) and stored into i_pages at the file’s page offset, after which the folio joins a reclaim LRU list. Counted into address_space->nrpages.
  • Lookup is the fast path of every cache hit, and it is lockless — it runs under RCU with no spinlock on the read side (filemap_get_entry), which is what lets many CPUs read a shared file concurrently without contending. The internals are in the companion note.
  • Eviction is performed by reclaim, not by the page cache itself. When free memory falls below a watermark, kswapd (or direct reclaim) scans the LRU lists, and for a clean file folio simply removes it from i_pages and frees it; for a dirty one it must first write it back. Reclaim may leave behind a tiny shadow entry recording the eviction recency, so a later refault can tell a thrashing working set from a genuinely cold miss (the workingset mechanism; see Address Space and the Page Cache XArray).

Interaction with Writeback and Reclaim

The page cache, writeback, and reclaim form a tight triangle, and confusing their roles is the most common source of misunderstanding:

  • 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 page; after writeback a clean folio stays cached.
  • Reclaim is about freeing physical memory under pressure. It removes folios from the cache. It depends on writeback only insofar as a dirty folio must be written back before reclaim can free it — which is why a box that is simultaneously low on memory and generating dirty data can stall: reclaim cannot make progress until writeback drains, and writeback competes for the same disk.

The reclaim side classifies pages as reclaimable or not (MM concepts, v6.12): page cache and anonymous memory are reclaimable; pinned kernel data and DMA buffers are not. Page cache is the cheapest thing to reclaim (clean file pages cost nothing to drop), which is why under modest pressure the kernel sheds cache before touching anonymous memory — the bias controlled by swappiness. The LRU machinery that ages cached folios 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 (proc.rst, v6.12):

Cached:          5587612 kB   # filesystem page cache + tmpfs/shmem (not SwapCached)
Buffers:          581092 kB   # raw block-device blocks (small; metadata)
Dirty:                12 kB   # cached data waiting to be written back
Writeback:             0 kB   # cached data actively being written back right now
Mapped:                ...    # page-cache files currently mmap'd into a process
Shmem:                 ...    # tmpfs/shmem (a subset of Cached)
SwapCached:            0 kB   # anonymous pages that are in both RAM and swap (NOT page cache)
MemAvailable:          ...    # estimate including reclaimable page cache

Two practical reads. First, MemAvailable is the number to watch, not MemFree: its definition explicitly “takes into account that the system needs some page cache to function well” and counts reclaimable cache as available — so a box with little MemFree but large Cached is healthy, not starved. Second, a persistently large Dirty under load signals that applications are producing data faster than the disk can absorb it; combined with high Writeback and IO stalls in PSI, it points at write-throttling. Buffers staying small (the docs say “20MB or so”) is normal; if it grows large, something is doing heavy raw block-device I/O. Note SwapCached is not page cache — it is anonymous memory, listed nearby but conceptually on the other side of the file/anonymous divide.

You can observe per-file cache residency with tools like fincore/mincore(2), drop the entire clean cache for testing with echo 1 > /proc/sys/vm/drop_caches (drops page cache; 2 drops slab dentries/inodes; 3 both), and trace cache churn via the mm_filemap_add_to_page_cache / mm_filemap_delete_from_page_cache tracepoints.

Failure Modes and Misunderstandings

  • “High Cached / low MemFree means I’m out of memory.” No — cache is reclaimable; MemAvailable is the real headroom. This is the single most common false alarm.
  • write() returning means the data is on disk.” No — buffered writes are write-back; the data is in dirty page cache and survives only after fsync() (and the filesystem’s journal commit). A crash between write() and writeback loses it.
  • O_DIRECT uses the page cache.” No — O_DIRECT bypasses the page cache, DMA-ing straight between the device and a user buffer. It avoids the double-copy and cache pollution, but loses caching and readahead, and mixing O_DIRECT with buffered I/O on the same file risks coherence problems. Databases that manage their own cache use it deliberately.
  • The dirty-then-low-memory stall. When memory is tight and lots of data is dirty, reclaim must write dirty folios before freeing them, contending with writeback for the disk; the system thrashes on the boundary. PSI exists precisely to surface this early. See Direct Reclaim.
  • drop_caches as a “fix.” Dropping caches forces every subsequent access back to disk and is almost never a real remedy — it is a benchmarking/diagnostic tool, not a tuning knob.
  • tmpfs counted as cache but backed by swap. Shmem/tmpfs shows under Cached, but unlike disk-file cache it cannot be cheaply dropped — its only backing is swap, so it is reclaimed by swapping, not by dropping. Treating all Cached as freely droppable overestimates available memory on tmpfs-heavy systems.

Alternatives and When to Bypass the Page Cache

The page cache is the default for all file I/O, so “alternatives” means bypassing or tuning it:

  • O_DIRECT — bypass entirely for applications with their own cache (databases, some media servers), trading caching/readahead for predictable, copy-free I/O.
  • posix_fadvise / madvise — hint the kernel: POSIX_FADV_DONTNEED evicts a range from cache after use (streaming a file once), POSIX_FADV_WILLNEED prefetches it, POSIX_FADV_SEQUENTIAL/RANDOM tune readahead aggressiveness. These let an application steer the same page cache rather than replace it.
  • mmap() — maps file-backed page-cache folios directly into a process address space, so faults populate the cache and stores dirty it; no read/write copy. The cache underneath is identical; only the access method differs (see The Page Fault Handler and Anonymous vs File-Backed Memory).
  • hugetlbfs / large folios — for very large files, large folios reduce per-page overhead and TLB pressure while staying in the page cache.

There is no “different cache” to choose — the page cache is the universal file cache. The choice is how much to use it, when to bypass it, and how to hint its behaviour.

Production Notes

The page cache is why filesystem benchmarks must distinguish cold (cache-empty) from warm (cache-resident) runs — a warm read is a memory copy, a cold read is disk-bound, and conflating them produces meaningless numbers. In containerized environments the cache is charged per memcg: a container’s page cache counts against its memory.max, so a workload that reads many files can hit its cgroup memory limit on clean, reclaimable cache and trigger per-cgroup reclaim (or, badly configured, the cgroup OOM killer) — a frequent and confusing source of “my container OOMed but was barely using anonymous memory” reports. Page-cache double-counting also matters: a file both read() and mmap()’d shares one set of folios, but reasoning about a process’s footprint via RSS vs Cached requires care (Mapped is the mmap’d subset). Finally, the steady 6.x move to large folios (multi-index page-cache entries) measurably reduces softirq and locking overhead for large sequential I/O on modern filesystems like XFS — one of the most active page-cache work areas as of the 6.12/6.18 LTS era.

See Also