Linux内核分析之文件系统-03

This language version is unavailable; showing the other language.

36.1 页缓存 (Page Cache) —— address_space

address_space 把"文件的页缓存视图"形式化:以 XArray 保存"文件偏移 → folio"的映射,以三个标记位维护脏/回写状态,以 a_ops 把"这页缺了谁去供货"多态到具体文件系统。read/write/mmap/共享内存全部经此一仓。本节拆解容器、索引与读路径。


36.1.1 address_space:每 inode 一个仓库

// include/linux/fs.h:470-498
struct address_space {
    struct inode        *host;      /* 属主 inode */
    struct xarray       i_pages;    /* 页缓存: offset → folio */
    struct rw_semaphore invalidate_lock;    /* 失效 vs 读并发 */
    gfp_t           gfp_mask;
    atomic_t        i_mmap_writable;    /* 共享映射计数 */
    ...
    struct rb_root_cached   i_mmap;     /* 反向映射树(rmap, 25 章用) */
    unsigned long       nrpages;    /* 缓存页数 */
    pgoff_t         writeback_index;    /* 回写起点提示 */
    const struct address_space_operations *a_ops;   /* 多态钩子 */
    unsigned long       flags;
    errseq_t        wb_err;     /* 错误序列(ESPIPE 语义) */
    ...
};

/* XArray tags, for tagging dirty and writeback pages in the pagecache. */
#define PAGECACHE_TAG_DIRTY XA_MARK_0   /* :493 */
#define PAGECACHE_TAG_WRITEBACK XA_MARK_1
#define PAGECACHE_TAG_TOWRITE   XA_MARK_2

XArray(17 章提到的 RCU 友好基数树变体)以文件偏移(页号)为键存 folio 指针;标记位(XA_MARK)在树上向上聚合——"找第一个脏页"从全树扫描降为标记跳跃,这是回写定位(36.3 节)与 sync_file_range 的高效地基。i_mmap 红黑树把"映射本文件的全部 VMA"挂链——25.3 节 COW 与 24 章回收的反向映射文件侧支点。invalidate_lock 仲裁"读/回写进行中 vs 截断失效"的竞态。

36.1.2 读路径:filemap_read

// mm/filemap.c:2769(签名)与 :2668(批量取页)
ssize_t filemap_read(struct kiocb *iocb, struct iov_iter *iter,
        ssize_t already_read)
static int filemap_get_pages(struct kiocb *iocb, size_t count,
                 struct folio_batch *fbatch, bool need_uptodate)
filemap_read 的一次循环:

 [1] filemap_get_pages (:2668):
       page_cache_next_miss (:1807) 探测连续命中段
       → folio_batch (pagevec.h:28) 批量装入最多 16 页
       未命中的页: page_cache_alloc + a_ops->readahead/read_folio
         发起异步 IO, 标 PG_readahead 链式预读窗口
 [2] 逐页: 等 PG_uptodate (同步读阻塞于此)
     → copy_page_to_iter → 推进 pos
 [3] 首页命中+窗口可扩 → 扩大本批 (顺序读加速)
 EOF 检查 (i_size) → 返回已拷贝字节数

 预读窗口的演化: 4 页起步 → 命中即倍增(最大 128KB+
 /bdi read_ahead_kb) → 随机读探测(RAND_READ 提示, 22.3
 节 VM_RAND_READ)则收缩 — filemap_read 与 readahead
 模块配合的"自适应窗口"

mmap 读不经过 filemap_read:缺页直接 filemap_fault(25.2.2 节)从同一 XArray 取页——两条读路径在缓存层汇合,这正是"read 后 mmap 可见"一致性的来源。

36.1.3 写路径与 a_ops 钩子

address_space_operations (a_ops) 的核心钩子:

 write_begin/write_end   写准备/完成 (ext4 的 delalloc 挂点, 34.2.3)
 dirty_folio             标脏 (加入回写记账)
 invalidate_folio        截断/失效时的清理
 release_folio           回收前的清理 (buffer_head 脱钩)
 migrate_folio           页迁移 (18.5 节) 时的搬家
 error_remove_folio      IO 错误后的移除

 写路径 generic_file_write_iter (33.4.2):
   buffered: 写 XArray 里的 folio → dirty_folio 标脏
     → PAGECACHE_TAG_DIRTY → 回写系统认领 (36.3)
   O_DIRECT: 绕过缓存, invalidate 后直组 bio (36.2)
     — 与缓存页的一致性由 invalidate_lock 仲裁

一致性义务:写后的页必须"新读者读到新数据"——PG_uptodate 与内容更新在锁内完成;wb_err 的 errseq 机制把 IO 错误以"世代号"传给后续 fsync 的调用者(fsync 返回上一次以来的首个错误)。


小结

