AI智能摘要
围绕 Linux‑6.1 的 Page Cache 脏页回写机制展开,承接上一篇“脏页标记”,完整追踪数据从被修改到落盘的路径。先提炼回写要解决的三大问题:何时触发、由谁执行、如何高效写回;再从历史演进切入,对比早期全局 pdflush/kupdated 与现代按设备划分的 bdi_writeback 框架,重点解析 backing_dev_info、bdi_writeback 等核心结构中与脏页控制、阈值、速率限制和等待队列相关的关键字段。通过源码路径展示回写线程从 bdi_queue_work 被唤醒,到 wb_workfn 轮询任务、wb_do_writeout 扫描 inode 链表并
此摘要由AI分析文章内容生成,仅供参考。

前言

紧接上文 [linux内存管理] 第043篇 Page Cache脏页跟踪机制,在上文中我们详细剖析了内核如何标记脏页——即哪些页面被修改过、需要写回磁盘。标记脏页是持久化的"第一步",但仅靠标记并不能保证数据安全,真正的持久化依赖于脏页回写(Writeback)机制。本章以 Linux-6.1 内核为基准,深入剖析脏页从标记到落盘的完整流程。

回写要解决的核心问题

在正式进入代码之前,我们需要理解回写机制要解决三个核心问题:

  • 何时回写?——触发回写的时机(阈值超时、内存压力、用户主动调用)

  • 谁来回写?——回写线程的工作模型

  • 怎么回写?——单页与批量回写的实现路径

一、回写线程体系:从 pdflush 到 bdi_writeback

1.1 历史回顾

在早期 Linux-2.6 内核中,脏页回写由两个全局线程完成:pdflush(负责扫描脏页)和 kupdated(负责超时回写)。这种设计的问题在于:所有块设备的脏页混在一起回写,无法针对不同设备的 I/O 特性做优化,且全局锁竞争严重。

从 Linux-3.10 开始,内核引入了 bdi_writeback 框架,每个 backing_device_info(bdi)拥有独立的回写工作线程,从根本上解决了全局竞争问题。Linux-6.1 沿用并完善了这一架构。

1.2 backing_dev_info 结构体

每个块设备(或每个挂载点对应的后端设备)在内核中对应一个 backing_dev_info(简称 bdi),它管理该设备的回写状态:

// include/linux/backing-dev-defs.h

struct backing_dev_info {
    struct device *dev;
    struct dev_mapping *dev_map;

    struct bdi_writeback *wb;  // 每个 bdi 一个 wb 线程
    struct list_head bdi_list; // 全局 bdi 链表
    unsigned long capabilities;
    unsigned int ra_pages;     // readahead 页数
    unsigned int io_pages;    // 支持的最大 I/O 页数

    // 脏页控制参数
    unsigned long dirty_ratelimit;
    unsigned long balanced_dirty_ratelimit;

    // dirty_thresh、max_ratio 等阈值信息
    struct bdi_threshold __rcu *wb_thresh;
    struct bdi_threshold __rcu *wb_bg_thresh;

    // 错误状态
    errseq_t wb_err;

    // 等待队列(脏页过多时进程在此等待)
    wait_queue_head_t wb_waitq;

    // 用于跟踪属于本设备的 inode 链表
    struct list_head bdi_io_list;
    struct list_head bdi_dirty_list;

    // 统计信息
    atomic_long_t writeback_pages;
    atomic_long_t writeback_bytes;
    ...
};

关键字段说明:

  • wb:指向本设备的回写线程,每个 bdi 有且仅有一个 bdi_writeback 实例

  • wb_waitq:当脏页数量超过阈值时,等待中的进程在此排队

  • bdi_io_list / bdi_dirty_list:将脏页按 I/O 状态分组,提高回写批量效率

  • dirty_ratelimit:脏页回写的速率限制,防止回写占满磁盘带宽

1.3 bdi_writeback 结构体

struct bdi_writeback {
    struct backing_dev_info *bdi;
    unsigned int dirty_sleep;    // 上次回写时间(用于计算是否超时)
    struct list_head works;       // 待处理的回写任务链表
    struct list_head list;       // 挂入 bdi 链表的节点

    struct work_struct wq_work;  // 工作队列任务(核心)
    wait_queue_head_t wq_waitq;  // 任务等待队列

    struct delayed_work dwork;   // 周期性回写(dirty_expire)

