[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系统性能调优的关键部分。

核心原理与设计

它的核心工作原理是什么?

页面回写机制是一个由多个策略和触发器驱动的复杂系统。

  1. 页面的“变脏”:当进程通过write()系统调用或写入内存映射文件(memory-mapped file)时,内核会找到对应的Page Cache中的页面,修改其内容,并将其标记为**“Dirty”**状态。此时,write()调用可以立即返回,应用程序无需等待数据落盘。

  2. 脏页的追踪:所有脏页都被链接到其所属文件的inodeaddress_space结构中,同时也被内核的内存管理系统进行全局追踪。

  3. 回写的触发:回写操作由以下几种条件自动触发:

    • 基于时间的触发:当一个页面变为“脏”状态后,它有一个“保鲜期”。如果它持续处于脏状态超过一定时间(由 /proc/sys/vm/dirty_expire_centisecs 控制),flusher线程就会被唤醒来回写这些“过期”的脏页。
    • 基于全局阈值的触发
      • 后台阈值(Background Threshold):当整个系统的脏页总量超过一个设定的阈值(由 /proc/sys/vm/dirty_background_ratiodirty_background_bytes 控制)时,内核会唤醒flusher线程在后台开始回写脏页,这个过程对应用程序是透明的。
      • 主动阈值(Active Threshold):当脏页总量继续增长,超过一个更高的阈值(由 /proc/sys/vm/dirty_ratiodirty_bytes 控制)时,系统认为脏页压力过大。此时,正在产生脏页的进程本身将会被阻塞,并被强制要求执行回写操作(即“写时阻塞”),直到脏页数量降到阈值以下。这是一种反向压力机制,防止系统被无限的脏页淹没。
    • 显式调用触发:当用户或应用程序调用sync()fsync()msync()等系统调用时,会强制、同步地将指定的脏页(或全部脏页)回写到存储设备。
    • 内存回收触发:当系统内存不足,需要回收页面时,如果遇到一个干净的页面,可以直接回收;如果遇到一个脏的页面,必须先将其回写到磁盘,才能将其回收。
  4. 回写的执行:被唤醒的flusher线程会扫描脏页列表,通过文件系统的writepages方法,将脏页的数据提交给块设备层,最终由I/O调度器安排写入物理设备。

它的主要优势体现在哪些方面?

  • 高性能:通过将写操作缓冲在内存中,极大地降低了应用程序感受到的I/O延迟。
  • 高吞吐量:通过I/O合并,将多次小写操作聚合成大写操作,提升了存储设备的利用效率。
  • 系统响应性:防止了应用程序因等待慢速I/O而被长时间阻塞。
  • 可调优性:提供了丰富的sysctl参数,允许系统管理员根据具体的硬件和工作负载进行精细的性能调优。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 数据丢失风险:这是写回缓存的固有权衡。在数据被写入内存但尚未回写到磁盘的时间窗口内,如果发生系统崩溃或断电,这部分数据将会永久丢失。需要高数据一致性的应用(如数据库)必须使用fsync()等调用来确保关键数据的落盘。
  • I/O突发:当积累了大量脏页后,回写操作可能导致突发性的、剧烈的I/O活动(I/O Storm),这可能会影响系统中其他对延迟敏感的应用的性能。
  • 调优复杂性:默认的内核参数是为通用场景设计的,可能不适用于特定 workload。例如,对于大量突发写入的场景,不当的参数可能会导致系统响应迟钝或性能下降,找到最优参数需要深入的知识和实验。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

页面回写是Linux文件I/O的默认和基础行为,因此它适用于几乎所有标准的文件操作场景。

  • 通用文件操作:用户执行cpmv命令复制大文件。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
/* 间隔之后,我们认为 wb 空闲,不估计带宽 */
#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 &&
/* 当前没有正在写回的 inode,则认为写回设备处于空闲状态 */
!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;/* 目标阈值(thresh) */
unsigned long limit = dom->dirty_limit;/* 当前的脏页面限制值 */

/*
* 如果当前限制值低于目标阈值,函数直接将限制值提升到阈值。
这种快速提升避免了系统因限制过低而导致性能下降
*/
if (limit < thresh) {
limit = thresh;
goto update;
}

/*
* 慢慢跟着。使用较高的 target 作为目标,因为 thresh 可能会低于 dirty。这正是引入 dom->dirty_limit 的原因,它保证位于脏页面之上。
*/
/* 如果当前限制值高于目标阈值,函数缓慢降低限制值。
使用位移操作(>> 5)实现逐步调整,确保限制值不会突然下降,避免影响系统稳定性。 */
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)
{
/* 获取写回域(wb_domain) */
struct wb_domain *dom = dtc_dom(dtc);

/*
* 首先检查 lockless 以最长时间地优化 away lock
*/
/* 检查当前时间(now)是否在脏页面限制的更新时间间隔
如果时间未超过间隔,直接返回 */
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 * HZ / elapsed
*
* bw * elapsed + write_bandwidth * (period - elapsed)
* write_bandwidth = ---------------------------------------------------
* period
*
* 定义写回带宽的计算周期,周期长度为 3 秒(3 * HZ),并向上取整为 2 的幂次。
这种周期设计有助于优化计算效率,同时平滑带宽变化
*/
/* 计算写回页面的增量,并将其转换为每秒的写回速率(bw) */
bw = written - min(written, wb->written_stamp);
bw *= HZ;
/* 时间间隔(elapsed)超过周期长度,直接计算带宽并跳转到更新逻辑 */
if (unlikely(elapsed > period)) {
bw = div64_ul(bw, elapsed);
avg = bw;
goto out;
}
bw += (u64)wb->write_bandwidth * (period - elapsed);
bw >>= ilog2(period);

/*
* 多一级平滑,用于过滤掉突然的尖峰
*/
/* 使用加权平均法平滑带宽变化,避免因突发写回操作导致的带宽波动
通过比较当前带宽(bw)、旧带宽(old)和平均带宽(avg),逐步调整平均值*/
if (avg > old && old >= (unsigned long)bw)
avg -= (avg - old) >> 3;

if (avg < old && old <= (unsigned long)bw)
avg += (old - avg) >> 3;

out:
/* 确保平均带宽(avg)始终大于零,以避免统计数据无效 */
avg = max(avg, 1LU);
/* 如果写回设备有脏数据,更新全局写回带宽(tot_write_bandwidth) */
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
/*
* 保持 wb->dirty_ratelimit,即基本脏油门率。
*
* 从长远来看,正常的 wb 任务将受到限制。
* 显然,当有 N dd 个任务时,它应该在 (write_bw / N) 左右。
*/
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;

/*
* 脏页率将长期与写出率匹配,除非脏页被用户空间截断或被 FS 重新弄脏。
*/
/* 计算脏页面生成速率 该速率反映了系统在过去一段时间内生成脏页面的速度*/
dirty_rate = (dirtied - wb->dirtied_stamp) * HZ / elapsed;

/*
* task_ratelimit 反映了每个 DD 在过去 200 毫秒内的脏速率。
* 根据当前速率限制(dirty_ratelimit)和位置比例(pos_ratio),计算每个写回任务的速率限制(task_ratelimit)
*/
task_ratelimit = (u64)dirty_ratelimit *
dtc->pos_ratio >> RATELIMIT_CALC_SHIFT;
task_ratelimit++; /* 通过增加一个单位值,确保速率限制能够从较小值逐步提升 */

/*
* 如何通过线性估算计算“平衡的节流速率”(balanced throttle rate),以优化写回设备(wb)的脏页面生成速率(dirty_rate)
* 理论:如果有 N 个写回任务(dd tasks),每个任务的速率限制为 task_ratelimit,那么写回设备的脏页面生成速率(dirty_rate)将被测量为 N * task_ratelimit。基于此,平衡的节流速率可以通过公式 write_bw / N 计算,其中 write_bw 是写回设备的平均写回带宽。
* 速率反馈公式:rate_(i+1) = rate_(i) * (write_bw / dirty_rate) (1)
表示下一轮的速率限制(rate_(i+1))是当前速率限制(rate_(i))乘以写回带宽与脏页面生成速率的比值。
* 引入位置比例:rate_(i+1) = rate_(i) * (write_bw / dirty_rate) * pos_ratio (2)
公式(2)在公式(1)的基础上引入了位置比例(pos_ratio),用于进一步平衡脏页面生成速率。pos_ratio 是一个动态调整的参数,反映了脏页面数量与目标值的关系。
* 公式(1)在某些情况下可能无法实现理想的速率调整。例如,当 pos_ratio = 0.5 时,速率限制可能会陷入一个固定状态,无法进一步优化。为了解决这一问题,公式(2)被采用,因为它能够确保速率限制始终趋向于 write_bw / N,无论 pos_ratio 的具体值如何。
* 平衡速率的动态调整: 通过公式(2),速率限制能够逐步调整到平衡状态(rate_(i+1) ~= write_bw / N)。当速率限制达到这一状态时,pos_ratio 会逐渐趋向于 1.0。这不仅使脏页面数量达到目标值(setpoint),还确保位置比例的变化最为平缓,从而减少任务速率限制的波动。
*/
balanced_dirty_ratelimit = div_u64((u64)task_ratelimit * write_bw,
dirty_rate | 1);
/*
* balanced_dirty_ratelimit ~= (write_bw / N) <= write_bw
*/
if (unlikely(balanced_dirty_ratelimit > write_bw))
balanced_dirty_ratelimit = write_bw;

/*
* 直接设置速率限制的局限性
虽然可以直接将 dirty_ratelimit 设置为 balanced_dirty_ratelimit 并立即返回,但这种简单的做法可能导致速率限制的不稳定性。balanced_dirty_ratelimit 是通过线性估算计算的理想速率限制,但它可能包含异常点或剧烈波动,尤其是在脏页面生成速率(dirty_rate)估算周期较短(如 200 毫秒)时。
* 使用相对值过滤异常点
为了获得更稳定的 dirty_ratelimit,代码使用 task_ratelimit 和相对值进行调整:
task_ratelimit - dirty_ratelimit = (pos_ratio - 1) * dirty_ratelimit
这个公式反映了脏页面位置误差的方向和大小。
pos_ratio 是一个动态调整的参数,表示脏页面数量与目标值(setpoint)的关系。当 pos_ratio 接近 1 时,误差最小,速率限制最稳定。
*/

/*
* 条件调整速率限制
* 当 dirty_ratelimit 和 task_ratelimit 都高于 balanced_dirty_ratelimit 时,降低 dirty_ratelimit 有助于同时满足位置和速率控制目标。
* 如果调整只会满足速率目标而无法改善位置误差,则不更新 dirty_ratelimit。这种设计确保用户感受到的是稳定的脏页面生成速率和较小的误差。
* 限制步长以减少波动
为了避免 balanced_dirty_ratelimit 的剧烈跳动,代码使用步长限制来平滑调整过程:
|task_ratelimit - dirty_ratelimit|
步长限制过滤掉异常点,并限制速率限制的调整幅度。
这种设计减少了由于短时间估算周期导致的速率限制波动,同时保持调整的灵活性。
*/
step = 0;

/*
* 对于 strictlimit 情况,上述计算基于 wb 计数器和限制(从 pos_ratio = wb_position_ratio() 开始,一直到 balanced_dirty_ratelimit = task_ratelimit * write_bw / dirty_rate)。因此,要正确计算 “step”,我们必须使用 wb_dirty 作为 “dirty” 和 wb_setpoint 作为 “setpoint”。
*/
/* 启用了严格限制(BDI_CAP_STRICTLIMIT) */
if (unlikely(wb->bdi->capabilities & BDI_CAP_STRICTLIMIT)) {
dirty = dtc->wb_dirty;
/* setpoint 是脏页面的目标值,取决于阈值(wb_thresh 和 wb_bg_thresh)的平均值 */
setpoint = (dtc->wb_thresh + dtc->wb_bg_thresh) / 2;
}

/* 如果当前脏页面数量低于目标值,代码选择最小的速率限制值(min3),包括平衡速率限制(balanced_dirty_ratelimit)、任务速率限制(task_ratelimit) */
if (dirty < setpoint) {
x = min3(wb->balanced_dirty_ratelimit,
balanced_dirty_ratelimit, task_ratelimit);
/* 如果当前速率限制(dirty_ratelimit)小于选定值,则计算步长(step)为两者的差值 */
if (dirty_ratelimit < x)
step = x - dirty_ratelimit;
} else {
/* 脏页面数量高于目标值 */
/* 如果当前脏页面数量高于目标值,代码选择最大的速率限制值(max3) */
x = max3(wb->balanced_dirty_ratelimit,
balanced_dirty_ratelimit, task_ratelimit);
/* 如果当前速率限制大于选定值,则计算步长为两者的差值 */
if (dirty_ratelimit > x)
step = dirty_ratelimit - x;
}

/*
* 不要追求 100% 的费率匹配。这是不可能的,因为平衡汇率本身是不断波动的。因此,当跟踪速度接近目标时,请降低跟踪速度。有助于消除无意义的震颤。
*/
/* 函数通过动态调整步长的大小,避免速率限制的剧烈变化,从而减少系统抖动 */
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);