address_space 以 XArray(offset→folio)+ 三个聚合标记(DIRTY/WRITEBACK/TOWRITE)+ i_mmap 反向树构成文件页缓存的全部容器;filemap_read 以 folio_batch 批量取页、自适应预读窗口放大顺序性,mmap 与 read 在同一仓库汇合;写路径经 a_ops 多态进入各文件系统的标脏/延迟分配逻辑,一致性义务由 PG_uptodate 与 errseq 兑现。下一节看脏页离仓的路——bio 提交。

36.2 BIO 结构与提交路径

35.1.2 节已给出 struct bio 的字段本节不再重复;本节从页缓存的视角看"一页数据如何变成设备工单":mpage 的多页打包、buffer_head 的历史兼容、提交与完成回调的消费关系。


36.2.1 从页到 bio:mpage 的多页打包

回写提交的打包演进:

 老路径 (buffer_head 时代): 每块(1KB)一个 bh →
   每个 bh 一个请求 → 千块文件千次提交
 mpage 路径 (现代): 遍历页缓存连续脏页,
   物理连续的页合成一个 bio:
   bio_vec = [page1|full], [page2|full], ... [pageN|full]
   → 一个 bio 携带几十页, 一次门铃送达设备
   (这是 34.2 节 extent 连续性在块层的直接回报)

 大 IO 的边界:
   bio->bi_max_vecs (BIO_MAX_VECS = 256 页 = 1MB)
   超过则 bio_split 切链 (35.1.2 节)
   设备 max_sectors/max_segments 再裁

JBD2 的元数据仍走 buffer_head(34.3 节日志块记账以 bh 为单位)——submit_bh(REQ_OP_WRITE, bh) 把 bh 包装成单页 bio 提交;bh 与页缓存页的对应(每页多个 bh 指回同一 folio)是元数据路径的粘合层。

36.2.2 提交与完成的握手

提交侧 (submit_bio_noacct, 35.1.3):
   bio->bi_end_io = 回调 (回写: end_page_writeback 类;
     同步读: filemap 的等待者)
   submit_bio_noacct → 调度器 → blk-mq

 完成侧 (设备中断 → blk_mq_complete_request):
   bio->bi_status 记结果
   bi_end_io(bio):
     成功: 清 PAGECACHE_TAG_WRITEBACK, folio_end_writeback
       → 唤醒等待回写完成的线程 (fsync!)
     失败: mapping_set_error → errseq 记代 (36.1.3)
       → 后续 fsync 报错给应用

 PG_writeback 的角色 (page-flags.h:95):
   提交时置位 (页进"回写中"账)
   bi_end_io 清位 — 回写期间的页不可再标脏提交
   (防同页两份在途 IO; 24.3.3 节回收器的
    "跳过 writeback 页" 判定即此位)

O_DIRECT 的提交路径更直接:用户页经 pin_user_pages(GUP 的长期 pin 变体,18.5 节迁移约束的来源)固定后直接组成 bio_vec,绕过页缓存——完成后 bi_end_io 直接唤醒 io_uring/线程。缓存与非缓存的一致性仲裁在 invalidate_lock(36.1.1 节):DIO 期间缓存页先失效。

36.2.3 观测:blktrace 与 vmstat

I/O 路径的观测口径:

 /proc/vmstat: pgpgin/pgpgout (页进出),
   pgmajfault (需 IO 的缺页, 22.4.7 节)
 /proc/diskstats: 每设备 in_flight/io_ticks —
   队列深度与设备繁忙度
 blktrace/BPF: bio 的完整旅程
   (Q→提交, G→入队, D→派发, C→完成 时间戳)
 iostat 的 w_await/r_await: 调度器效果的最终口径

小结

页到 bio 的转译以 mpage 多页打包为主形态——物理连续的 extent(34.2 节红利)合成大 bio 直达设备,buffer_head 仅存于 JBD2 元数据的兼容粘合层;提交-完成的握手以 PG_writeback + bi_end_io 构成"在途账",错误经 errseq 世代号传给 fsync。DIO 以 GUP pin 用户页绕过缓存、invalidate_lock 仲裁一致性。下一节看谁在何时决定把这些 bio 排出来——回写机制。

36.3 回写机制 —— dirty page 与 flush 线程

脏页不能永远滞留内存:回写子系统以水位 + 周期 + 强制三种触发,把脏页经"每设备回写线程"批量转成 bio。它的调控面(dirty_ratio 等)直接决定"突然断电能丢多少数据"与"写入延迟毛刺"的平衡。本节拆解记账、触发与执行三层。


36.3.1 脏页记账与水位