    // 状态标记
    unsigned long state;
#define WB_start_all    0  // 启动所有脏 inode 回写
#define WB_writeback_ready (1 << 0)
#define WB_writeback_running (1 << 1)

    // 统计
    atomic_t idle_infos;          // 空闲 inode 数
    struct fprop_local_percpu completions; // I/O 完成计数器
    ...
};

bdi_writeback 的核心是 wq_work(立即执行的工作任务)和 dwork(延迟/周期性执行的工作任务),两者最终都调用同一个核心处理函数 wb_workfn

1.4 回写线程的启动

当某个 inode 首次产生脏页时,如果对应的 bdi 还没有启动回写线程,内核会通过 bdi_queue_work() 唤醒回写线程:

static void bdi_queue_work(struct bdi_writeback *wb, struct wb_writeout_work *work)
{
    trace_writeback_queue(wb, work->sb);
    list_add_tail(&work->list, &wb->works);
    mod_delayed_work(bdi_wq, &wb->dwork, 0); // 立即调度 dwork
}

static void wb_workfn(struct work_struct *work)
{
    struct bdi_writeback *wb = container_of(work, struct bdi_writeback, dwork.work);

    // 首先处理 pending 队列中的工作任务
    for (;;) {
        struct wb_writeout_work *work;
        work = list_first_entry_or_null(&wb->works, struct wb_writeout_work, list);
        if (!work)
            break;
        list_del(&work->list);
        // 处理每一个回写任务
        wb_do_writeout(wb, work);
    }

    // 如果还有脏页没有被回写,重新调度
    if (wb_has_dirty_io(wb))
        wb_schedule_work(wb); // 继续调度

    // 如果没有更多工作,进入 idle 状态
    wb_clear_pending(wb, WB_writeback_running);
}

wb_workfn 的执行逻辑非常清晰:

  1. wb->works 链表取出所有待处理的回写任务,逐个执行

  2. 如果本设备还有脏页(wb_has_dirty_io(wb)),则继续调度

  3. 完成后清除 WB_writeback_running 标记,表示回写完成

wb_do_writeout() 是真正执行批量回写的函数,它会遍历 inode 列表,对每个脏 inode 调用 writeback_sb_inodes() —— 这里就进入了我们上文中提到的 write_cache_pages 路径:

static void wb_do_writeout(struct bdi_writeback *wb, struct wb_writeout_work *work)
{
    struct inode *inode;

    // 根据 work 类型决定回写范围
    while (!list_empty(&wb->bdi_io_list)) {
        inode = list_first_entry(&wb->bdi_io_list, struct inode, i_io_list);
        // 从 io_list 取出 inode,逐个回写
        __writeback_single_inode(inode, wb, work);
    }
    // 处理 bdi_dirty_list
    while (!list_empty(&wb->bdi_dirty_list)) {
        inode = list_first_entry(&wb->bdi_dirty_list, struct inode, i_io_list);
        __writeback_single_inode(inode, wb, work);
    }
}

二、回写的三类触发时机

脏页回写的触发时机由内核统一调度,主要分为三类:

触发场景

触发条件

触发函数

说明

周期性回写

dirty_expire_centisecs 超时

wb_check_old_data_flush()

脏页老化超时后唤醒回写线程

阈值触发回写

脏页数量超过 dirty_background_ratio

balance_dirty_pages()

脏页过多时后台回写

阈值触发阻塞

脏页数量超过 dirty_ratio

balance_dirty_pages()(进程阻塞)

进程主动等待回写完成

用户主动触发

fsync/fdatasync/msync/sync

各自对应入口函数

数据完整性保证

内存回收触发

shrink_inactive_list() 等路径

wakeup_flusher_threads()

内存压力时强制回写

2.1 周期性回写

内核为每个 bdi 维护一个 dwork 延迟工作,在 dirty_expire_centisecs(默认 3000,即 30 秒)超时后触发回写:

// mm/page-writeback.c
static unsigned long dirty_expire_interval __read_mostly = 3000; // ms

static bool wb_check_old_data_flush(struct bdi_writeback *wb)
{
    unsigned long expire_interval;

    expire_interval = msecs_to_jiffies(dirty_expire_interval);
    if (!time_after(jiffies, wb->dirty_sleep + expire_interval))
        return false; // 还没超时

    // 超时了,唤醒回写线程
    wb_do_writeout(wb, NULL);
    return true;
}