/*
* 对运行时间的无锁检查很活泼,IO 完成后的延迟更新根本不会这样做(以确保写入的页面被合理快速地计算在内)。确保 elapsed >= 1 以避免除法错误。
*/
/* 获取当前时间(jiffies)并计算自上次更新以来的时间间隔(elapsed)。确保时间间隔至少为 1,避免除零错误 */
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);

/*
* @mdtc is always NULL if !CGROUP_WRITEBACK but the
* compiler has no way to figure that out. Help it.
*/
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;
/* 调用 inode_to_wb_wbc 获取与 inode 关联的写回设备(bdi_writeback) */
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
/* 处理 chardevs 和其他特殊文件 */
ret = 0;
if (ret != -ENOMEM || wbc->sync_mode != WB_SYNC_ALL)
break;

/*
* 缺少分配上下文或任何 inode 页面的位置或写回状态,请根据本地节点上的写回活动进行限制。这是一个很好的猜测。
*/
/* 基于 NUMA 节点的写回活动进行节流,避免资源过度消耗 */
reclaim_throttle(NODE_DATA(numa_node_id()),
VMSCAN_THROTTLE_WRITEBACK);
}

/*
* 如果写回操作持续较长时间,调用 wb_update_bandwidth 更新写回带宽估算。
这确保写回带宽能够动态调整以适应系统负载。
*/
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
/**
* folio_wait_writeback - 等待作品集完成写回。
* @folio:要等待的作品集。
*
* 如果作品集当前正在写回存储,请等待 I/O 完成。
*
* 上下文:睡眠。 必须在进程上下文中调用,并且没有持有自旋锁。 调用方应在作品集上保留引用。如果作品集未锁定,则写回可能会在写回完成后再次开始。
*/
void folio_wait_writeback(struct folio *folio)
{
/* 检查 folio 是否处于写回状态
如果 folio 正在写回,函数进入循环等待,直到写回完成*/
while (folio_test_writeback(folio)) {
trace_folio_wait_writeback(folio, folio_mapping(folio));
/* 等待 PG_writeback 标志位清除,表示写回操作完成。
该操作会使当前线程进入睡眠状态,直到写回完成。 */
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
/*
* 提早调用以调整页面回写脏页限制。
*
* 我们过去根据总内存与可分配给缓冲区的页面之间的关系来调整脏页比例。
*
* 然而,那是我们使用“dirty_ratio”来根据所有内存进行调整的时候,
* 而现在我们不再这样做。“dirty_ratio”现在应用于总的非HIGHPAGE内存,
* 因此我们不再会遇到过去那种疯狂的情况,即脏页数量远远超过少量的非HIGHMEM内存。
*
* 但我们可能仍然希望根据设备的内存量来调整脏页比例。
*/
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
}