内存管理-77-kernel-internals系列-Memory专题-Caching & Reclaim-1-Page Cache
一、页缓存 (Page Cache)翻译
注: 翻译自 https://kernel-internals.org/mm/page-cache/
在内存中缓存文件数据。
1. 什么是页缓存?
页缓存将最近访问过的文件数据保存在 RAM 中。当您读取文件时,数据会留在内存中——后续的读取将直接来自 RAM,而不是磁盘。
首次读取:应用程序 ──► 内核 ──► 存储设备(速度慢:机械硬盘 ~10ms,NVMe 固态硬盘 ~100μs)
│
▼
页缓存(保存副本)
再次读取:应用程序 ──► 内核 ──► 页缓存(速度快,~100ns,不触及存储设备)
2. 为什么需要页缓存?
2.1 速度差距:
---------------------------------------------------------------------- 存储介质 延迟 (Latency) 带宽 (Bandwidth) ---------------------------------------------------------------------- RAM ~100ns ~100 GB/s NVMe SSD ~100µs ~7 GB/s SATA SSD ~100µs ~600 MB/s HDD(机械硬盘) ~10ms ~200 MB/s ----------------------------------------------------------------------
(以上为数量级估计;详情可参考“每个程序员都应该知道的延迟数字”。)
RAM 的速度比存储设备快 1000 到 100,000 倍。缓存利用了时间局部性(temporal locality)——最近被访问过的数据在不久 retail 极有可能再次被访问。
2.2 核心优势
• 提升读取性能:热数据直接从 RAM 中提供。
• 写入缓冲:写入操作可以批量处理,无需同步写入磁盘。
• 数据共享:多个进程可以共享缓存中的同一个页面。
• 提高内存效率:未使用的 RAM 会自动转化为缓存。
3. 工作原理
3.1 读取文件
read(fd, buf, 4096) │ ▼ 在缓存中查找页面? ─── 是 ──► 复制到用户缓冲区 (buf) │ └─ 否 ─► 分配页面 ─► 从磁盘读入页面 ─► 添加到页缓存 ─► 复制到用户缓冲区
3.2 写入文件
write(fd, buf, 4096) │ ▼ 在缓存中查找/分配页面 │ ▼ 将数据从用户缓冲区复制到页面中 │ ▼ 将该页面标记为 脏页 (Dirty) │ ▼ 返回应用程序(写入“完成”) │ ▼(后续,异步执行) 回写线程 (Writeback thread) 将数据刷新到磁盘
写入操作会立即返回——数据此时已进入缓存并被标记为脏页。实际的磁盘 I/O 由后台的回写机制处理。
3.3 address_space 结构体
每个文件(inode)都有一个 address_space 结构体来管理其缓存页:
struct address_space {
struct inode *host; /* Owning inode */
struct xarray i_pages; /* Cached pages */
unsigned long nrpages; /* Number of cached pages */
const struct address_space_operations *a_ops; /* Operations */
/* ... */
};
页面在 XArray 中按文件偏移量进行索引,从而实现快速查找。
4. 脏页与回写
4.1 脏页生命周期:
干净页 (Clean page) ──► write() ──► 脏页 (Dirty page) ──► 回写 (writeback) ──► 干净页 (Clean page)
│
▼
在脏页列表中追踪
4.2 回写触发条件
----------------------------------------------------------------------------------------- 触发器 (Trigger) 触发时机 (When) ----------------------------------------------------------------------------------------- 定期触发 (Periodic) 每隔 dirty_writeback_centisecs 时间(默认 5 秒) 内存压力 (Memory pressure) 当页面回收(Reclaim)需要释放内存空间时 显式刷新 (sync/fsync) 应用程序发出显式的刷新请求时 脏页阈值 (Dirty threshold) 全系统范围内的脏页数量过多时 脏页生存期 (Dirty age) 页面保持脏状态的时间超过了 dirty_expire_centisecs 时 -----------------------------------------------------------------------------------------
4.3 脏页限制参数
这些参数的默认值定义在内核源码的 mm/page-writeback.c 中;相关的可调优项在虚拟内存系统控制文档("vm sysctl docs")中进行了详细记载:
# Percentage of memory that can be dirty. 20 (default) - start blocking writers at 20% cat /proc/sys/vm/dirty_ratio # Background writeback threshold. 10 (default) - start background writeback at 10% cat /proc/sys/vm/dirty_background_ratio # Time-based settings (centiseconds). 3000 (30s) - pages older than this get written cat /proc/sys/vm/dirty_expire_centisecs # 500 (5s) - writeback thread wakes this often, 6.1内核实测是 300 cat /proc/sys/vm/dirty_writeback_centisecs
4.4 回写线程
每个块设备都拥有专属的后台回写工作线程,可通过 ps -AT | grep writeback 进行查看。下面是 X86 的:
# Per-device writeback threads. kworker/u8:0+flush-8:0 (for device 8:0) ps aux | grep writeback
5. 读取策略
5.1 预读 (Readahead)
内核会预测文件的顺序访问行为并提前进行读取:
应用程序读取第 0 页
│
▼
内核检测到顺序访问模式
│
▼
提前将第 1, 2, 3, 4... 页加载到缓存中
│
▼
当应用程序读取第 1 页时,该页已经被缓存了
相关文件:
# 默认预读大小(单位:KB), 128 (默认值) cat /sys/block/sda/queue/read_ahead_kb # 根据业务负载进行调整 echo 256 > /sys/block/sda/queue/read_ahead_kb
5.2 mmap() vs read()
----------------------------------------------------------------------------- 方法 机制 最适用场景 ----------------------------------------------------------------------------- read() 将数据从缓存复制到用户缓冲区 顺序访问、简单的 I/O mmap() 将缓存页直接映射到进程的地址空间 随机访问、大文件 -----------------------------------------------------------------------------
通过 mmap(),页缓存的页面可以直接映射到进程的地址空间中:
/* ptr directly references page cache pages - true zero-copy */
void *ptr = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
/* With MAP_PRIVATE, writes trigger COW (copy-on-write). Reads are zero-copy, but writes get a private copy */
void *ptr = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
6. 内存压力 (Memory Pressure)
页缓存作为“空闲”内存 (Page Cache as "Free" Memory)
Linux 会将“空闲”的物理内存(RAM)用于页缓存。当应用程序需要内存时:
(1) 干净的缓存页(Clean cache pages)会被立即释放(丢弃)。
(2) 脏的缓存页(Dirty cache pages)会被先写回到磁盘中,然后再释放。
(3) 腾出的内存将分配给应用程序。
# 查看内存细节划分 cat /proc/meminfo | grep -E "MemFree|Cached|Buffers|Available" # MemFree: 1234567 kB (真正未被使用的物理内存) # Cached: 8765432 kB (页缓存大小) # MemAvailable: 9876543 kB (可用内存:包含空闲内存 + 可回收的缓存)
7. 清理缓存 (Drop Caches - 仅用于测试)
可以通过向 /proc/sys/vm/drop_caches 写入不同数值来手动清理缓存(通常用于性能测试)。
# Drop clean caches (for benchmarking) sync # Write dirty pages first echo 3 > /proc/sys/vm/drop_caches # 1 = page cache # 2 = dentries/inodes # 3 = both
这两个命令的作用是不一样的。
8. 监控(Monitoring)
全系统缓存统计信息
# Disk I/O counters (not direct cache hit/miss - includes readahead, direct I/O)
# pgpgin - Pages read from disk (KB)
# pgpgout - Pages written to disk (KB)
cat /proc/vmstat | grep -E "pgpgin|pgpgout"
# Page cache size and state
# Cached: Page cache size (file-backed pages in memory)
# Buffers: Block device metadata cache
# Dirty: Pages modified, waiting to be written
# Writeback: Pages currently being written to disk
cat /proc/meminfo | grep -E "Cached|Buffers|Dirty|Writeback"
注意:Linux 不提供直接的“缓存命中率”计数器。`pgpgin` 数值高而 Cached(缓存)增长缓慢,表明存在缓存压力。
8.1 每文件的缓存状态
# Check if file is cached (using vmtouch or fincore) # Files: 1 # Pages: 1000 (cached: 800) vmtouch -v /path/to/file # Or with fincore (coreutils) fincore /path/to/file
8.2 Tracing
# Trace page cache events echo 1 > /sys/kernel/debug/tracing/events/filemap/mm_filemap_add_to_page_cache/enable echo 1 > /sys/kernel/debug/tracing/events/writeback/writeback_dirty_page/enable cat /sys/kernel/debug/tracing/trace_pipe
9. 演进历史 (Evolution)
起源 (1990s): 页缓存自 Linux 早期版本就已存在。最初,它与用于块设备的“缓冲区缓存(buffer cache)”是相互独立的。
统一页缓存 (v2.4, 2001): 缓冲区缓存被合并到了页缓存中。自此,所有的文件 I/O 都统一通过同一个缓存进行处理。
基数树 (v2.6): 页面被组织在基数树(radix tree)中,以便通过文件偏移量进行快速查找。
XArray (v4.20, 2018): 提交记录:a28334862993 ("page cache: Finish XArray conversion") | LKML
作者:Matthew Wilcox
基数树被替换为 XArray —— 它的 API 更加简洁,同时保持了相同的性能表现。
Folios (v5.16, 2022)
提交记录:7b230db3b8d3 ("mm: Introduce struct folio") | LKML
作者:Matthew Wilcox
Folio 将一个或多个物理上连续的页面表示为一个单一单元。这种抽象减少了页缓存中大页(huge pages)的管理开销,并消除了过去关于“某个函数到底是作用于头页(head page)还是任意普通页面”的混乱定义。
10. 直接 I/O (Bypassing Cache / 直接 I/O)
某些应用程序会完全绕过页缓存:
/* 读取和写入将直接绕过页缓存. 要求用户缓冲区和 I/O 大小必须对齐 */ int fd = open("file", O_RDWR | O_DIRECT);
适用场景:
• 拥有自身独立缓存系统的数据库。
• 避免双重缓存(Double-caching)。
• 需要可预测的延迟表现。
权衡与代价:
• 无法享受预读(Readahead)带来的优势。
• 没有写入缓冲(Write buffering)。
• 应用程序必须自己负责和实现缓存管理。
11. 常见问题 (Common Issues)
11.1 缓存抖动 (Cache Thrashing)
工作集(Working set)的大小超过了系统当前可用的 RAM。
• 症状:高 pgpgin / pgpgout,I/O 速度变得极慢。
• 解决方案:增加物理内存(RAM)。减小应用程序的工作集。对特定的业务负载采用 O_DIRECT。
11.2 脏页积压 (Dirty Page Buildup)
过多的脏页导致写入操作发生停顿(Stalls)。
• 症状:进程在 balance_dirty_pages 中被阻塞。
• 解决方案:调低 dirty_ratio 阈值。使用速度更快的高性能存储设备。限制或降低写入速率。
11.3 预读不匹配 (Readahead Mismatch)
顺序预读机制反而损害了随机访问型负载的性能。
• 解决方案:使用 posix_fadvise(POSIX_FADV_RANDOM) 显式告知内核。减少 read_ahead_kb 的大小。针对 mmap 使用 madvise(MADV_RANDOM)。
12. 参考(References)
...
posted on 2026-10-08 13:49 Hello-World3 阅读(2) 评论(0) 收藏 举报
浙公网安备 33010602011771号