dirty_expire_centisecs 可以通过 /proc/sys/vm/dirty_expire_centisecs 查看和修改。

2.2 阈值触发回写(dirty_ratio / dirty_background_ratio)

这是生产环境中最常见的回写触发方式。当用户进程持续写入文件时,balance_dirty_pages() 会实时监控全局脏页数量:

// mm/page-writeback.c
void balance_dirty_pages(struct address_space *mapping,
                         unsigned long write_chunk)
{
    unsigned long nr_dirty, nr_writeback, nr_thresh, bg_thresh;

    for (;;) {
        // 获取全局脏页数、回写中页数
        nr_dirty = global_node_page_state(NR_FILE_DIRTY);
        nr_writeback = global_node_page_state(NR_FILE_WRITEBACK);

        // 获取全局脏页阈值
        bg_thresh = dirty_background_bytes();
        nr_thresh = dirty_bytes();

        // 如果脏页数未超过后台阈值,无需处理
        if (nr_dirty <= bg_thresh)
            return;

        // 超过后台阈值但未超过总阈值:后台回写,进程不阻塞
        if (nr_dirty <= nr_thresh) {
            wakeup_flusher_threads_bdi(&mapping->host->i_sb->s_bdi,
                                      WB_REASON_BACKGROUND);
            return;
        }

        // 超过总阈值:进程必须等待回写完成
        __set_current_state(TASK_KILLABLE);
        io_schedule_timeout(usecs_to_jiffies(pause));
        // 让出 CPU,等待回写线程处理
    }
}

关键的阈值参数说明:

  • dirty_background_bytes(默认空)或 dirty_background_ratio(默认 10%):后台回写阈值。超过后唤醒后台回写线程,但进程继续执行。

  • dirty_bytes(默认空)或 dirty_ratio(默认 20%):总脏页阈值。超过后写入进程主动等待回写完成,防止脏页无限累积。

这两个阈值可以通过以下命令查看和修改:

# 查看
cat /proc/sys/vm/dirty_background_ratio
cat /proc/sys/vm/dirty_ratio

# 修改(临时)
echo 5 > /proc/sys/vm/dirty_background_ratio
echo 10 > /proc/sys/vm/dirty_ratio

2.3 用户主动触发(fsync / sync)

用户程序调用 fsync(fd)sync() 时,内核需要立即将脏页写入磁盘,不等待超时或阈值触发。这是数据完整性保证的关键路径:

// fsync 系统调用入口
SYSCALL_DEFINE1(fsync, int, fd)
{
    return do_fsync(fd, 0);
}

int do_fsync(int fd, bool datasync)
{
    struct file *file = fget(fd);
    struct inode *inode = file->f_path.dentry->d_inode;
    int ret;

    ret = vfs_fsync(file, datasync); // 通用 VFS 层
    return ret;
}

// vfs_fsync 调用文件系统的具体实现
int vfs_fsync(struct file *file, bool datasync)
{
    struct inode *inode = file->f_path.dentry->d_inode;

    if (!inode->i_sb->s_op->sync_fs)
        return 0;

    // 调用 ext4_sync_file(以 ext4 为例)
    return inode->i_sb->s_op->sync_fs(inode, datasync);
}

对于 ext4 文件系统,ext4_sync_file 的实现如下:

int ext4_sync_file(struct file *file, loff_t start, loff_t end, int datasync)
{
    struct inode *inode = file_inode(file);
    struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
    int ret;

    // ordered 模式:确保数据对应元数据已写入日志,但不等待数据页落盘
    // 如果要确保数据落盘,需要 O_SYNC 或 fsync
    if (ext4_should_journal_data(inode))
        return ext4_flush_unwritten_io(inode);

    // 非 data journal 模式:先同步 inode(确保元数据一致)
    ret = ext4_write_inode(inode, NULL);
    if (ret)
        return ret;

    // 然后触发 page cache 的回写
    ret = filemap_fdatawait_range(inode->i_mapping, start, end);
    return ret;
}

注意:fsync 在 ordered 模式下只保证数据对应的元数据(inode、extent 树等)写入日志,数据页本身的落盘由内核的后台回写线程异步完成。如果需要数据页也落盘(data=journal 模式或 O_SYNC),则需要额外等待数据页的回写完成。

三、批量回写:write_cache_pages 深入分析

在第043篇中我们介绍了 write_cache_pages 的核心流程,本章深入分析其内部实现细节以及关键的状态管理。

