[toc]
mm/backing-dev.c 回写管理(Writeback Management) 脏页回写的调速器与执行者
历史与背景
这项技术是为了解决什么特定问题而诞生的?
这项技术是为了解决Linux内核中一个核心的性能与数据一致性难题:如何智能、高效地将内存中被修改过的数据(“脏页”,Dirty Pages)写回到持久化存储设备(“后备设备”,Backing Device)中。
在mm/backing-dev.c
所代表的现代回写框架出现之前,Linux的脏页回写机制比较原始,存在诸多问题:
- 全局瓶颈:早期的
pdflush
机制使用一个全局的线程池来处理所有设备的回写任务。这意味着一个慢速设备(如USB 1.0 U盘)的回写任务,可能会长时间占用一个flusher线程,从而阻塞一个高速设备(如NVMe SSD)的回写,造成**队头阻塞(Head-of-line blocking)**问题。 - 缺乏精细控制:无法对单个设备设置不同的回写策略。所有设备共享一套全局的回写参数,这对于性能差异巨大的异构存储环境是极其低效的。
- 写操作延迟风暴(Latency Spikes):当系统中积累了大量脏页后,如果一个进程需要分配内存,可能会触发大规模的同步回写,导致该进程乃至整个系统出现长时间的卡顿。
- 无法有效处理I/O拥塞:当存储设备已经不堪重负时,内核缺乏一个有效的机制来减缓(“节流”,throttle)那些正在产生新脏页的进程,导致问题雪上加霜。
mm/backing-dev.c
的核心——backing_dev_info
(BDI) 框架,就是为了解决以上所有问题,旨在创建一个** per-device 的、可伸缩的、能够感知拥塞的回写机制**。
它的发展经历了哪些重要的里程碑或版本迭代?
Linux的回写机制经历了几个重要的演进阶段:
bdflush
和kupdate
时代:非常早期的内核使用这两个独立的守护进程。bdflush
在内存压力大时被唤醒,kupdate
则定期执行。逻辑简单,效率低下。pdflush
时代:引入了一个由多个内核线程组成的pdflush
池,取代了bdflush
和kupdate
。相比之前是巨大进步,但仍存在上文提到的全局瓶颈和队头阻塞问题。- BDI Flusher Threads 时代(当前):这是决定性的变革。
backing_dev_info
(BDI) 结构被赋予了更重要的角色。内核为每个独立的后备设备(更准确地说是每个请求队列)创建了专属的flusherkworker
线程。mm/backing-dev.c
就是这个现代框架的管理中心。这一变革实现了:- Per-device 线程:彻底隔离了不同设备的回写任务。
- 拥塞感知与写者节流:BDI能够跟踪设备的拥塞状态。
mm/backing-dev.c
中的逻辑可以根据这个状态,让产生脏页的进程睡眠一小段时间,从而实现反压(backpressure)。
目前该技术的社区活跃度和主流应用情况如何?
mm/backing-dev.c
是Linux内核MM和块层之间至关重要的粘合剂,其代码非常成熟和稳定。
- 绝对核心:它是所有对块设备进行**缓冲I/O(Buffered I/O)**操作的必经之路。你系统中的每一次常规文件写入,最终都由BDI框架调度回写。
- 社区状态:社区的活跃度主要体现在对回写算法的持续微调、对新类型存储设备(如高速NVMe、 zoned-storage)的适应性优化,以及解决在极端负载下发现的延迟问题。它与
/proc/sys/vm/
下众多dirty_*
参数的调优密切相关。
核心原理与设计
它的核心工作原理是什么?
mm/backing-dev.c
的核心是围绕struct backing_dev_info
(BDI) 和其关联的struct bdi_writeback
展开的策略与执行框架。
数据结构:
struct backing_dev_info
(BDI):代表一个后备存储设备(或一组共享队列的设备)的回写属性和状态。它包含了最小/最大回写比率、拥塞状态等策略信息。struct bdi_writeback
:代表一个实际的回写执行单元。它包含了脏页的链表、统计信息,并与一个专属的flusher内核线程关联。
触发回写:回写操作由多种事件触发:
- 内存压力:当系统中的总脏页数量超过
vm.dirty_background_ratio
(或_bytes
)时,BDI的后台flusher线程会被唤醒,开始异步地将脏页写回磁盘。 - 时间到期:当一个脏页在内存中停留的时间超过
vm.dirty_expire_centisecs
时,它也会被后台flusher线程回收。 - 同步请求:当用户调用
sync()
、fsync()
、msync()
时,或者当一个进程的脏页数量超过限制并需要同步等待回写时(例如总脏页超过vm.dirty_ratio
),会触发一个同步的回写请求。
- 内存压力:当系统中的总脏页数量超过
执行回写:
- 当一个BDI的flusher线程被唤醒时,它会从该设备关联的
bdi_writeback
结构中获取脏页列表(这些脏页由文件系统在弄脏它们时链接上去)。 - 线程将这些脏页组织成
bio
请求,并提交给块设备层。
- 当一个BDI的flusher线程被唤醒时,它会从该设备关联的
拥塞控制与写者节流(Throttling):这是该框架最精妙的部分。
- BDI会监控其关联的I/O请求队列的健康状况。如果队列深度很大,或者I/O延迟很高,BDI会将自己标记为**“拥塞”(congested)**。
- 当一个进程(例如
cp
命令)正在快速产生脏页时,内核在允许其弄脏更多页面之前,会检查对应设备的BDI状态。如果BDI处于拥塞状态,内核会强制该进程睡眠一小段时间,等待设备缓解压力。这就是写者节流,它是一种有效的反压机制,防止了失控的写操作压垮存储系统。
它的主要优势体現在哪些方面?
- 隔离性:Per-device的回写队列和线程,彻底解决了快慢设备间的干扰问题。
- 可伸缩性:在拥有大量CPU和多个存储设备的系统上,回写工作可以并行进行,伸缩性良好。
- 延迟控制:通过写者节流机制,有效地平滑了I/O负载,避免了系统因突发的大量回写而导致的长时间卡顿。
- 可调优性:提供了丰富的
sysctl
参数,允许系统管理员根据硬件和工作负载特性,精细地调整回写行为。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 复杂性:回写行为由多个全局和per-device的参数共同决定,其内部交互逻辑非常复杂,使得问题诊断和性能调优变得困难。
- 调优挑战:对于系统管理员来说,找到一组最优的
vm.dirty_*
参数组合以适应特定的工作负载,是一个不小的挑战。错误的参数可能导致性能下降或延迟增加。 - 不适用于非缓冲I/O:该框架完全基于页面缓存。对于使用直接I/O(
O_DIRECT
)的应用程序,数据流绕过了页面缓存,因此也完全不受BDI回写机制的管理。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?
它是Linux系统中进行缓冲I/O的唯一且标准的解决方案,不存在替代品。
- 所有常规文件操作:当你在任何主流文件系统(ext4, XFS, Btrfs)上创建、写入或修改文件时,只要没有使用
O_DIRECT
,你的数据就是脏页,最终都由mm/backing-dev.c
的框架负责调度写回。 - 系统调用
sync()
fsync()
:这些系统调用是BDI框架的“用户”。sync()
会唤醒所有BDI的flusher线程,要求它们写回所有脏数据。fsync(fd)
则只针对特定文件所在的设备进行同步回写。 - 系统性能监控:
iostat
等工具中显示的I/O拥塞情况,以及/proc/vmstat
中的nr_dirty
,nr_writeback
等指标,都直接反映了BDI框架的工作状态。
是否有不推荐使用该技术的场景?为什么?
更准确的说法是,哪些场景会绕过这个技术:
- 数据库等高性能应用:如前所述,需要精细控制I/O、避免双重缓存的数据库通常使用
O_DIRECT
,它们自己管理缓存和回写时机,不依赖BDI。 - 内存文件系统:
tmpfs
的数据完全在RAM中,没有后备设备,因此与BDI无关。 - 网络文件系统:NFS等有自己的客户端缓存和一致性协议,其回写逻辑比本地BDI要复杂得多,尽管其本地缓存最终也可能作为脏页存在并由BDI管理。
对比分析
请将其 与 其他相似技术 进行详细对比。
BDI Writeback (缓冲I/O) vs. Direct I/O (O_DIRECT
)
特性 | BDI Writeback (Buffered I/O) | Direct I/O (O_DIRECT ) |
---|---|---|
数据路径 | App Buffer -> Page Cache -> Disk |
App Buffer -> Disk |
回写控制 | 由内核(BDI框架)根据全局策略自动、异步地管理。 | 由应用程序显式地通过write() 调用同步触发。 |
性能模型 | 吞吐量优化。通过延迟和合并写入来提高效率。对小写入友好。 | 延迟和一致性优化。应用程序可以精确控制数据何时落盘。 |
适用场景 | 通用文件操作,桌面应用,Web服务器静态内容服务。 | 数据库,虚拟机监视器,需要自定义缓存策略的高性能应用。 |
BDI Writeback (后台异步) vs. fsync()
(前台同步)
特性 | BDI Writeback (后台) | fsync() |
---|---|---|
触发方式 | 自动(基于时间或内存压力)。 | 手动(由应用程序显式调用)。 |
作用范围 | 系统级或设备级。 | 文件级。只保证调用fsync 的那个文件描述符相关的脏数据和元数据被写回。 |
调用者行为 | 对应用程序透明,不阻塞write() 调用。 |
阻塞调用进程,直到数据和元数据确认落盘。 |
关系 | fsync 是BDI框架的一个同步用户。它向BDI发出一个高优先级的、必须立即完成并等待结果的回写请求。 |
fsync 是BDI框架的一个同步用户。它向BDI发出一个高优先级的、必须立即完成并等待结果的回写请求。 |
inode_to_bdi
1 | static inline bool sb_is_blkdev_sb(struct super_block *sb) |
wb_init
- 此函数是bdi_init的逻辑延续,用于初始化一个struct bdi_writeback(通常简称为wb)对象。如果说bdi是块设备的静态“档案”,那么wb就是负责执行该设备写回操作的动态“工作单元”或“执行引擎”。它包含了执行脏数据回写所需的所有状态、列表、锁和后台任务。
1 | /* |
bdi_init - 初始化一个 backing_dev_info 结构体
- 此函数是Linux内核中块设备I/O子系统的一个基础函数。它的核心职责是初始化一个struct backing_dev_info(简称bdi)结构体。这个结构体是内核用来描述一个“后备设备”(通常是块设备,如SD卡、eMMC、QSPI Flash等)的I/O特性和状态的元数据集合,尤其与写回缓存(writeback caching)策略紧密相关。
对于STM32H750
1 | static int cgwb_bdi_init(struct backing_dev_info *bdi) |
BDI Sysfs Interface: I/O Performance Tuning for Block Devices
此代码片段为Linux内核的**Backing Device Info (BDI)子系统创建了一个sysfs
接口。BDI是内核中用于描述一个块设备(如SD卡、eMMC、硬盘或NFS挂载点)的I/O特性和状态的核心结构体。这段代码通过在/sys/class/bdi/
目录下为每个块设备创建一个对应的目录, 并在其中填充一系列可读写的属性文件, 允许系统管理员在运行时(runtime)**动态地调整该设备的I/O性能参数, 如文件预读大小和脏页回写策略。
1 | /* |
default_bdi_init: 初始化全局回写工作队列
此函数是Linux内核块设备层(Block Device Layer)初始化过程中的一个关键步骤。它的核心作用是创建一个全局的、专用的工作队列(workqueue), 命名为”writeback”, 并将其句柄保存在全局指针bdi_wq
中。这个工作队列是整个Linux I/O子系统中负责执行**异步回写(asynchronous writeback)**操作的核心引擎。
基本原理:
当一个应用程序向文件写入数据时, 为了提高性能, Linux内核并不会立即将数据同步写入物理存储设备(如SD卡、闪存芯片)。相反, 它会将数据写入位于RAM中的页缓存(page cache), 并将对应的内存页标记为”脏”(dirty)。之后, 内核会在合适的时机(例如, 系统空闲时、脏数据过多时、或周期性地), 通过后台任务将这些脏页”回写”到持久化存储中。
这个”后台任务”的执行机制就是由bdi_wq
工作队列提供的。内核中的”flusher”线程会创建工作项(work item), 描述需要被回写的脏页, 然后将这些工作项放入bdi_wq
队列中。工作队列的内核线程(worker threads)会从队列中取出这些工作项并执行它们, 最终调用底层存储驱动程序(如SD/MMC驱动)来完成实际的I/O操作。
这种异步机制极大地提高了系统的响应速度和吞吐量, 因为应用程序的写操作可以几乎瞬间完成(只需写入高速的RAM), 而将耗时较长的物理I/O操作推迟到后台进行。
代码逐行解析
1 | /* bdi_wq serves all asynchronous writeback tasks */ |
include/linux/backing-dev.h
mapping_can_writeback 用于检查页面缓存(address_space)是否支持写回操作
- 写回操作是文件系统中将脏数据从内存同步到磁盘的关键机制。该函数通过检查 address_space 所属的 inode 的后备设备(backing device)的能力,决定是否可以执行写回操作
1 | static inline bool mapping_can_writeback(struct address_space *mapping) |
inode_to_wb 将 inode 转换为写回块设备结构体
1 | static inline struct bdi_writeback *inode_to_wb(struct inode *inode) |
inode_to_wb_wbc 将 inode 转换为写回控制结构体中的写回块设备
1 | static inline struct bdi_writeback *inode_to_wb_wbc( |