K05 —— Page Cache:文件数据和匿名内存的统一缓存
金句:Linux 不会浪费任何一块内存——它把空闲的内存都变成了缓存。
Page Cache 的概念
Page Cache 是 Linux 用于缓存文件数据和匿名内存的机制。它把物理内存变成磁盘块的缓存,既加速文件读写,又管理进程的匿名内存。
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 流程
读取文件时(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 的索引结构
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
匿名页面(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 的预读
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不着急」,回复”资料”获取内存学习路线图。
评论