3.1 脏页遍历:从 XArray 中找脏页

write_cache_pages 通过 pagevec_lookup_range_tag() 遍历给定地址空间(address_space)中带有 PAGECACHE_TAG_DIRTY 标签的页:

int write_cache_pages(struct address_space *mapping,
                      struct writeback_control *wbc,
                      writepage_t writepage, void *data)
{
    struct pagevec pvec;
    int ret = 0;
    pgoff_t index = wbc->range_start >> PAGE_SHIFT;
    pgoff_t end = wbc->range_end >> PAGE_SHIFT;

    pagevec_init(&pvec);

    while (!done) {
        // 关键:从 i_pages xarray 中查找带有 DIRTY 标签的脏页
        nr_pages = pagevec_lookup_range_tag(&pvec, mapping,
                                            &index, end,
                                            PAGECACHE_TAG_DIRTY);
        if (nr_pages == 0)
            break; // 没有更多脏页

        for (i = 0; i < nr_pages; i++) {
            struct page *page = pvec.pages[i];

            lock_page(page);

            // 有效性检查
            if (page->mapping != mapping)
                goto continue_unlock;
            if (!PageDirty(page))  // 已被其他路径清除脏标记
                goto continue_unlock;

            // 如果页面正在回写中(其他线程正在写)
            if (PageWriteback(page)) {
                if (wbc->sync_mode == WB_SYNC_ALL)
                    wait_on_page_writeback(page); // 同步模式等待
                else
                    goto continue_unlock;         // 异步模式跳过
            }

            // 核心:清除脏标记,同时通过 rmap 处理页表
            if (!clear_page_dirty_for_io(page))
                goto continue_unlock;

            // 调用文件系统的 writepage 回调
            ret = (*writepage)(page, wbc, data);
            ...
        }
        pagevec_release(&pvec);
        cond_resched();
    }
    return ret;
}

遍历依赖 XArray 的 PAGECACHE_TAG_DIRTY 标签——这也是为什么在脏页跟踪时,内核要通过 xa_set_mark(&mapping->i_pages, page_index, PAGECACHE_TAG_DIRTY) 设置标签的原因。没有标签,回写线程就找不到脏页。

3.2 清除脏标记:clear_page_dirty_for_io

这个函数是回写流程中最关键的步骤之一——它不仅要清除页面的 PG_dirty 标志,还要通过反向映射同步所有映射该页的页表项:

// mm/page-writeback.c
bool clear_page_dirty_for_io(struct page *page)
{
    struct address_space *mapping = page_mapping(page);
    bool ret = false;

    // 1. 先清除页表项的脏位(仅对 mmap 方式有效,write 方式无页表映射)
    ret = page_mkclean(page);

    // 2. 清除页面描述符的脏标志
    ret = TestClearPageDirty(page) || ret;

    return ret;
}

page_mkclean() 通过反向映射(rmap)遍历所有映射该页的页表,将每个页表项设置为只读并清除脏位:

// mm/rmap.c
int page_mkclean(struct page *page)
{
    struct address_space *mapping;
    struct rmap_walk_control rwc = {
        .arg = &args,
        .rmap_one = page_mkclean_one,
        .invalid_vma = page_mkclean_file_vma,
    };

    if (!page_mapped(page))
        return 0;

    mapping = page_mapping(page);
    // 遍历所有映射该页的 VMA,清除页表脏位
    rmap_walk(page, &rwc);
    return 0;
}

static int page_mkclean_one(struct page *page, struct vm_area_struct *vma,
                            unsigned long address, void *arg)
{
    pte_t *pte;
    pte_t ptent;

    pte = pte_offset_map(pmd, address);
    ptent = ptep_get(pte);

    if (!pte_dirty(ptent) && !pte_write(ptent))
        goto out;

    // 写保护:设置为只读
    ptent = pte_wrprotect(ptent);
    // 清除脏位
    ptent = pte_mkclean(ptent);
    set_pte_at(vma->vm_mm, address, pte, ptent);
out:
    pte_unmap(pte);
    return 0;
}

这一步骤的意义在于:在回写期间阻止进程的写入操作,避免一边回写一边有新的写入,导致数据不一致。

3.3 ext4 的 writepage 实现

write_cache_pages 调用文件系统的 writepage 回调时,ext4 走的是 ext4_writepage

static int ext4_writepage(struct page *page,
                          struct writeback_control *wbc)
{
    struct inode *inode = page->mapping->host;
    struct ext4_io_submit io_submit;
    int ret;

