page cache 不是一个单一对象,而是每个 inode 一份的"文件偏移 → 物理页帧"的索引表。核心数据结构:
struct inode
└── i_mapping ──► struct address_space
├── host (回指 inode)
├── a_ops (readpage/writepage/...)
├── i_pages ◄── XArray: pgoff_t → struct page *
└── nrpages, flags, ...
被缓存的每一页就是一个普通的物理页帧(struct page,对应一个 4 KiB RAM 帧),上面挂着:
page->mapping回指 address_spacepage->index在文件中的页偏移- 标志位:
PG_uptodate(内容与磁盘一致)、PG_dirty(待回写)、PG_locked等 - 同时挂在 LRU 链表上供 reclaim 扫描
所以"page cache"= address_space 的 XArray + 一堆带标记的普通 RAM 页帧。它不在某个固定地址,而是散落在物理内存里,由 XArray 按 (inode, file offset) 查找到。
用户进程
read/write/mmap (syscall)
│
▼
┌──────────────────┐
│ VFS │ file->f_op->read_iter / write_iter / mmap
└────────┬─────────┘
▼
┌──────────────────┐
│ filemap.c │ ←—— page cache 的入口层
│ (address_space) │ filemap_read / filemap_fault
└────────┬─────────┘ generic_perform_write
│ miss
▼
┌──────────────────┐
│ a_ops->readpage │ 文件系统(ext4/xfs/...)实现
│ ->writepage │
└────────┬─────────┘
▼
┌──────────────────┐
│ block layer │ bio → request → driver → 磁盘
└──────────────────┘
page cache 夹在 VFS 与文件系统/块层之间,是"按页缓存文件数据"的薄层。
vfs_read
└─ generic_file_read_iter
└─ filemap_read
├─ filemap_get_pages
│ ├─ xa_load(&mapping->i_pages, index) ← 查 XArray
│ └─ miss: page_cache_ra_unbounded
│ └─ a_ops->readpage → submit_bio → 等 PG_uptodate
└─ copy_page_to_iter(page, ..., iter) ← memcpy
└─ 源地址 = page_address(page) 即 direct map
关键点:copy 的源是 page 的内核 direct-map 虚拟地址,目的是用户 buf。用户态从未"看见"这个物理页。
generic_perform_write
├─ grab_cache_page_write_begin (查不到就分配并插入 XArray)
├─ a_ops->write_begin (可能预读旧内容)
├─ copy_from_user(page_address(page)+off, buf, k)
└─ a_ops->write_end (SetPageDirty, 标 inode dirty)
只标脏,不落盘。之后由 wb_workfn → writepages → a_ops->writepage 异步回写。
mmap() ← 仅建立 vma,不映射页
└─ file->f_op->mmap = generic_file_mmap
└─ vma->vm_ops = &generic_file_vm_ops
第一次访问 → page fault:
do_user_addr_fault → handle_mm_fault → do_fault
└─ __do_fault → vma->vm_ops->fault = filemap_fault
├─ 在 i_pages 中找页;miss 则 readpage
└─ vmf->page = 该 cache 页
└─ finish_fault → 把该页 PFN 写入用户 PTE
此后用户态 load/store 直接打到该物理页,与内核的 direct map 指向同一个 RAM 帧。
它的价值在性能与一致性,与权限正交:
- 避免重复 I/O:同一文件被多次 read、被多个进程打开/mmap,只读一次磁盘。
- 统一视图:
read/write与MAP_SHARED看的是同一份页帧,写穿透 → 立即可见,无需 flush。 - 写聚合 / 延迟回写:把许多小写合并成少量大 bio,磁盘吞吐显著提升。
- 预读:基于访问模式提前把后续页读入。
- 作为内存与磁盘之间的统一货架:reclaim 时按 LRU 把干净页直接丢弃、脏页回写后丢弃,让"文件数据"与"匿名内存"在内存压力下统一调度。
权限检查发生在进入这一层之前(open 时的 inode_permission、syscall 入口的 f_mode、mmap 时的 vm_flags 协商)和离开这一层之后(用户访问时 MMU 查 PTE)。page cache 处在中间,专心做"数据缓冲与索引"。