// mm/page-writeback.c:92(全局默认)
static int vm_dirty_ratio = 20;
// :347
static void domain_dirty_limits(struct dirty_throttle_control *dtc)
// :2115
void balance_dirty_pages_ratelimited(struct address_space *mapping)
脏页的记账层级:

 全局:   dirty_background_ratio (5%) / dirty_ratio (20%)
   按可用内存的百分比换算页数 (domain_dirty_limits, :347)
 每 BDI: 每个(虚拟)设备一份可调 (sysfs bdi/*/min_ratio)
 每 inode: 脏页挂在 XArray 标记 + wb 的 b_dirty 链

 写入者的闸门 balance_dirty_pages (:1802):
   每写若干页 (ratelimited, :2115 限频) 检查:
   超过 background (5%): 唤醒回写线程 (异步清账)
   超过 dirty (20%):     **写入者自己开始节流** —
     按脏页生成速率与回写速率配比睡眠
     ("你写得比盘快, 那就写得慢一点")
   硬顶 (过脏限): 直接阻塞到回写追上
   → 这就是"拷贝大文件到 U 盘速率忽快忽慢"的机制来源

36.3.2 执行体:per-BDI 回写线程

// include/linux/backing-dev-defs.h:106(回写执行体, 节选要点)
struct bdi_writeback {
    ...
    struct list_head    b_dirty;    /* 脏 inode 链 */
    struct list_head    b_io;       /* 正在回写的 inode */
    struct list_head    b_dirty_time;   /* 仅时间戳脏的 inode */
    spinlock_t      list_lock;
    ...
    struct work_struct  dwork;      /* 挂 workqueue 执行 */
    ...
};

// fs/fs-writeback.c:2411 与 :2380
void wb_workfn(struct work_struct *work)
static long wb_do_writeback(struct bdi_writeback *wb)

每个 backing device 一条回写执行体(bdi_writeback,简称 wb):物理盘各一条,cgroup 有自己配额时再分(12.5 节 io cgroup 的 writeback 隔离)。它不是传统内核线程而是挂 workqueue 的 wb_workfn——动态伸缩、按设备生命周期生灭。工作流(wb_do_writeback :2380 → wb_writeback → writeback_sb_inodes :1957):

一次回写工作的流程:

 [1] queue_io (:1560): 把 b_dirty 的 inode 移入 b_io
     (按 inode->dirtied_when 排序 — 最老的先写)
 [2] writeback_sb_inodes (:1957) 逐 inode:
     持 i_rwsem (与截断/失效互斥, 36.1.1)
     tag_pages_for_writeback: PAGECACHE_TAG_TOWRITE
       圈定本轮范围 (TOWRITE 标记, 36.1.1 节)
     write_cache_pages: 遍历标记页
       → wait_for_stable_page (若正被写? 24.3 节节流联动)
       → a_ops 写准备 (ext4 delalloc 分配, 34.2.3)
       → 组 bio 提交 (36.2.1) → PG_writeback
 [3] 等 in-flight 完成过半再继续 (防队列爆)
 [4] 超时/配额 (wbc: writeback_control, writeback.h:43
     —— nr_to_write/sync_mode/range 全在这)

36.3.3 触发源与 fsync 的强制路径

三种触发:

 [1] 周期: dirty_writeback_centisecs (默认 5s)
     醒来清"脏了超过 30s"的页 (dirty_expire_centisecs)
 [2] 水位: balance_dirty_pages 超 background 唤醒 (36.3.1)
 [3] 强制: sync()/fsync()/sync_file_range
     fsync: wbc.sync_mode = WB_SYNC_ALL
       → 逐页等 PG_writeback 清零 (36.2.2 完成回调)
       → f_op->fsync → JBD2 commit (34.3.2) → 元数据落盘
     sync_file_range: 只触发不等待 (30.2.3 节 msync 的
       真实异步替代)

 观测:
 /proc/meminfo Dirty/Writeback    当前脏/在途页量
 /proc/vmstat nr_dirty/writeback  累计口径
 writeback 线程的 CPU/IO 占比     节流强度指示

数据丢失窗口的公式化:默认配置下"最多丢 30 秒前的脏页"(expire)+ 水位决定的新脏页——笔记本/关键数据以 dirty_writeback 调小或应用 fsync 收紧;批处理机以调大 ratio 换吞吐。24 章回收器与回写的关系在此闭环:回收写页 = 先触发回写再回收(kswapd 对脏页只能转交 wb 不能自己写——IO 是 wb 的职责)。


小结

回写子系统以"全局/BDI 两级水位 + balance_dirty_pages 写入者节流"控制脏页总量(20% 阈值后的自动限速是平滑 IO 的关键),以"每设备 bdi_writeback 执行体 + TOWRITE 圈选 + mpage 打包"批量落盘,周期/水位/强制三种触发覆盖从省电到 fsync 的全部语义;writeback_control 是它的统一参数面,PG_writeback 完成回调是它与页缓存、回收、fsync 的交汇点。文件 I/O 的完整栈——VFS、页缓存、回写、块层——至此贯通;下一章看挂在 VFS 上的"内存文件系统们":proc、sysfs、debugfs 与 tmpfs。