当前位置: 首页 > news >正文

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_uptodatePG_dirtyPG_writeback
  • writeback 控制:bdiwb(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 1iostat -x 1sar -B 1perf 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.cmm/vmscan.cfs/buffer.cinclude/linux/writeback.h
  • 文档:Documentation/admin-guide/mm/ 下相关条目(新内核),Documentation/filesystems/
  • 工具:blktracefioperfiostatvmstat

13. 源码解读(内核关键片段)

以下片段基于 4.4.94 内核,选取与“标脏→背压→回写→回收协作”直接相关的核心函数,便于将概念与实现对齐。

  1. 脏页背压入口: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 下调阈值以快速收敛。

  1. 回写遍历: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 避免重复提交。

  1. 标脏入口: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 统计在周边辅助函数中完成。

  1. 回收协作: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 等 */
}

要点:回收路径区分匿名页与文件页;对文件页,PageDirtyPageWriteback 决定是否先写回;当大量页处于写回中,ZONE_WRITEBACK 标记触发进一步的节流以避免洗页落后于分配速度。

以上源码片段与第 11 节 ASCII 流程图一一对应,可帮助读者把概念与实现对齐。

14. 应用示例(用户态代码)

以下示例展示典型读写、fsync 强制持久化、O_DIRECT 绕过页缓存,以及 mmap 写入的差异。

  1. 顺序写入并观察脏页与回写:
#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 1iostat -x 1cat /proc/meminfo | egrep 'Dirty|Writeback' 观测脏页增长与回写速率;perf trace -e sys_write,sys_fsync 观测系统调用。

  1. 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 变化较小。

  1. 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=5sudo sysctl -w vm.dirty_background_ratio=2
    • 运行“顺序写入示例”,监控 Dirty/Writebackvmstat 1bi/boprocs
    • 现象:写入越过 dirty_background_ratio 后后台回写线程活跃;继续增长逼近 dirty_ratio 时,写进程进入 balance_dirty_pages 限速。
  • 预期:
    • Dirty 值接近 MemTotal*5% 后不再无约束增长;Writeback 随后台刷写上升;用户态写入耗时上升。

案例 B:fsync 峰值与延迟

  • 步骤:运行顺序写入示例,循环写若干 MB 后立即 fsync
  • 现象:iostat 写队列深度与写吞吐会出现尖峰;应用侧 fsync 耗时显著增加,保证了数据完整性。

案例 C:回收路径遇到脏页

  • 步骤:在内存较紧张环境(或用 stress --vm 制造压力)同时运行顺序写入示例。
  • 现象:kswapd 活跃;vmstatsi/sofree 下降;当回收遇到大量 PG_writeback 时,ZONE_WRITEBACK 可能被置位,回收线程出现等待;整体写入速率受控,避免抖动。

备注:实验前后恢复参数:sudo sysctl -w vm.dirty_ratio=20sudo sysctl -w vm.dirty_background_ratio=10。不同内核版本/文件系统对具体表现有差异。

http://www.dtcms.com/a/486934.html

相关文章:

  • 【NI测试方案】基于ARM+FPGA的整车仿真与电池标定
  • JavaScript将url转为blob和file,三种方法
  • 电商营销型网站建设中国菲律宾关系现状
  • 英文网站建设 飞沐wordpress公众号文章分类
  • 怎么做qq靓号网站岳阳网站设计公司
  • Unity 通过Texture生成的Sprite存在边缘黑线问题 Image黑边问题
  • 计算机方向如何才能更好的找到工作?(成长心得)
  • 大连市城市建设投资集团网站网站怎么做文件上传
  • 织梦网站转跳手机站盐城网盐城网站建设站建设
  • Spring AOP 中@annotation的两种写法详解
  • Linux设备模型
  • Linux fg命令使用教程
  • 微博爬虫流程解析——session的使用
  • 企业网站建设的核心是专业手机建站公司
  • Vllm Semantic-router MoM 架构
  • LLMs-from-scratch :KV 缓存
  • dshot协议数据帧格式
  • 华清远见25072班QT学习day1
  • 一个网站多个子域名优化网站系统建设与管理
  • SpringBoot使用LocalDate接收参数解析问题
  • 推荐系统学习笔记(十八)SECR-快手-用户留存建模
  • 自助网站建设技术支持建商城网站需要什么
  • 物联网设备安全接入管理系统的设计与核心功能
  • 在React中使用函数式编程的优点和缺点是什么?
  • UE5 Windows平台下打包Linux
  • UE5 建模专题 - 2:地形模式里的工具栏里的按钮的使用,以及样条点制作道路
  • 基于PHP+Mysql的旅游介绍网
  • HOW - prefetch 二级页面实践
  • OCR:开启档案管理的智能新时代
  • 设计网站官网入口广州做一个网站多少钱