Linux 页缓存(Page Cache)与回写(Writeback)机制详解
Linux 页缓存(Page Cache)与回写(Writeback)机制详解
面向 Linux 4.x~6.x 的通用说明,结合通用 VFS/block/内存子系统,系统性讲解页缓存的读写路径、脏页管理与回写调度,并提供观测与调优建议。示意图为概念化,具体实现以内核源码为准(4.4.94 周期参考:mm/
, fs/
, block/
)。
1. 为什么需要页缓存
- 降低磁盘 I/O:把文件页缓存在内存,提升命中率与吞吐。
- 合并写入:把多次写入聚合为顺序写,减少随机写代价与设备磨损。
- 隔离设备速度:进程写入落入内存,后台异步回写到设备,降低写延迟。
2. 总览:读/写/回写三条主路径
用户态 read/write → VFS → 文件系统 → address_space/page cache →读:命中→拷贝到用户缓冲;未命中→页分配+IO读入→入cache→返回写:覆盖/追加→标记脏页→落入 cache;必要时触发回写/比例回写回写:后台/同步→把脏页刷到设备→清理脏标记→可回收
关键参与者:
address_space
:每个 inode 的缓存视图,管理其页缓存(radix/xarray)。page
:内核页结构(通常 4KB)。包含标志位:PG_uptodate
、PG_dirty
、PG_writeback
。writeback
控制:bdi
、wb
(writeback),比例回写、回写队列、背压机制。
3. 读路径(命中与缺页)
read()├─ 查 address_space → xarray 寻找目标文件页├─ 命中:PG_uptodate=1 → 拷贝到用户缓冲区└─ 未命中:├─ 分配 page(可能从 LRU 活跃/不活跃回收,或直接分配)├─ 提交 BIO/REQ 读盘 → 完成后置 PG_uptodate=1└─ 将 page 加入 address_space(可参与 LRU)
要点:
- 页缓存命中率决定读性能;readahead(预读)提升顺序访问吞吐。
- 内核维护文件页的 LRU,以冷热分层实现内存压力下的回收。
4. 写路径(脏页生成与合并)
write()/pwrite()/memcpy-to-mapped-file├─ 定位/创建 page(address_space)├─ 用户数据拷贝到 page → 标记 PG_dirty=1├─ 可能合并多个小写入到相邻页(文件系统层做聚合/日志)└─ 返回(异步);后台策略按脏比例与期限决定何时回写
要点:
- 脏页不会立刻写盘(除非显式
fsync
/O_SYNC
/O_DIRECT
)。 - 顺序写通常被合并,形成更大的顺序 IO;随机写可能导致写放大。
- 写入过快会触发背压(balance_dirty_pages),降低写入速率以避免脏页失控。
5. 回写机制(writeback 调度与比例回写)
回写的触发来源:
- 周期性回写(pdflush/
flush-*
线程,现代为wb
工作线程)。 - 比例回写:脏页占比超过阈值触发(
vm.dirty_ratio
/vm.dirty_background_ratio
)。 - 显式同步:
fsync
/sync
/msync
/O_SYNC
。 - 内存压力:回收路径遇到脏页,可能触发同步回写以释放页框。
核心控制参数(sysctl):
vm.dirty_background_ratio
/vm.dirty_background_bytes
:后台开始写回的触发阈值。vm.dirty_ratio
/vm.dirty_bytes
:强制进程进入比例回写的阈值(更高)。vm.dirty_expire_centisecs
:脏页“过期”年龄,过期更易被刷写。vm.dirty_writeback_centisecs
:后台回写周期。
比例回写与背压流程(简化):
进程写入过快 → 脏页占比↑├─ 背景阈值:触发后台 writeback 线程刷写(降低脏占比)└─ 比例阈值:触发 balance_dirty_pages → 限速当前写进程后台 writeback:├─ 选择 inode 队列(按脏量、过期、cgroup)├─ submit BIO(顺序聚合、文件系统日志/元数据)└─ IO 完成 → 清 PG_writeback → 清 PG_dirty → 页可回收
6. 页回收与 writeback 的协作
- 页面回收(kswapd/直接回收)在遇到
PG_dirty
会优先触发回写;PG_writeback
表示正在写盘,避免重复提交。 - 干净页(PG_dirty=0)可直接回收;脏页需先写回再回收。
- 文件页与匿名页分离管理:匿名页靠 swap 回收;文件页靠 writeback。
7. O_DIRECT、缓存绕过与 fsync
O_DIRECT
:应用绕过页缓存,直接与块设备交互(仍受文件系统与设备对齐约束),适合数据库等自管缓存场景。fsync
/fdatasync
:要求持久化,触发对应 inode 的数据与元数据写回;可能导致显著 IO 峰值。mmap
写:落入页缓存并标脏,msync
可同步到盘。
8. NUMA/IO 调度器与硬件影响
- NUMA:页缓存页的物理分配遵循本地优先;跨节点 IO 影响延迟。
- IO 调度器:CFQ/Deadline/None(现代多为 blk-mq+设备队列);顺序聚合与队列深度影响吞吐。
- 设备类型:SSD/HDD/NVMe 的写放大与并行度差异显著,回写参数需按设备调优。
9. 观测与诊断
- 系统级:
cat /proc/meminfo | egrep 'Dirty|Writeback'
、vmstat 1
、iostat -x 1
、sar -B 1
、perf stat -e block:*
。 - 进程级:
/proc/<pid>/smaps
中的文件映射、perf trace
系统调用、blktrace
设备 IO 路径。 - 文件系统:
/proc/sys/vm/
下脏页参数、/sys/fs/ext4/*
日志与提交(不同 fs 差异)。
10. 调优建议
- 顺序化写入、增大合并机会;随机写考虑日志型文件系统或设备缓存策略。
- 控制脏页上限:适当下调
dirty_ratio
,上调/设置dirty_background_bytes
。 - 高吞吐写入:增大回写周期与队列深度;评估设备写缓存(
write cache
)。 - 延迟敏感:降低
dirty_expire_centisecs
,更快刷写;关键路径使用O_DIRECT
/fsync
。 - 与应用缓存协调:数据库/存储系统避免双缓存(页缓存+应用缓存)。
11. 流程与结构图(ASCII)
读路径(Page Cache 命中/缺页):
[用户 read()]|v
[ VFS 层 ] -> [ 文件系统 ] -> [ address_space / page cache ]|+-----------+-----------+| |命中(PG_uptodate=1) 未命中| |v v拷贝到用户缓冲区 分配 page -> 提交读IO|IO完成置 PG_uptodate=1|加入 cache -> 返回用户
写路径(脏页与回写调度):
[用户 write()/pwrite()/mmap写 ]|v
[ VFS / 文件系统 ]|v
[ address_space / page cache ] -- 创建/定位 page|v
置 PG_dirty=1(标脏) ─────────────────────┐| |v |
异步返回给用户 || |背景写回触发(dirty_background_*) |或比例回写限速(dirty_ratio) || |v v[ writeback 线程 ] <─ balance_dirty_pages(限速)|v
提交写IO -> 置 PG_writeback=1|v
IO完成 -> 清 PG_writeback -> 清 PG_dirty|v
页可被回收(LRU)
回收协作(vmscan × writeback):
[ 内存压力 / kswapd / 直接回收 ]|v
扫描 LRU(文件页/匿名页分离)|v
遇到页:├─ 干净页(PG_dirty=0):直接回收├─ 脏页(PG_dirty=1):触发回写 → 设置 PG_writeback│ 写IO完成 → 清 PG_writeback/PG_dirty → 可回收└─ 匿名页:走 swap(非页缓存),独立回收路径
12. 参考
- 源码路径:
mm/page-writeback.c
、mm/vmscan.c
、fs/buffer.c
、include/linux/writeback.h
- 文档:
Documentation/admin-guide/mm/
下相关条目(新内核),Documentation/filesystems/
- 工具:
blktrace
、fio
、perf
、iostat
、vmstat
13. 源码解读(内核关键片段)
以下片段基于 4.4.94 内核,选取与“标脏→背压→回写→回收协作”直接相关的核心函数,便于将概念与实现对齐。
- 脏页背压入口:
balance_dirty_pages_ratelimited()
(mm/page-writeback.c
)
void balance_dirty_pages_ratelimited(struct address_space *mapping)
{struct inode *inode = mapping->host;struct backing_dev_info *bdi = inode_to_bdi(inode);struct bdi_writeback *wb = NULL;int ratelimit;int *p;if (!bdi_cap_account_dirty(bdi))return;if (inode_cgwb_enabled(inode))wb = wb_get_create_current(bdi, GFP_KERNEL);if (!wb)wb = &bdi->wb;ratelimit = current->nr_dirtied_pause;if (wb->dirty_exceeded)ratelimit = min(ratelimit, 32 >> (PAGE_SHIFT - 10));/* 省略:per-CPU 限速状态更新与泄漏补偿 */if (unlikely(current->nr_dirtied >= ratelimit))balance_dirty_pages(mapping, wb, current->nr_dirtied);wb_put(wb);
}
要点:应用侧频繁写入时,内核依据 dirty_*
阈值触发比例回写与限速;nr_dirtied_pause
控制调用频率,wb->dirty_exceeded
下调阈值以快速收敛。
- 回写遍历:
write_cache_pages()
(mm/page-writeback.c
)
int write_cache_pages(struct address_space *mapping,struct writeback_control *wbc,writepage_t writepage, void *data)
{int ret = 0, done = 0, nr_pages, tag;struct pagevec pvec;pgoff_t index, end, done_index;int cycled, range_whole = 0;/* 根据 sync 模式选择 TOWRITE 或 DIRTY 标签 */if (wbc->sync_mode == WB_SYNC_ALL || wbc->tagged_writepages)tag = PAGECACHE_TAG_TOWRITE;elsetag = PAGECACHE_TAG_DIRTY;retry:if (wbc->sync_mode == WB_SYNC_ALL || wbc->tagged_writepages)tag_pages_for_writeback(mapping, index, end);done_index = index;while (!done && (index <= end)) {nr_pages = pagevec_lookup_tag(&pvec, mapping, &index, tag,min(end - index, (pgoff_t)PAGEVEC_SIZE-1) + 1);if (nr_pages == 0)break;for (int i = 0; i < nr_pages; i++) {struct page *page = pvec.pages[i];lock_page(page);if (unlikely(page->mapping != mapping)) { unlock_page(page); continue; }if (!PageDirty(page)) { unlock_page(page); continue; }if (PageWriteback(page)) {if (wbc->sync_mode != WB_SYNC_NONE)wait_on_page_writeback(page);else { unlock_page(page); continue; }}if (!clear_page_dirty_for_io(page)) { unlock_page(page); continue; }ret = (*writepage)(page, wbc, data);if (--wbc->nr_to_write <= 0 && wbc->sync_mode == WB_SYNC_NONE) { done = 1; break; }}pagevec_release(&pvec);cond_resched();}/* 省略:range_cyclic 环绕与 writeback_index 更新 */return ret;
}
要点:通过 tag_pages_for_writeback()
把将要刷写的页打 TOWRITE 标签以避免与持续写脏进程“赛跑”;WB_SYNC_ALL
保证数据完整性场景不漏写;PageWriteback
避免重复提交。
- 标脏入口:
set_page_dirty()
(mm/page-writeback.c
)
int set_page_dirty(struct page *page)
{struct address_space *mapping = page_mapping(page);if (likely(mapping)) {int (*spd)(struct page *) = mapping->a_ops->set_page_dirty;
#ifdef CONFIG_BLOCKif (!spd)spd = __set_page_dirty_buffers;
#endifreturn (*spd)(page);}if (!PageDirty(page)) {if (!TestSetPageDirty(page))return 1;}return 0;
}
要点:文件页通过 address_space_operations.set_page_dirty
进入文件系统特定逻辑;无映射页走通用路径。伴随计数更新与 cgroup 统计在周边辅助函数中完成。
- 回收协作:
shrink_page_list()
(mm/vmscan.c
)
static unsigned long shrink_page_list(struct list_head *page_list,struct zone *zone, struct scan_control *sc, enum ttu_flags ttu_flags,unsigned long *ret_nr_dirty, unsigned long *ret_nr_unqueued_dirty,unsigned long *ret_nr_congested, unsigned long *ret_nr_writeback,unsigned long *ret_nr_immediate, bool force_reclaim)
{/* 省略:隔离、锁页、状态检查 */if (!page_is_file_cache(page)) { /* 匿名页不由 flusher 管理 */*dirty = false; *writeback = false; return; }*dirty = PageDirty(page);*writeback = PageWriteback(page);if (mapping && mapping->a_ops->is_dirty_writeback)mapping->a_ops->is_dirty_writeback(page, dirty, writeback);/* 省略:遇到脏页触发写回、统计 nr_writeback/nr_dirty 等 */
}
要点:回收路径区分匿名页与文件页;对文件页,PageDirty
与 PageWriteback
决定是否先写回;当大量页处于写回中,ZONE_WRITEBACK
标记触发进一步的节流以避免洗页落后于分配速度。
以上源码片段与第 11 节 ASCII 流程图一一对应,可帮助读者把概念与实现对齐。
14. 应用示例(用户态代码)
以下示例展示典型读写、fsync
强制持久化、O_DIRECT
绕过页缓存,以及 mmap
写入的差异。
- 顺序写入并观察脏页与回写:
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>int main() {int fd = open("/tmp/pagecache_demo.bin", O_CREAT|O_TRUNC|O_WRONLY, 0644);if (fd < 0) { perror("open"); return 1; }const size_t sz = 1<<20; // 1MBchar *buf = malloc(sz);memset(buf, 'A', sz);for (int i = 0; i < 1024; i++) { // 写 1GBssize_t w = write(fd, buf, sz);if (w != sz) { perror("write"); break; }// 可插入 usleep(1000) 观察限速与后台回写差异}// 强制持久化,触发数据与元数据写回if (fsync(fd) < 0) perror("fsync");close(fd);return 0;
}
运行时用:vmstat 1
、iostat -x 1
、cat /proc/meminfo | egrep 'Dirty|Writeback'
观测脏页增长与回写速率;perf trace -e sys_write,sys_fsync
观测系统调用。
O_DIRECT
绕过页缓存(需按设备与文件系统对齐要求分配缓冲):
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>int main() {int fd = open("/tmp/direct_demo.bin", O_CREAT|O_TRUNC|O_WRONLY|O_DIRECT, 0644);if (fd < 0) { perror("open"); return 1; }size_t align = 4096; // 常见对齐要求size_t sz = align * 256; // 1MBvoid *buf;if (posix_memalign(&buf, align, sz) != 0) { perror("posix_memalign"); return 1; }memset(buf, 'B', sz);ssize_t w = write(fd, buf, sz);if (w != sz) perror("write");fsync(fd); // 仍建议持久化确保落盘close(fd);free(buf);return 0;
}
用 strace -e open,write,fsync
观察系统调用;iostat -x 1
看到 IO 直接到设备,Dirty/Writeback
变化较小。
mmap
写入与msync
:
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>int main() {int fd = open("/tmp/mmap_demo.bin", O_CREAT|O_TRUNC|O_RDWR, 0644);if (fd < 0) { perror("open"); return 1; }size_t sz = 1<<20; // 1MBftruncate(fd, sz);char *p = mmap(NULL, sz, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);if (p == MAP_FAILED) { perror("mmap"); return 1; }memset(p, 'C', sz); // 触发页标脏if (msync(p, sz, MS_SYNC) < 0) perror("msync"); // 同步到盘munmap(p, sz);close(fd);return 0;
}
配合 perf trace -e sys_mmap,sys_msync
与 /proc/meminfo
观察脏页与写回。
15. 实战案例(可复现实验)
目标:在通用 Linux 环境下复现“写入背压与脏页阈值”的影响,以及“回收与写回协作”行为。
案例 A:降低阈值以观察比例回写的限速
- 步骤:
- 设置:
sudo sysctl -w vm.dirty_ratio=5
、sudo sysctl -w vm.dirty_background_ratio=2
- 运行“顺序写入示例”,监控
Dirty/Writeback
、vmstat 1
的bi/bo
、procs
。 - 现象:写入越过
dirty_background_ratio
后后台回写线程活跃;继续增长逼近dirty_ratio
时,写进程进入balance_dirty_pages
限速。
- 设置:
- 预期:
Dirty
值接近MemTotal*5%
后不再无约束增长;Writeback
随后台刷写上升;用户态写入耗时上升。
案例 B:fsync
峰值与延迟
- 步骤:运行顺序写入示例,循环写若干 MB 后立即
fsync
。 - 现象:
iostat
写队列深度与写吞吐会出现尖峰;应用侧fsync
耗时显著增加,保证了数据完整性。
案例 C:回收路径遇到脏页
- 步骤:在内存较紧张环境(或用
stress --vm
制造压力)同时运行顺序写入示例。 - 现象:
kswapd
活跃;vmstat
的si/so
、free
下降;当回收遇到大量PG_writeback
时,ZONE_WRITEBACK
可能被置位,回收线程出现等待;整体写入速率受控,避免抖动。
备注:实验前后恢复参数:sudo sysctl -w vm.dirty_ratio=20
、sudo sysctl -w vm.dirty_background_ratio=10
。不同内核版本/文件系统对具体表现有差异。