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
struct address_space {
    struct inode        *host;              // owning inode
    struct xarray       i_pages;            // page cache (page->index → page)
    
    unsigned long       nrpages;            // total number of cached pages
    unsigned long       nrexceptional;      // shadow/swap entries

    const struct address_space_operations *a_ops;
    // → read_folio, writepages, readahead, write_begin, write_end, ...

    struct list_head    i_private_list;
    struct address_space *private_data;
};

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 *xa_store(struct xarray *xa, unsigned long index, void *entry, gfp_t gfp);
void *xa_load(struct xarray *xa, unsigned long index);
void *xa_erase(struct xarray *xa, unsigned long index);

// 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

struct folio {
    struct page page;           // "head page"
    // A folio can contain multiple consecutive pages
    //   folio_nr_pages(folio)  returns the total number of pages
    //   folio_page(folio, n)   returns the nth sub-page
};

// 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):
__iomap_dio_rw()
  ├─ Data submitted directly via bio → block device
  ├─ User buffer pages are pinned (get_user_pages_fast)
  ├─ Unpin directly after bio completion
  └─ No page cache involvement required

Debugging

# Page cache usage
cat /proc/meminfo | grep -E 'Cached|Dirty|Writeback|WritebackTmp'
# Cached:     page cache (file pages)
# Dirty:      dirty pages (pending writeback)
# Writeback:  pages currently being written back

# Manually trigger writeback
sync
echo 3 > /proc/sys/vm/drop_caches  # Clear page cache (for testing)

# View page cache per file
vmtouch /path/to/file  # Requires installing vmtouch

# Writeback statistics
cat /proc/vmstat | grep -E 'nr_dirty|nr_writeback|pgwrite'

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 throttling
    • fs/fs-writeback.c — flusher threads
    • mm/readahead.c — readahead
    • lib/xarray.c — Xarray implementation
    • include/linux/page-flags.h — folio definition

Keywords: page cache, address_space, xarray, folio, readahead, writeback, flusher, O_DIRECT, iomap, dirty_ratio