[TOC]
mm/page-writeback.c 页面回写(Page Writeback) 数据持久化的核心机制 历史与背景 这项技术是为了解决什么特定问题而诞生的? 这项技术是为了解决一个计算机体系结构中的根本性矛盾:内存(RAM)与持久性存储设备(如硬盘、SSD)之间巨大的性能差距 。
性能缓冲 :应用程序向文件写入数据时,如果每次写入都必须等待慢速的存储设备完成,那么整个系统的性能和响应速度将变得无法接受。页面回写机制利用高速的内存作为写回缓存(Write-back Cache) ,允许write()
系统调用将数据写入内存(即 Page Cache)后立即返回,从而极大地提升了应用程序的写入性能和系统吞吐量。
数据持久化 :内存中的数据是易失的,断电后会丢失。因此,必须有一个可靠的机制,将在内存中被修改过的数据(称为“脏页”,Dirty Page)在合适的时机写回到持久性存储设备上,确保数据的最终安全性。
I/O效率优化 :该机制可以将多次小的、离散的写入操作在内存中合并成一次大的、连续的写入操作,然后再提交给存储设备。这种**I/O合并(I/O Coalescing)**大大减少了磁盘寻道次数(对HDD而言)和I/O开销,提高了存储设备的利用效率。
它的发展经历了哪些重要的里程碑或版本迭代? Linux的页面回写机制经历了数次重要的架构演进,以适应不断变化的硬件和工作负载。
早期的 bdflush
守护进程 :在非常早期的内核中(2.4之前),一个名为 bdflush
的内核守护进程负责回写所有的脏数据。这是一种简单但粗粒度的全局性机制。
pdflush
时代 :为了解决 bdflush
的单线程瓶颈问题,内核引入了 pdflush
。这是一个由多个内核线程组成的线程池。任何一个 pdflush
线程都可以回写任何设备上的脏页。这提高了并行性,但也带来了新问题:I/O调度器难以区分来自不同设备的回写请求 ,可能导致多个 pdflush
线程同时向同一个慢速设备写入数据,引发I/O拥塞。
BDI Flusher(现代机制) :为了解决pdflush
的问题,内核引入了Backing Device Info (struct backing_dev_info
) 的概念。每个块设备都有自己的BDI结构,该结构描述了设备的I/O特性。回写工作现在由与特定BDI关联的专有flusher线程(通过通用kworker实现)来完成。这使得回写操作可以基于每个设备进行调度和调整,从而实现了更精细的I/O控制,避免了不必要的I/O竞争,是目前仍在使用的核心架构。
目前该技术的社区活跃度和主流应用情况如何? 页面回写是Linux内存管理和I/O子系统的绝对核心,其代码稳定、高效且仍在不断优化。
社区活跃度 :作为内核性能的关键路径,任何与I/O栈、内存管理相关的重大改进都会涉及对页面回写机制的审视和微调。社区持续对其进行性能分析和优化,以适应新的存储硬件(如NVMe SSDs)和工作负载。
主流应用 :该机制是所有标准Linux文件系统(如ext4, XFS, btrfs)进行I/O操作的基础。从嵌入式设备、桌面系统到大型数据库服务器和超级计算机,任何涉及文件写入的场景都在使用页面回写。对 /proc/sys/vm/dirty_*
等参数的调整,是Linux系统性能调优的关键部分。
核心原理与设计 它的核心工作原理是什么? 页面回写机制是一个由多个策略和触发器驱动的复杂系统。
页面的“变脏” :当进程通过write()
系统调用或写入内存映射文件(memory-mapped file)时,内核会找到对应的Page Cache中的页面,修改其内容,并将其标记为**“Dirty”**状态。此时,write()
调用可以立即返回,应用程序无需等待数据落盘。
脏页的追踪 :所有脏页都被链接到其所属文件的inode
的address_space
结构中,同时也被内核的内存管理系统进行全局追踪。
回写的触发 :回写操作由以下几种条件自动触发:
基于时间的触发 :当一个页面变为“脏”状态后,它有一个“保鲜期”。如果它持续处于脏状态超过一定时间(由 /proc/sys/vm/dirty_expire_centisecs
控制),flusher线程就会被唤醒来回写这些“过期”的脏页。
基于全局阈值的触发 :
后台阈值(Background Threshold) :当整个系统的脏页总量超过一个设定的阈值(由 /proc/sys/vm/dirty_background_ratio
或 dirty_background_bytes
控制)时,内核会唤醒flusher线程在后台 开始回写脏页,这个过程对应用程序是透明的。
主动阈值(Active Threshold) :当脏页总量继续增长,超过一个更高的阈值(由 /proc/sys/vm/dirty_ratio
或 dirty_bytes
控制)时,系统认为脏页压力过大。此时,正在产生脏页的进程本身 将会被阻塞,并被强制要求执行回写操作(即“写时阻塞”),直到脏页数量降到阈值以下。这是一种反向压力机制,防止系统被无限的脏页淹没。
显式调用触发 :当用户或应用程序调用sync()
、fsync()
、msync()
等系统调用时,会强制、同步地将指定的脏页(或全部脏页)回写到存储设备。
内存回收触发 :当系统内存不足,需要回收页面时,如果遇到一个干净的页面,可以直接回收;如果遇到一个脏的页面,必须先将其回写到磁盘,才能将其回收。
回写的执行 :被唤醒的flusher线程会扫描脏页列表,通过文件系统的writepages
方法,将脏页的数据提交给块设备层,最终由I/O调度器安排写入物理设备。
它的主要优势体现在哪些方面?
高性能 :通过将写操作缓冲在内存中,极大地降低了应用程序感受到的I/O延迟。
高吞吐量 :通过I/O合并,将多次小写操作聚合成大写操作,提升了存储设备的利用效率。
系统响应性 :防止了应用程序因等待慢速I/O而被长时间阻塞。
可调优性 :提供了丰富的sysctl
参数,允许系统管理员根据具体的硬件和工作负载进行精细的性能调优。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
数据丢失风险 :这是写回缓存的固有权衡。在数据被写入内存但尚未回写到磁盘的时间窗口内,如果发生系统崩溃或断电,这部分数据将会永久丢失。需要高数据一致性的应用(如数据库)必须使用fsync()
等调用来确保关键数据的落盘。
I/O突发 :当积累了大量脏页后,回写操作可能导致突发性的、剧烈的I/O活动(I/O Storm),这可能会影响系统中其他对延迟敏感的应用的性能。
调优复杂性 :默认的内核参数是为通用场景设计的,可能不适用于特定 workload。例如,对于大量突发写入的场景,不当的参数可能会导致系统响应迟钝或性能下降,找到最优参数需要深入的知识和实验。
使用场景 在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。 页面回写是Linux文件I/O的默认和基础行为,因此它适用于几乎所有标准的文件操作场景。
通用文件操作 :用户执行cp
或mv
命令复制大文件。cp
进程会快速地将数据读入和写出Page Cache,命令执行完毕可能远早于数据完全落盘的时间点,用户可以继续执行其他操作,而内核的flusher线程在后台完成回写。
数据库系统 :数据库在处理事务时,会修改其数据文件,从而产生大量脏页。数据库依赖内核高效的回写机制来处理大部分的I/O。但在事务提交(Commit)的关键点,数据库必须调用fsync()
来强制回写其事务日志(WAL/Redo Log),以保证事务的持久性(ACID中的D)。
Web服务器和日志记录 :Web服务器持续不断地写入访问日志。页面回写机制将这些频繁的小写入缓存在内存中,合并成较大的写入操作,大大降低了对日志文件所在磁盘的I/O压力。
是否有不推荐使用该技术的场景?为什么?
需要自己管理缓存的应用程序 :一些高性能数据库(如Oracle)或存储应用,为了实现更精细的I/O控制和避免内核Page Cache与应用层缓存造成的“双重缓存”(Double Cache)问题,会选择绕过Page Cache。它们通过打开文件时指定O_DIRECT
标志来实现直接I/O(Direct I/O) 。在这种模式下,数据直接在应用程序的用户空间缓冲区和存储设备之间传输,完全绕过了页面回写机制。
对比分析 请将其 与 其他相似技术 进行详细对比。 页面回写(Write-back Caching)是缓存策略的一种,可以与其他策略和技术进行对比。
对比一:Write-back vs. Write-through Caching
特性
Page Writeback (写回缓存)
Write-through Caching (写穿缓存)
写操作流程
数据写入缓存后,写操作立即返回。缓存块被标记为“脏”,由后台机制在稍后写回存储。
数据写入缓存的同时,也必须同步地写入后端存储。只有当两者都完成后,写操作才返回。
性能
高 。应用程序延迟低,不直接受存储速度影响。
低 。应用程序延迟直接受限于后端存储的写入速度。
数据一致性
较低 。在发生故障时,存在数据丢失的风险窗口。
高 。写操作一旦返回,数据就保证已在持久化存储中。
Linux中实现
默认行为 。
可以通过以sync
模式挂载文件系统来模拟。这会导致所有写操作都同步进行,性能极差,通常只用于特殊场景。
对比二:Page Writeback vs. 文件系统日志 (Journaling)
这两者是互补而非竞争关系,但常常被混淆。它们解决了不同层面的问题。
特性
Page Writeback (页面回写)
Filesystem Journaling (如ext4, XFS的日志)
保护对象
文件数据 (Data) 。关注的是文件内容本身是否最终被写入。
文件系统元数据 (Metadata) 。关注的是文件系统的结构一致性,如目录条目、inode表、空闲块位图等。
核心目标
性能 和最终的数据持久化 。
一致性 (Consistency) 。防止系统崩溃导致文件系统结构损坏(例如,文件大小正确但数据块丢失)。
工作方式
将脏的数据页缓存起来,延迟写入。
在修改元数据之前,先将要做的修改操作(“事务”)以日志的形式写入一块连续的磁盘区域(Journal)。即使在修改过程中崩溃,重启后可以通过回放日志来恢复文件系统的一致状态。
关系
页面回写负责最终将数据块 和元数据块 写入其在磁盘上的最终位置。而日志系统则确保了在这些写入(特别是元数据写入)发生之前,操作已经被安全地记录。它们协同工作,fsync
通常会同时触发数据回写和日志提交。
mm/page-writeback.c wb_bandwidth_estimate_start 初始化或更新写回设备(bdi_writeback)的带宽估算状态 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #define WB_BANDWIDTH_IDLE_JIF (HZ) static void wb_bandwidth_estimate_start (struct bdi_writeback *wb) { unsigned long now = jiffies; unsigned long elapsed = now - READ_ONCE(wb->bw_time_stamp); if (elapsed > WB_BANDWIDTH_IDLE_JIF && !atomic_read (&wb->writeback_inodes)) { spin_lock(&wb->list_lock); wb->dirtied_stamp = wb_stat(wb, WB_DIRTIED); wb->written_stamp = wb_stat(wb, WB_WRITTEN); WRITE_ONCE(wb->bw_time_stamp, now); spin_unlock(&wb->list_lock); } }
update_dirty_limit 用于动态调整写回域(wb_domain)的脏页面限制(dirty_limit) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 static void update_dirty_limit (struct dirty_throttle_control *dtc) { struct wb_domain *dom = dtc_dom(dtc); unsigned long thresh = dtc->thresh; unsigned long limit = dom->dirty_limit; if (limit < thresh) { limit = thresh; goto update; } thresh = max(thresh, dtc->dirty); if (limit > thresh) { limit -= (limit - thresh) >> 5 ; goto update; } return ; update: dom->dirty_limit = limit; }
domain_update_dirty_limit 更新写回域(wb_domain)的脏页面限制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static void domain_update_dirty_limit (struct dirty_throttle_control *dtc, unsigned long now) { struct wb_domain *dom = dtc_dom(dtc); if (time_before(now, dom->dirty_limit_tstamp + BANDWIDTH_INTERVAL)) return ; spin_lock(&dom->lock); if (time_after_eq(now, dom->dirty_limit_tstamp + BANDWIDTH_INTERVAL)) { update_dirty_limit(dtc); dom->dirty_limit_tstamp = now; } spin_unlock(&dom->lock); }
wb_update_write_bandwidth 更新写回设备(bdi_writeback)的写回带宽统计信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 static void wb_update_write_bandwidth (struct bdi_writeback *wb, unsigned long elapsed, unsigned long written) { const unsigned long period = roundup_pow_of_two(3 * HZ); unsigned long avg = wb->avg_write_bandwidth; unsigned long old = wb->write_bandwidth; u64 bw; bw = written - min(written, wb->written_stamp); bw *= HZ; if (unlikely(elapsed > period)) { bw = div64_ul(bw, elapsed); avg = bw; goto out; } bw += (u64)wb->write_bandwidth * (period - elapsed); bw >>= ilog2(period); if (avg > old && old >= (unsigned long )bw) avg -= (avg - old) >> 3 ; if (avg < old && old <= (unsigned long )bw) avg += (old - avg) >> 3 ; out: avg = max(avg, 1LU ); if (wb_has_dirty_io(wb)) { long delta = avg - wb->avg_write_bandwidth; WARN_ON_ONCE(atomic_long_add_return(delta, &wb->bdi->tot_write_bandwidth) <= 0 ); } wb->write_bandwidth = bw; WRITE_ONCE(wb->avg_write_bandwidth, avg); }
wb_update_dirty_ratelimit 动态调整写回设备(bdi_writeback)的脏页面速率限制(dirty_ratelimit) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 static void wb_update_dirty_ratelimit (struct dirty_throttle_control *dtc, unsigned long dirtied, unsigned long elapsed) { struct bdi_writeback *wb = dtc->wb; unsigned long dirty = dtc->dirty; unsigned long freerun = dirty_freerun_ceiling(dtc->thresh, dtc->bg_thresh); unsigned long limit = hard_dirty_limit(dtc_dom(dtc), dtc->thresh); unsigned long setpoint = (freerun + limit) / 2 ; unsigned long write_bw = wb->avg_write_bandwidth; unsigned long dirty_ratelimit = wb->dirty_ratelimit; unsigned long dirty_rate; unsigned long task_ratelimit; unsigned long balanced_dirty_ratelimit; unsigned long step; unsigned long x; unsigned long shift; dirty_rate = (dirtied - wb->dirtied_stamp) * HZ / elapsed; task_ratelimit = (u64)dirty_ratelimit * dtc->pos_ratio >> RATELIMIT_CALC_SHIFT; task_ratelimit++; balanced_dirty_ratelimit = div_u64((u64)task_ratelimit * write_bw, dirty_rate | 1 ); if (unlikely(balanced_dirty_ratelimit > write_bw)) balanced_dirty_ratelimit = write_bw; step = 0 ; if (unlikely(wb->bdi->capabilities & BDI_CAP_STRICTLIMIT)) { dirty = dtc->wb_dirty; setpoint = (dtc->wb_thresh + dtc->wb_bg_thresh) / 2 ; } if (dirty < setpoint) { x = min3(wb->balanced_dirty_ratelimit, balanced_dirty_ratelimit, task_ratelimit); if (dirty_ratelimit < x) step = x - dirty_ratelimit; } else { x = max3(wb->balanced_dirty_ratelimit, balanced_dirty_ratelimit, task_ratelimit); if (dirty_ratelimit > x) step = dirty_ratelimit - x; } shift = dirty_ratelimit / (2 * step + 1 ); if (shift < BITS_PER_LONG) step = DIV_ROUND_UP(step >> shift, 8 ); else step = 0 ; if (dirty_ratelimit < balanced_dirty_ratelimit) dirty_ratelimit += step; else dirty_ratelimit -= step; WRITE_ONCE(wb->dirty_ratelimit, max(dirty_ratelimit, 1UL )); wb->balanced_dirty_ratelimit = balanced_dirty_ratelimit; trace_bdi_dirty_ratelimit(wb, dirty_rate, task_ratelimit); }
__wb_update_bandwidth 更新写回设备(bdi_writeback)的带宽统计信息和相关限制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 static void __wb_update_bandwidth(struct dirty_throttle_control *gdtc, struct dirty_throttle_control *mdtc, bool update_ratelimit) { struct bdi_writeback *wb = gdtc->wb; unsigned long now = jiffies; unsigned long elapsed; unsigned long dirtied; unsigned long written; spin_lock(&wb->list_lock); elapsed = max(now - wb->bw_time_stamp, 1UL ); dirtied = percpu_counter_read(&wb->stat[WB_DIRTIED]); written = percpu_counter_read(&wb->stat[WB_WRITTEN]); if (update_ratelimit) { domain_update_dirty_limit(gdtc, now); wb_update_dirty_ratelimit(gdtc, dirtied, elapsed); if (IS_ENABLED(CONFIG_CGROUP_WRITEBACK) && mdtc) { domain_update_dirty_limit(mdtc, now); wb_update_dirty_ratelimit(mdtc, dirtied, elapsed); } } wb_update_write_bandwidth(wb, elapsed, written); wb->dirtied_stamp = dirtied; wb->written_stamp = written; WRITE_ONCE(wb->bw_time_stamp, now); spin_unlock(&wb->list_lock); } void wb_update_bandwidth (struct bdi_writeback *wb) { struct dirty_throttle_control gdtc = { GDTC_INIT(wb) }; __wb_update_bandwidth(&gdtc, NULL , false ); }
do_writepages 将页面缓存(address_space)中的脏页面写入磁盘 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 int do_writepages (struct address_space *mapping, struct writeback_control *wbc) { int ret; struct bdi_writeback *wb ; if (wbc->nr_to_write <= 0 ) return 0 ; wb = inode_to_wb_wbc(mapping->host, wbc); wb_bandwidth_estimate_start(wb); while (1 ) { if (mapping->a_ops->writepages) ret = mapping->a_ops->writepages(mapping, wbc); else ret = 0 ; if (ret != -ENOMEM || wbc->sync_mode != WB_SYNC_ALL) break ; reclaim_throttle(NODE_DATA(numa_node_id()), VMSCAN_THROTTLE_WRITEBACK); } if (time_is_before_jiffies(READ_ONCE(wb->bw_time_stamp) + BANDWIDTH_INTERVAL)) wb_update_bandwidth(wb); return ret; }
folio_wait_writeback 等待 folio(页面的抽象表示)的写回操作完成 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void folio_wait_writeback (struct folio *folio) { while (folio_test_writeback(folio)) { trace_folio_wait_writeback(folio, folio_mapping(folio)); folio_wait_bit(folio, PG_writeback); } } EXPORT_SYMBOL_GPL(folio_wait_writeback);
wb_domain_init 初始化写回域(wb_domain) 1 2 3 4 5 6 7 8 9 10 11 12 int wb_domain_init (struct wb_domain *dom, gfp_t gfp) { memset (dom, 0 , sizeof (*dom)); spin_lock_init(&dom->lock); timer_setup(&dom->period_timer, writeout_period, TIMER_DEFERRABLE); dom->dirty_limit_tstamp = jiffies; return fprop_global_init(&dom->completions, gfp); }
page_writeback_init 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void __init page_writeback_init (void ) { BUG_ON(wb_domain_init(&global_wb_domain, GFP_KERNEL)); cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, "mm/writeback:online" , page_writeback_cpu_online, NULL ); cpuhp_setup_state(CPUHP_MM_WRITEBACK_DEAD, "mm/writeback:dead" , NULL , page_writeback_cpu_online); #ifdef CONFIG_SYSCTL register_sysctl_init("vm" , vm_page_writeback_sysctls); #endif }