页缓存(page cache)把磁盘文件内容缓存在内存——read/write/mmap 几乎都经过它。D-29 讲了 VM 模型;这里从 VM 视角深入:address_space 如何组织页、folio 改造、readahead 怎么决策、writeback 怎么限速。
一、先看图
flowchart LR
READ[用户 read/mmap] --> AS[address_space<br/>XArray 索引]
AS -->|hit| PAGE[folio<br/>在内存]
AS -->|miss| RA[readahead 决策]
RA --> BIO[submit_bio<br/>读盘]
BIO --> PAGE
WRITE[用户 write] --> PAGE
PAGE -->|dirty| WB[writeback<br/>flush thread]
WB --> DISK[磁盘]
MEMCG[memcg 限制] -.限速.-> WB
classDef sw fill:#388bfd22,stroke:#388bfd,color:#adbac7;
classDef io fill:#f0883e22,stroke:#f0883e,color:#adbac7;
class READ,WRITE,AS,PAGE,MEMCG sw
class RA,BIO,WB,DISK io
二、address_space
每个 inode 有一个
address_space(inode->i_mapping),管理此文件的所有缓存页。
struct address_space {
struct inode *host;
struct xarray i_pages; // XArray 索引(原 radix tree)
gfp_t gfp_mask;
atomic_t i_mmap_writable;
struct rb_root_cached i_mmap; // file-backed VMA rmap(interval tree)
const struct address_space_operations *a_ops;
...
};a_ops 包含
read_folio、writepages、dirty_folio
等回调——文件系统实现它们。
三、radix tree → XArray
5.x 以前用 radix
tree(RADIX_TREE)索引页。Matthew Wilcox
主导改造成 XArray:
- API 更清晰(
xa_load/xa_store) - 内建 multi-index entry(一个 slot 代表一组连续页 → folio)
- RCU-safe
- tag 机制替代 radix tree tag
6.x 内核里再看不到 radix_tree_lookup——全部变
xa_*。
四、folio:告别 compound page 混乱
4.1 问题
THP 引入 compound page(head page + tail page),但旧 API
分不清”这个 struct page 到底是 head 还是
tail”,代码里充满 compound_head() 调用和
bug。
4.2 folio 方案
struct folio = “逻辑上的一整页(1 个或 2^n
个物理页)”。用 folio 就不会踩到 tail page。
struct folio {
// 嵌入 struct page(head page)
unsigned long flags;
struct address_space *mapping;
pgoff_t index;
void *private;
atomic_t _mapcount;
atomic_t _refcount;
unsigned int _nr_pages; // 这个 folio 有几页
...
};好处:
- 消除 head/tail 歧义
- 一次 I/O 覆盖整个 folio(2M THP = 一个 folio)
- locking / refcount 操作在 folio 粒度
6.x 的 readahead、writeback、reclaim 路径基本 folio 化。
五、readahead
readahead 预读是减少 major fault 的核心:
5.1 算法
ondemand_readahead:检测顺序读模式,动态扩大
readahead window(初始 128KB → 最大
read_ahead_kb,默认 128KB-2MB)。
随机读 → readahead 关闭(只读当前页)。
5.2 触发
filemap_fault → do_async_mmap_readahead (触发异步预读)
generic_file_read_iter → filemap_read → page_cache_sync_readahead (同步读)
5.3 调优
cat /sys/block/sda/queue/read_ahead_kb # 默认 128
echo 2048 > /sys/block/sda/queue/read_ahead_kb # 大文件顺序读
# 或 per-file
posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL); # 提示顺序
posix_fadvise(fd, 0, 0, POSIX_FADV_RANDOM); # 关闭预读六、writeback 与 dirty throttling
6.1 脏页限速
内核不允许脏页占太多内存——否则突然写盘卡住。balance_dirty_pages
在脏页比例超过阈值时开始”限速”写入进程。
两个阈值:
vm.dirty_ratio = 20 # 全局脏页占可用内存 20% 时,进程同步等
vm.dirty_background_ratio = 10 # 10% 时唤醒 flush 线程后台写
6.2 flush 线程
kworker/u*:*+flush-*(workqueue
flush):
- 定时(dirty_writeback_centisecs,默认 5 秒)
- 阈值触发
- sync/fsync 触发
6.3 memcg 交互
cgroup v2 的 memory.max
限制包含页缓存。memcg 内 dirty 页超限 → 触发本 cgroup 的
writeback。
memory.stat 里 file_dirty / file_writeback 看 cgroup 级脏页。
七、页缓存淘汰
见 D-36 详细讲;简述:
- inactive list → 扫描 → 回收
- file-backed clean 页直接丢(再读回就行)
- dirty 页先 writeback 再回收
- MGLRU 替代经典 LRU
八、常见问题
A:大文件拷贝占满 RAM 正常——page cache
吃光闲置内存。回收机制保证压力时释放。如要避免:posix_fadvise(DONTNEED)
或 dd iflag=direct。
B:dirty page 积压导致延迟抖动 降低
vm.dirty_ratio /
vm.dirty_background_ratio;或用 cgroup
限单个服务。
C:频繁 drop_caches
echo 3 > /proc/sys/vm/drop_caches 丢掉所有
page cache——短暂降内存但之后 I/O 骤增。生产不推荐。
D:mmap 读大文件 vs read mmap 少一次 copy(零拷贝),但 page fault 开销 + mmap_lock 争抢。小文件/随机读 mmap 不占优。
九、观察
free -h # buff/cache 列
cat /proc/meminfo | grep -E 'Cached|Dirty|Writeback|AnonPages'
vmtouch /path/to/file # 看特定文件缓存状态
sar -r 1 # kbcached 列
# 脏页实时
watch -n1 grep -E 'Dirty|Writeback' /proc/meminfo十、小结
- address_space + XArray 索引每个文件的缓存页
- folio 消除 compound page 歧义,统一大小页处理
- readahead 靠顺序检测预读,减少 major fault
- dirty throttling 限速写入、flush 线程后台回写
- memcg 让页缓存约束在容器内
参考文献
mm/filemap.c、mm/readahead.c- Matthew Wilcox, “Folios.” LPC 2021
- Matthew Wilcox, “XArray.” LWN.net 2017
Documentation/admin-guide/mm/concepts.rst- Fengguang Wu, “Dynamic readahead and dirty throttling.” 2012
工具
vmtouch、fincore/proc/meminfosar -r、sar -Bbpftrace+ filemap tracepoints
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【操作系统百科】VFS I/O 路径全景
一次 read(fd, buf, n) 从用户态到磁盘要穿过多少层?本文追踪 VFS read/write 全路径——file_operations、iter_iov、iomap、页缓存命中与 miss、直接 I/O 旁路、块层提交、fsnotify 插桩。
【存储工程】Page Cache 深度解析
应用程序每一次 read() 或 write() 系统调用,感觉像是直接在操作磁盘上的文件,但实际上,内核在中间插入了一层透明的缓存——页缓存(Page Cache)。这层缓存用物理内存保存最近访问过的文件数据,使得绝大多数读操作不需要触发磁盘 I/O,而写操作可以先落到内存,再由后台线程异步刷回存储设备。
【操作系统百科】内存回收
Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。
【操作系统百科】交换
swap 还值得开吗?本文讲 swap area 基础、swap cache、zram 压缩内存、zswap 前端压缩池、swappiness 的真实含义、容器里的 swap 策略,以及为什么现代 Android 全靠 zram 不靠磁盘。