K05 —— Page Cache:文件数据和匿名内存的统一缓存

金句:Linux 不会浪费任何一块内存——它把空闲的内存都变成了缓存。


Page Cache 的概念

Page Cache 是 Linux 用于缓存文件数据和匿名内存的机制。它把物理内存变成磁盘块的缓存,既加速文件读写,又管理进程的匿名内存。

Bash
Page Cache 的核心思想:

1. 所有磁盘 I/O 都经过 Page Cache
   - 读文件:先查 Page Cache,命中则直接返回
   - 写文件:先写入 Page Cache,定期写回磁盘

2. Page Cache 和 buddy system 的关系
   - Page Cache 的页面来自 buddy system
   - 释放 Page Cache 时,页面归还 buddy system

3. 两种页面类型:
   - file-backed page:映射到文件的页面
   - anonymous page:进程的堆/栈,没有文件对应

两者都缓存在 Page Cache 中,都受 LRU 管理。

文件读写的 Page Cache 流程

Bash
读取文件时(read syscall):

1. 磁盘文件 → Page Cache → 用户 Buffer

   read(fd, buf, size):
   → VFS 接收请求
   → 查找 Page Cache(radix tree)

   Cache Hit:
     → 直接从 Page Cache 读取(无磁盘 I/O)
     → 耗时:~100ns(内存访问)

   Cache Miss:
     → 从 buddy system 分配页面
     → 发起磁盘 I/O(读写磁盘)
     → 数据写入 Page Cache
     → 返回给用户

2. 写文件时(write syscall):

   write(fd, buf, size):
   → 直接写入 Page Cache(不立即写磁盘)
   → 设置页面为 dirty
   → 标记 inode 为 dirty
   → 立即返回(快速写入)
   → 后台 pdflush/flush 线程写回磁盘(延迟写回)

   好处:写入比实际磁盘 I/O 快 100~1000

radix tree:Page Cache 的索引结构

Bash
Page Cache 用 radix tree 快速查找:

struct address_space {
    struct radix_tree_root page_tree; // radix tree 根
    spinlock_t tree_lock;             // 保护锁
    unsigned long nrpages;            // 页面总数
    // ...
};

查找 key = (inode, offset):
  → 线性扫描太慢 → 用 radix tree
  → O(log N) 查找,N = 页面数量
  → 每个文件一个 radix tree

radix tree 的特点:
  - 稀疏数组(sparse array)
  - 只有存在的页面才占用节点
  - 节省内存(不需要预分配整个数组)
  - 查找:给定文件偏移,快速找到对应页面

radix_tree_insert():
  → 把 page 对象插入 tree
  → key = offset >> PAGE_SHIFT

radix_tree_lookup():
  → 给定 offset,查找 page
  → 返回 struct page *

匿名页面和 Page Cache 的 LRU

Bash
匿名页面(anonymous pages)也走 LRU:

当物理内存紧张时,kswapd 回收页面:
  - 优先回收文件页(干净 → 直接释放,脏 → 写回)
  - 其次回收匿名页(需要 swapout 到磁盘)

LRU 链表结构:

struct zone {
    // 文件页链表
    struct list_head[NR_LRU_LISTS] fileInactive;
    struct list_head[NR_LRU_LISTS] fileActive;

    // 匿名页链表
    struct list_head[NR_LRU_LISTS] anonInactive;
    struct list_head[NR_LRU_LISTS] anonActive;
};

LRU_LIST 分类:
  - LRU_INACTIVE_ANON:匿名冷页(最近没访问)
  - LRU_ACTIVE_ANON:匿名热页(最近访问过)
  - LRU_INACTIVE_FILE:文件冷页
  - LRU_ACTIVE_FILE:文件热页

回收优先级(默认):
  1. file cold(最优先,因为直接释放)
  2. anon cold(需要 swapout)
  3. file hot(脏文件先写回)
  4. anon hot(最不愿意回收)

Page Cache 的预读

Bash
Page Cache 的预读(readahead):

预读的原理:
  - 应用可能顺序访问文件
  - 内核预测下一步访问,提前把页面加载到 Page Cache
  - 减少磁盘 I/O 等待

预读触发(典型):
  1. 应用顺序读文件(read(fd, buf, size))
  2. Page Cache miss → 分配页面 → 从磁盘读
  3. 检测到顺序访问模式
  4. 预读 2~4 个额外页面(提前到 Page Cache)

查看预读效果:
  $ strace -e trace=read ./program 2>&1 | head -20

  read(3, "hello world...", 4096) = 4096
  → 每次 read 4KB

  $ cat /proc/self/maps
  7f... rw-p ... // 内存映射文件

  如果是 mmap,顺序访问会触发 readahead:
  → 内核自动把后续页面读入 Page Cache

总结

  • Page Cache:所有磁盘 I/O 的缓存,文件读写都经过它
  • Cache Hit/Miss:Hit 直接返回(~100ns),Miss 分配页→磁盘 I/O
  • radix tree:每个文件一个 radix tree,key=offset,O(log N) 查找
  • 匿名页:匿名页面(heap/stack)也走 LRU,回收时需要 swapout
  • LRU 分类:file cold > anon cold > file hot > anon hot(回收优先级)
  • 预读(readahead):顺序访问时,内核提前加载后续页面到 Page Cache

下篇预告(K06):用户程序的 mmap 申请内存时,内核是怎么一步步分配物理页的?demand paging、page fault 处理、以及匿名映射的内存申请流程。


关注公众号「AI不着急」,回复”资料”获取内存学习路线图。

最后修改: 2024年5月10日

作者

评论

发表评论

您的邮箱地址不会被公开。