    // 如果是 journal 模式,数据通过日志路径写入,不走这里
    if (ext4_should_journal_data(inode))
        goto out_ignore;

    // 检查页面是否仍为脏(可能在 clear_page_dirty_for_io 后被其他路径修改)
    ret = block_write_full_page(page, ext4_bh_submit, wbc);
    return ret;

out_ignore:
    unlock_page(page);
    return 0;
}

block_write_full_page() 是通用块层的实现,它遍历页面所有 buffer head,构造 bio 提交到底层块设备,并在 I/O 完成后通过 end_buffer_async_write() 回调清除 PG_writeback 标志。

四、writeback_control:回写的控制参数

writeback_control(简称 wbc)是贯穿整个回写流程的核心数据结构,它控制回写的范围、模式和统计:

struct writeback_control {
    long nr_to_write;          // 本次还需要写多少页
    long pages_skipped;        // 跳过(被锁等)的页数

    // 回写范围(字节)
    loff_t range_start;
    loff_t range_end;

    // 同步模式
    enum writeback_sync_modes sync_mode;
#define WB_SYNC_NONE  0  // 异步回写
#define WB_SYNC_ALL   1  // 同步回写(等待所有页完成)

    // 回写原因(用于 tracing)
    enum wb_reason reason;

    // 是否是范围内的回写(还是全量)
    unsigned int for_kupdate:1;
    unsigned int for_background:1;
    unsigned int for_sync:1;
    unsigned int range_cyclic:1;
};

不同调用路径传入不同的 wbc 参数:

  • fsync:传入 sync_mode = WB_SYNC_ALL,表示必须等待所有脏页落盘才返回

  • 后台回写线程:传入 sync_mode = WB_SYNC_NONE,异步执行,不等待

  • msync(MS_ASYNC):传入 WB_SYNC_NONE

  • msync(MS_SYNC):传入 WB_SYNC_ALL

五、ext4 批量回写:ext4_writepages

实际生产环境中,文件系统更多使用的是批量回写接口 writepages(注意是复数),而不是单页回写的 writepage。ext4 的 ext4_writepages 实现中,核心调用的就是 write_cache_pages

static int ext4_writepages(struct address_space *mapping,
                           struct writeback_control *wbc)
{
    struct inode *inode = mapping->host;
    struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);

    // mballoc 分配上下文(延迟分配)
    handle_t *handle = NULL;

    // 批量回写的核心路径
    ret = ext4_bio_write_page(&io_submit, page, len, NULL);
    // 实际上底层走的还是 write_cache_pages
    return generic_writepages(mapping, wbc);
}

// generic_writepages 会调用 write_cache_pages
int generic_writepages(struct address_space *mapping,
                       struct writeback_control *wbc)
{
    return write_cache_pages(mapping, wbc, mapping->a_ops->writepage, NULL);
}

ext4 在批量回写时会使用延迟分配(delalloc)优化:多个相邻的脏页可以合并为一次大的块分配请求,减少块分配次数,提高性能。

六、writeback 与内存回收的交互

当系统内存压力增大时,内核的 shrink_inactive_list() 会尝试回收不活跃的页面缓存。如果这些页面中有脏页,必须先回写后才能释放:

// mm/vmscan.c
static unsigned long shrink_inactive_list(unsigned long nr_to_scan, ...)
{
    LIST_HEAD(file_list);
    struct page *page;
    unsigned long nr_unqueued_dirty = 0;

    while (!list_empty(&page_list)) {
        page = lru_to_page(&page_list);

        // 如果是脏页,必须先回写
        if (PageDirty(page)) {
            list_move(&page->lru, &ret_pages);
            nr_unqueued_dirty++;
            continue;
        }

        // 干净页可以直接回收
        nr_scanned++;
        if (PageActive(page))
            ...
    }

    // 触发回写
    if (nr_unqueued_dirty)
        wakeup_flusher_threads(nr_unqueued_dirty, WB_REASON_VMSCAN);

    return nr_reclaimed;
}

在内存压力场景下,回写会与内存回收线程紧密配合:shrink 扫描脏页后将其移入 ret_pages,然后通过 wakeup_flusher_threads() 唤醒回写线程来处理这些脏页。

七、脏页回写完整生命周期

结合第043篇的脏页跟踪机制,我们可以画出脏页的完整生命周期:

