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
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
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;
    else
        tag = 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
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_BLOCK
        if (!spd)
            spd = __set_page_dirty_buffers;
#endif
        return (*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
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. 顺序写入并观察脏页与回写:
c 复制代码
#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; // 1MB
    char *buf = malloc(sz);
    memset(buf, 'A', sz);
    for (int i = 0; i < 1024; i++) { // 写 1GB
        ssize_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 绕过页缓存(需按设备与文件系统对齐要求分配缓冲):
c 复制代码
#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; // 1MB
    void *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
c 复制代码
#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; // 1MB
    ftruncate(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。不同内核版本/文件系统对具体表现有差异。

相关推荐
蓝冰印6 小时前
HarmonyOS Next 快速参考手册
linux·ubuntu·harmonyos
---学无止境---6 小时前
Linux中在字符串中查找指定字符的第一次出现位置的汇编实现
linux
tianyuanwo6 小时前
虚拟机监控全攻略:从基础到云原生实战
linux·云原生·虚机监控
别或许6 小时前
在centos系统下,安装MYSQL
linux·mysql·centos
丁丁丁梦涛6 小时前
CentOS修改MySQL数据目录后重启失败的问题及解决方案
linux·mysql·centos
黑马金牌编程6 小时前
Jenkins的Linux与window部署方式
linux·运维·windows·jenkins·持续集成·cicd
web安全工具库7 小时前
告别刀耕火种:用 Makefile 自动化 C 语言项目编译
linux·运维·c语言·开发语言·数据库·算法·自动化
DechinPhy7 小时前
Ubuntu挂载新硬盘
linux·运维·服务器·ubuntu
lht6319356127 小时前
Ubuntu Server 系统安装图形界面远程工具(RDP)
linux·运维·ubuntu