Linux内核分析之文件系统-03
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。