On this page
Page Cache and I/O
Coverage: address_space → radix tree/xarray → readahead → writeback → direct I/O → folio (5.16+) Kernel versions: 2.6 ~ 6.x, with a focus on folio conversion
Overview
The page cache is the most critical caching layer in the kernel—it sits between the VFS and the filesystem, caching pages read from disk. Most read() system calls never touch the disk because they hit the page cache. Similarly, most write() operations only modify pages in the page cache, while background flusher threads asynchronously write them back.
The underlying data structures of the page cache underwent significant refactoring in the 5.x series: upgrading from radix tree to xarray (5.0), and then to folio (5.16+), which elevates the basic unit of memory management from "a single 4KB page" to "a compound page that may contain multiple pages."
address_space
// include/linux/fs.h
;
Each file (inode->i_mapping) has its own address_space. The page cache is per-file: address_space->i_pages is the xarray containing all cached pages for that file.
Xarray: Replacing Radix Tree
// include/linux/xarray.h
// Introduced in 5.0: replaces the old radix tree
// Simpler API, built-in RCU protection, supports range locking
// Key operations:
void *;
void *;
void *;
// For page cache:
// index = page->index (offset in the file, in units of PAGE_SIZE)
// entry = folio (5.16+) or page
Folio: Compound Page Management
// include/linux/page-flags.h
// 5.16+: Elevates the basic unit of page cache from page to folio
;
// Why is folio needed?
// Old: Page cache used PAGE_SIZE (4KB) as the unit
// THP (2MB) required special compound page handling
// Lots of if (PageTransHuge(page)) branches everywhere
//
// New: Folio uniformly handles 4KB pages, 2MB THP, and future mTHP
// VFS and filesystems only operate on folios, without needing to know
// how many pages are inside.
// Simplifies code, reduces bugs.
Read Path: From Page Cache to bio
flowchart TD
START["read() → filemap_read()<br/>Iterate over each folio"]
START --> LOOKUP["filemap_get_entry()<br/>xa_load(&mapping->i_pages, index)"]
LOOKUP --> HIT{"page cache<br/>hit?"}
HIT -->|"✅ Hit (folio uptodate)"| COPY["copy_folio_to_iter()<br/>→ copy_to_user()<br/>→ Return to user space"]
HIT -->|"❌ Miss"| MISS["filemap_read_folio()<br/>→ readahead / read_folio"]
MISS --> ALLOC["Allocate new folio<br/>(order 0 or THP order)"]
ALLOC --> BIO["Submit bio to block layer<br/>(submit_bio)"]
BIO --> WAIT["Wait for bio completion<br/>(lock_folio)"]
WAIT --> UPTODATE["Mark folio uptodate<br/>→ Insert into xarray"]
UPTODATE --> COPY
COPY --> ACCESS["file_accessed(folio)<br/>Update LRU (mark accessed)"]
ACCESS --> DONE["Return to user space ✅"]
classDef start fill:#e3f2fd,stroke:#1565c0
classDef decision fill:#fff3e0,stroke:#ef6c00
classDef io fill:#ffebee,stroke:#c62828
classDef done fill:#e8f5e9,stroke:#2e7d32
class START start
class HIT decision
class MISS,ALLOC,BIO,WAIT,UPTODATE io
class COPY,ACCESS,DONE done
Readahead
// mm/readahead.c
// The kernel doesn't just read the requested 4KB, but "reads ahead" more
// Two types of readahead:
// 1. Synchronous readahead:
// Current read triggers a page cache miss
// → In addition to the current page, also read subsequent contiguous pages (e.g., 128KB)
// → Reduces subsequent page faults
//
// 2. Asynchronous readahead:
// Sequential read pattern detected → triggers background IO in advance
// → User space hasn't requested it yet, but the kernel is already reading
// Implemented by ondemand_readahead(), tracking read patterns
// page_cache_sync_readahead() → synchronous
// page_cache_async_readahead() → asynchronous
Write Path: Page Cache + Writeback
flowchart TD
START["write() → generic_perform_write()<br/>Iterate over each folio"]
START --> WB["a_ops->write_begin()<br/>Find or allocate folio<br/>(grab_cache_folio_write_begin)"]
WB --> RMW{"Write overwrite<br/>entire page?"}
RMW -->|"No → Partial write"| READ_IN["First read uncovered parts from disk<br/>(read-modify-write)"]
RMW -->|"Yes"| LOCK["lock_folio"]
READ_IN --> LOCK
LOCK --> COPY["copy_page_from_iter()<br/>→ copy_from_user()<br/>User data → folio"]
COPY --> WE["a_ops->write_end()"]
WE --> DIRTY["Mark folio dirty<br/>(folio_mark_dirty)"]
WE --> ISIZE["Update i_size<br/>(if file was extended)"]
WE --> UNLOCK["unlock_folio"]
DIRTY --> BALANCE
ISIZE --> BALANCE
UNLOCK --> BALANCE
BALANCE["balance_dirty_pages()<br/>Throttle: too many dirty pages?"]
BALANCE -->|"Exceeds threshold"| WAIT["Wait for background writeback"]
WAIT --> NEXT["Next folio"]
BALANCE -->|"Normal"| NEXT
classDef start fill:#e3f2fd,stroke:#1565c0
classDef decision fill:#fff3e0,stroke:#ef6c00
classDef step fill:#f3e5f5,stroke:#7b1fa2
classDef risky fill:#ffebee,stroke:#c62828
class START start
class RMW decision
class WB,READ_IN,LOCK,COPY,WE,DIRTY,ISIZE,UNLOCK,NEXT step
class BALANCE,WAIT risky
### Writeback Mechanism
```c
// fs/fs-writeback.c + mm/page-writeback.c
// Writeback is handled by background flusher threads (one per bdi)
// bdi = Block Device I/O context
// Trigger conditions:
// 1. Periodic: flusher thread wakes up every 5 seconds (dirty_writeback_centisecs)
// 2. Dirty page timeout: dirty_expire_centisecs (30 seconds)
// 3. Too many dirty pages: dirty_ratio (20% of total memory)
// 4. fsync / sync system calls
// Threshold control (via /proc/sys/vm/):
// dirty_background_ratio: Start background writeback after reaching this (default 10%)
// dirty_ratio: Block writers after reaching this (default 20%)
Direct I/O
// fs/direct-io.c (old) / fs/iomap/direct-io.c (new, iomap based)
// O_DIRECT: Bypass page cache, read/write directly to disk
// Key constraints:
// 1. Must be sector-aligned (512B / 4KB)
// 2. Write operations are synchronous (won't "fake complete" due to dirty page cache)
// 3. No kernel cache between user buffer and disk (page cache is not involved)
// Implementation (iomap version):
├─ Data submitted directly via bio → block device
├─ User buffer pages are
├─ Unpin directly after bio completion
└─ No page cache involvement required
Debugging
# Page cache usage
|
# Cached: page cache (file pages)
# Dirty: dirty pages (pending writeback)
# Writeback: pages currently being written back
# Manually trigger writeback
# View page cache per file
# Writeback statistics
|
References and Further Reading
- Kernel Documentation:
Documentation/filesystems/vfs.rst,Documentation/core-api/xarray.rst - LWN:
- "The folio conversion" series (lwn.net/Articles/849538/)
- "Better writeback with iomap" (lwn.net/Articles/869187/)
- Source Files:
mm/filemap.c— page cache core (read path)mm/page-writeback.c— writeback thresholds and throttlingfs/fs-writeback.c— flusher threadsmm/readahead.c— readaheadlib/xarray.c— Xarray implementationinclude/linux/page-flags.h— folio definition
Keywords: page cache, address_space, xarray, folio, readahead, writeback, flusher, O_DIRECT, iomap, dirty_ratio