┌──────────────────────────────────────────────────────┐
│                  脏页产生                              │
│                                                       │
│  write() → generic_perform_write() → set_page_dirty()│
│  mmap()写 → 缺页异常 → ext4_page_mkwrite() → PG_dirty │
│                                                       │
│  标记 PG_dirty = 1 + XArray 设置 PAGECACHE_TAG_DIRTY  │
└──────────────────┬───────────────────────────────────┘
                   │
                   │ balance_dirty_pages() / 周期性调度 / fsync
                   ▼
┌──────────────────────────────────────────────────────┐
│                  回写开始                              │
│                                                       │
│  wb_workfn → wb_do_writeout → __writeback_single_inode│
│                                                       │
│  write_cache_pages()                                  │
│    pagevec_lookup_range_tag(PAGECACHE_TAG_DIRTY)      │
│    → 遍历脏页                                         │
│                                                       │
│    for each page:                                     │
│      lock_page(page)                                  │
│      clear_page_dirty_for_io(page)                    │
│        → page_mkclean()  清除页表脏位+写保护         │
│        → TestClearPageDirty() 清除 PG_dirty         │
│      → 调用 writepage 回调(ext4_writepage)         │
│        → block_write_full_page()                     │
│        → 构造 bio 提交到块层                          │
│      → PG_writeback = 1(正在回写)                  │
└──────────────────┬───────────────────────────────────┘
                   │
                   │ I/O 完成中断(end_buffer_async_write)
                   ▼
┌──────────────────────────────────────────────────────┐
│                  回写完成                              │
│                                                       │
│  PG_writeback = 0                                     │
│  PG_dirty = 0(之前已清除)                           │
│  页面变回干净(clean)                               │
│                                                       │
│  如果 fsync 等待:wake_up(page_waitqueue(page))      │
└──────────────────────────────────────────────────────┘

八、回写调优实战

理解回写机制后,可以通过调优 sysctl 参数显著改善不同场景下的性能:

场景

推荐配置

说明

通用服务器

dirty_ratio=20, dirty_background_ratio=10

默认值,平衡性能与数据安全

大文件写入(如数据库)

dirty_ratio=40, dirty_background_ratio=15

提高阈值,减少回写频率,提高写入吞吐量

低延迟敏感场景

dirty_ratio=5, dirty_background_ratio=2

降低阈值,数据更快落盘,减少 fsync 延迟

容器 / 虚拟化环境

dirty_bytes=4194304(4MB)

使用绝对值代替百分比,避免容器间竞争

关键数据写入后立即 fsync

无需特殊调优

fsync 本身强制等待回写,调优参数主要影响无 fsync 的场景

可以通过以下命令实时查看脏页状态:

# 查看脏页数量(单位 KB)
cat /proc/meminfo | grep -E "Dirty|Writeback"

# 查看各设备的 bdi 信息(Linux-5.6+)
cat /sys/kernel/debug/bdi/<bdi-id>/stats

# 手动触发后台回写
sync

总结

本章系统剖析了 Linux-6.1 内核的脏页回写机制,核心要点如下:

  1. 回写线程模型:从旧的 pdflush 全局线程演进为每个 bdi 一个 bdi_writeback 工作线程,消除了全局竞争

  2. 三层触发时机

    • 周期性超时回写(dirty_expire_centisecs

    • 阈值触发(dirty_ratio / dirty_background_ratio

    • 用户主动触发(fsync / sync / msync

  3. 回写流程wb_workfnwrite_cache_pagesclear_page_dirty_for_iowritepage → 块层 bio

  4. 关键同步点:回写前通过 page_mkclean() 清除页表脏位并写保护,回写期间阻塞新写入;回写完成后清除 PG_writeback,页面恢复干净

  5. 与脏页跟踪的闭环:脏页跟踪标记了"哪些页需要回写",回写机制负责"何时回写"和"怎么回写",两者共同构成 Linux 内存管理中数据持久化的完整链路

结合第043篇和第044篇,读者应能完整理解 Linux Page Cache 中脏页从产生、跟踪到最终落盘的全流程。下一步可以深入的方向包括:

  • ext4 日志(jbd2)机制与数据回写的交互

  • 内存回收(kswapd / direct reclaim)中脏页的回收策略

  • eBPF/bpftrace 追踪脏页回写的实战方法

由于作者水平有限,文章不足之处在所难免,敬请广大读者朋友批评指正。