背景
RWF_DONTCACHE 是一种 I/O 提示(dropbehind):读写完成后立即丢弃相关 page cache,避免只访问一次的数据长期占用缓存。对文件系统,这套机制已经落地:writeback 完成后,对标记了 dropbehind 的 folio 执行失效,把它们从 page cache 清掉。直接操作裸块设备的程序(数据库是典型)却用不上它,读写产生的页只能堆在 page cache 里,等回收器慢慢清理,既污染缓存又徒增回收开销。
卡点在完成回调运行的上下文。块设备的 writeback 在中断(IRQ)上下文完成,bi_end_io 回调就跑在 IRQ 上下文里,而 dropbehind 失效 folio 需要可睡眠的任务上下文,不能就地做。文件系统(XFS)此前靠自己专门的 workqueue 把 dropbehind 从 IRQ 上下文搬到任务上下文,块设备却没有这样一套延迟完成的机制,RWF_DONTCACHE 也就无从支持。
块设备 writeback 完成跑在 IRQ 上下文
┌───────────────────────────────────────┐
│ 块设备 writeback 的完成回调运行在 IRQ │
│ 上下文 │
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ dropbehind 失效 folio 需要可睡眠的任 │
│ 务上下文 │
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ 块设备无处把完成延迟出去从而无法支持 │
│ dropbehind │
└───────────────────────────────────────┘
问题
- 块设备 writeback 在 IRQ 上下文完成,dropbehind 无法就地执行。
- 块设备缺少把完成延迟到任务上下文的机制,故不支持 RWF_DONTCACHE。
- 直接读写裸块设备的程序(如数据库)只能白白填充 page cache,承受回收开销。
方案
核心是在 block 层加一套通用的任务上下文完成基础设施,把那些不能在 IRQ 上下文收尾的 bio 排到 per-CPU worker 里延迟执行。
它提供两种触发方式:
BIO_COMPLETE_IN_TASK是一个 bio 标志,由提交者在提交时就明确回调需要任务上下文,dropbehind 回写即属此类;bio_endio()看到该标志且当前确在原子上下文(bio_in_atomic()),就把这个 bio 交给 workerbio_complete_in_task()则是个 helper,供回调在bi_end_io()里动态决定要不要延迟(例如 iomap 的 fserror 上报);本系列暂未用到它,但已有多个计划中的使用者
两种方式共用同一套 per-CPU 机制:每个 CPU 维护一个 bio 链表和一个 work item,挂在 WQ_PERCPU 工作队列上。生产者关中断把 bio 塞进本 CPU 的链表,链表原本为空就调度本 CPU 的 work;worker 在任务上下文里逐个调用 bi_end_io()。判断一律以 bio_in_atomic() 为准,它覆盖所有回调里睡眠不安全的上下文,包括不可抢占的任务上下文。配套的 CPU 热插拔回调负责在 CPU 下线时排空它的残余 bio,上线时重新入队。
有了这套基础设施,dropbehind 回写就能放心地在任务上下文里失效 folio。iomap 与 buffer-head 两条回写路径在提交 dropbehind bio 时设置 BIO_COMPLETE_IN_TASK,完成回调被 worker 搬到任务上下文后再做 dropbehind;顺带把旧的 IOMAP_IOEND_DONTCACHE 和 XFS 自己的 DONTCACHE workqueue 一并删掉,统一改走这条新路径。为了让基础设施能被各处复用,原本只在 erofs 里的原子上下文检测被下沉为 block 层的 bio_in_atomic(),erofs 也改用它。最后,块设备的读写路径识别并透传 RWF_DONTCACHE,把它真正交给裸块设备的使用者。
把完成延迟到 per-CPU worker 执行
┌──────────────────────────────────────────┐
│ bio 完成回调当前处在原子上下文 │
└────────────────────┬─────────────────────┘
▼
┌──────────────────────────────────────────┐
│ 排入本 CPU 完成队列并调度 worker │
└────────────────────┬─────────────────────┘
▼
┌──────────────────────────────────────────┐
│ worker 在任务上下文执行回调做 dropbehind │
└──────────────────────────────────────────┘
收益
测试条件:VM 单块设备,CONFIG_BUFFER_HEAD=y,结果有噪声。读测试在 root 分区上跨 45GB 区间(约 2 倍内存);写测试在已禁用的 swap 分区(约 1GB)上,放进一个 244MB 的 memcg 以制造回收压力。
| 场景 | Before | After | 改善 |
|---|---|---|---|
| 读吞吐(MB/s) | 1173.7 | 1828.8 | +56% |
| 写吞吐(MB/s) | 3762.5 | 7699.1 | +105% |
读测试区大于内存,dropbehind 让缓存不再被只读一次的数据挤占;写测试的 memcg 极小,dropbehind 直接消除了写完即弃数据的回收压力。