[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的回写机制经历了几个重要的演进阶段:

  1. bdflushkupdate 时代:非常早期的内核使用这两个独立的守护进程。bdflush在内存压力大时被唤醒,kupdate则定期执行。逻辑简单,效率低下。
  2. pdflush 时代:引入了一个由多个内核线程组成的pdflush池,取代了bdflushkupdate。相比之前是巨大进步,但仍存在上文提到的全局瓶颈和队头阻塞问题。
  3. BDI Flusher Threads 时代(当前):这是决定性的变革。backing_dev_info (BDI) 结构被赋予了更重要的角色。内核为每个独立的后备设备(更准确地说是每个请求队列)创建了专属的flusher kworker线程。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展开的策略与执行框架。

  1. 数据结构

    • struct backing_dev_info (BDI):代表一个后备存储设备(或一组共享队列的设备)的回写属性和状态。它包含了最小/最大回写比率、拥塞状态等策略信息。
    • struct bdi_writeback:代表一个实际的回写执行单元。它包含了脏页的链表、统计信息,并与一个专属的flusher内核线程关联。
  2. 触发回写:回写操作由多种事件触发:

    • 内存压力:当系统中的总脏页数量超过vm.dirty_background_ratio(或_bytes)时,BDI的后台flusher线程会被唤醒,开始异步地将脏页写回磁盘。
    • 时间到期:当一个脏页在内存中停留的时间超过vm.dirty_expire_centisecs时,它也会被后台flusher线程回收。
    • 同步请求:当用户调用sync()fsync()msync()时,或者当一个进程的脏页数量超过限制并需要同步等待回写时(例如总脏页超过vm.dirty_ratio),会触发一个同步的回写请求。
  3. 执行回写

    • 当一个BDI的flusher线程被唤醒时,它会从该设备关联的bdi_writeback结构中获取脏页列表(这些脏页由文件系统在弄脏它们时链接上去)。
    • 线程将这些脏页组织成bio请求,并提交给块设备层。
  4. 拥塞控制与写者节流(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
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
static inline bool sb_is_blkdev_sb(struct super_block *sb)
{
return IS_ENABLED(CONFIG_BLOCK) && sb == blockdev_superblock;
}

/* bdi_init(&noop_backing_dev_info); */
struct backing_dev_info noop_backing_dev_info;
EXPORT_SYMBOL_GPL(noop_backing_dev_info);

struct backing_dev_info *inode_to_bdi(struct inode *inode)
{
struct super_block *sb;
/* 表示无操作的后备设备 */
if (!inode)
return &noop_backing_dev_info;
/* 获取 inode 所属的超级块(super_block)。
超级块是文件系统的核心数据结构之一,包含文件系统的全局信息。 */
sb = inode->i_sb;
#ifdef CONFIG_BLOCK
/* 检查超级块是否属于块设备 */
if (sb_is_blkdev_sb(sb))
/* 通过 I_BDEV(inode) 获取块设备信息,并返回其关联的 bdi */
return I_BDEV(inode)->bd_disk->bdi;
#endif
return sb->s_bdi;
}
EXPORT_SYMBOL(inode_to_bdi);

wb_init

  • 此函数是bdi_init的逻辑延续,用于初始化一个struct bdi_writeback(通常简称为wb)对象。如果说bdi是块设备的静态“档案”,那么wb就是负责执行该设备写回操作的动态“工作单元”或“执行引擎”。它包含了执行脏数据回写所需的所有状态、列表、锁和后台任务。
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
/*
* 初始写入带宽:100 MB/s。
* 这是一个通用的、较为乐观的默认值。实际带宽会由内核根据
* 真实I/O性能动态调整。表达式 (20 - PAGE_SHIFT) 将字节单位
* 转换为页单位,因为内核以页为单位管理内存。
*/
#define INIT_BW (100 << (20 - PAGE_SHIFT))

// wb_init - 初始化一个 bdi_writeback 结构体。
// @wb: 指向要初始化的 bdi_writeback 结构体的指针。
// @bdi: 与此工作单元关联的后备设备信息结构体。
// @gfp: 内存分配标志,用于后续的内部内存申请。
static int wb_init(struct bdi_writeback *wb, struct backing_dev_info *bdi,
gfp_t gfp)
{
int err;

// 使用0填充整个结构体,确保所有字段都有一个已知的初始状态。
memset(wb, 0, sizeof(*wb));

// 建立反向链接,将此工作单元与其所属的bdi档案关联起来。
wb->bdi = bdi;
// 记录上一次冲洗(flush)旧脏数据的时间点,用于老化逻辑。
wb->last_old_flush = jiffies;

// 初始化一系列链表头,用于跟踪不同状态下的inode:
// b_dirty: 存放有脏数据、等待被回写的inode。
INIT_LIST_HEAD(&wb->b_dirty);
// b_io: 存放在回写过程中、正在进行I/O操作的inode。
INIT_LIST_HEAD(&wb->b_io);
// b_more_io: 存放需要立即进行更多I/O的inode。
INIT_LIST_HEAD(&wb->b_more_io);
// b_dirty_time: 用于基于时间的脏数据回写,通常按变脏时间排序。
INIT_LIST_HEAD(&wb->b_dirty_time);

// 初始化一个自旋锁。即使在单核系统上,此锁也是必需的,
// 用于保护上述链表免受中断处理程序的并发访问。
spin_lock_init(&wb->list_lock);

// 初始化一个原子计数器,用于跟踪正在被回写的inode数量。
atomic_set(&wb->writeback_inodes, 0);

// 初始化带宽估算相关的字段:
wb->bw_time_stamp = jiffies; // 带宽计算的时间戳。
wb->balanced_dirty_ratelimit = INIT_BW; // 平衡脏页回写的目标速率。
wb->dirty_ratelimit = INIT_BW; // 脏页回写的速率上限。
wb->write_bandwidth = INIT_BW; // 当前估算的写入带宽。
wb->avg_write_bandwidth = INIT_BW; // 平均写入带宽。

// 初始化用于管理后台任务的自旋锁和链表头。
spin_lock_init(&wb->work_lock);
INIT_LIST_HEAD(&wb->work_list);

// 初始化两个延迟工作(delayed work)项,这是异步执行的核心:
// dwork: 这是主回写任务。当被触发时,内核的工作队列线程会在后台执行 `wb_workfn` 函数。
INIT_DELAYED_WORK(&wb->dwork, wb_workfn);
// bw_dwork: 这是带宽更新任务。它会周期性地在后台执行 `wb_update_bandwidth_workfn` 来调整带宽估算。
INIT_DELAYED_WORK(&wb->bw_dwork, wb_update_bandwidth_workfn);

/*
* 初始化一个per-CPU的分数比例控制器。这是用于I/O调度的复杂机制。
* 在单核STM32上,`percpu`修饰的函数只会为其分配和初始化一个实例。
*/
err = fprop_local_init_percpu(&wb->completions, gfp);
if (err)
return err;

/*
* 初始化一组per-CPU计数器,用于统计各种回写相关的事件(如写入的页数)。
* 在单核系统上,这等价于初始化一组普通的计数器。
*/
err = percpu_counter_init_many(wb->stat, 0, gfp, NR_WB_STAT_ITEMS);
if (err)
// 如果第二次内存分配失败,必须撤销第一次的分配,以防内存泄漏。
fprop_local_destroy_percpu(&wb->completions);

return err;
}

bdi_init - 初始化一个 backing_dev_info 结构体

  • 此函数是Linux内核中块设备I/O子系统的一个基础函数。它的核心职责是初始化一个struct backing_dev_info(简称bdi)结构体。这个结构体是内核用来描述一个“后备设备”(通常是块设备,如SD卡、eMMC、QSPI Flash等)的I/O特性和状态的元数据集合,尤其与写回缓存(writeback caching)策略紧密相关。
    对于STM32H750
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
static int cgwb_bdi_init(struct backing_dev_info *bdi)
{
return wb_init(&bdi->wb, bdi, GFP_KERNEL);
}

// bdi_init - 初始化一个 backing_dev_info 结构体。
// @bdi: 指向要被初始化的 bdi 结构体的指针。
int bdi_init(struct backing_dev_info *bdi)
{
// 将bdi关联的设备指针初始化为NULL。这个链接通常在稍后阶段建立。
bdi->dev = NULL;

// 初始化引用计数器(kref)。kref是内核中管理对象生命周期的标准机制。
// 初始化后,此bdi对象的引用计数为1。
kref_init(&bdi->refcnt);

// 设置此设备相关的脏页占系统总脏页的最小比例,默认为0。
bdi->min_ratio = 0;
// 设置最大比例,默认为100%。BDI_RATIO_SCALE用于提供计算精度。
bdi->max_ratio = 100 * BDI_RATIO_SCALE;
// 初始化用于比例带宽控制器的最大属性分数,这是高级I/O调度的一部分。
bdi->max_prop_frac = FPROP_FRAC_BASE;

// 初始化bdi_list链表头。内核中存在一个全局的链表,用于链接系统中所有的bdi实例。
// 此操作确保该bdi可以被安全地添加到全局链表中。
INIT_LIST_HEAD(&bdi->bdi_list);

// 初始化wb_list链表头。'wb'代表'writeback'。这个链表用于链接属于此bdi的、
// 含有脏数据的inode(即需要被写回设备的文件元数据)。
INIT_LIST_HEAD(&bdi->wb_list);

// 初始化一个等待队列头。当进程需要等待此设备上的写回操作完成时,
// 它会在此等待队列上睡眠。例如,执行sync()系统调用时可能会使用。
init_waitqueue_head(&bdi->wb_waitq);

// 记录上一次因脏页压力而导致进程睡眠的时间点。'jiffies'是内核的全局节拍计数器。
// 此字段用于实现I/O限流(throttling)。
bdi->last_bdp_sleep = jiffies;

/*
* 调用 cgwb_bdi_init() 将此bdi实例与控制组的写回机制(cgroup writeback)进行关联。
* 这是为了在支持cgroup的系统上实现对每个控制组的I/O带宽进行精细化控制。
*
* 在为STM32H750这类资源受限的嵌入式设备编译内核时,
* CONFIG_CGROUPS 配置项通常是关闭的,以节省大量的代码和内存。
* 在这种情况下,cgwb_bdi_init() 会被编译为一个空内联函数,
* 仅仅返回0(表示成功),不执行任何实际操作。
*/
return cgwb_bdi_init(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
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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
/*
* read_ahead_kb_store: 'read_ahead_kb' sysfs文件的写操作处理函数.
* 当用户执行 'echo 128 > /sys/class/bdi/mmcblk0/read_ahead_kb' 时, 此函数被调用.
*
* @dev: 指向bdi设备(struct device)的指针.
* @attr: 指向被写入属性的描述符.
* @buf: 指向用户写入内容的缓冲区的指针.
* @count: 写入内容的字节数.
* @return: 成功时返回已处理的字节数, 失败时返回负值错误码.
*/
static ssize_t read_ahead_kb_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
/*
* dev_get_drvdata(dev) 从设备结构体中获取其私有数据指针, 这里就是指向 backing_dev_info (bdi) 实例的指针.
*/
struct backing_dev_info *bdi = dev_get_drvdata(dev);
unsigned long read_ahead_kb;
ssize_t ret;

/*
* 调用 kstrtoul 将用户输入的字符串(buf)转换为一个无符号长整型数.
*/
ret = kstrtoul(buf, 10, &read_ahead_kb);
if (ret < 0)
return ret;

/*
* 将用户设置的预读值(单位为KB)转换为内核内部使用的单位(页数), 并存入bdi->ra_pages.
* PAGE_SHIFT 是页大小的对数(例如, 4KB页是12), 10是1KB的对数.
* (PAGE_SHIFT - 10) 得到的就是 KB 和 页大小之间的转换因子(的对数).
*/
bdi->ra_pages = read_ahead_kb >> (PAGE_SHIFT - 10);

/*
* 返回已处理的字节数, 表示写入成功.
*/
return count;
}

/*
* 这是一个宏, 用于快速定义一个 BDI 属性的 _show 函数和一个完整的可读写(RW) sysfs 属性.
* @name: 属性的名称 (例如 read_ahead_kb).
* @expr: 用于从 bdi 结构体中提取值的表达式.
*/
#define BDI_SHOW(name, expr) \
/*
* ## 是宏中的连接操作符, 它会创建像 read_ahead_kb_show 这样的函数名.
*/
static ssize_t name##_show(struct device *dev, \
struct device_attribute *attr, char *buf) \
{ \
struct backing_dev_info *bdi = dev_get_drvdata(dev); \
\
/*
* 调用 sysfs_emit, 将 expr 表达式计算出的值格式化后写入用户缓冲区.
*/
return sysfs_emit(buf, "%lld\n", (long long)expr); \
} \
/*
* DEVICE_ATTR_RW 宏会利用上面定义的 name_show 和之前定义的 name_store 函数,
* 创建一个完整的、名为 dev_attr_name 的可读写设备属性结构体.
*/
static DEVICE_ATTR_RW(name);

/*
* 使用 BDI_SHOW 宏来定义 'read_ahead_kb' 属性.
* K(bdi->ra_pages) 是一个宏, 用于将页数转换回KB单位.
*/
BDI_SHOW(read_ahead_kb, K(bdi->ra_pages))

/* --- min_ratio --- */
/*
* min_ratio_store: 'min_ratio' sysfs文件的写操作处理函数.
* 'min_ratio' 是一个百分比, 它控制当可用内存低于某个阈值时, 内核开始回写脏页的积极程度.
*/
static ssize_t min_ratio_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t count)
{
struct backing_dev_info *bdi = dev_get_drvdata(dev);
unsigned int ratio;
ssize_t ret;

ret = kstrtouint(buf, 10, &ratio);
if (ret < 0)
return ret;

/* 调用 bdi_set_min_ratio 设置新的百分比. */
ret = bdi_set_min_ratio(bdi, ratio);
if (!ret)
ret = count;

return ret;
}
/* 使用 BDI_SHOW 定义 'min_ratio' 属性. bdi->min_ratio 是一个放大后的值, 这里将其缩放回百分比. */
BDI_SHOW(min_ratio, bdi->min_ratio / BDI_RATIO_SCALE)

/* --- min_ratio_fine --- */
/*
* min_ratio_fine_store: 'min_ratio_fine' sysfs文件的写操作处理函数.
* 这个版本允许不经缩放, 直接设置内部使用的原始值, 提供更精细的控制.
*/
static ssize_t min_ratio_fine_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t count)
{
struct backing_dev_info *bdi = dev_get_drvdata(dev);
unsigned int ratio;
ssize_t ret;

ret = kstrtouint(buf, 10, &ratio);
if (ret < 0)
return ret;

ret = bdi_set_min_ratio_no_scale(bdi, ratio);
if (!ret)
ret = count;

return ret;
}
/* 使用 BDI_SHOW 定义 'min_ratio_fine' 属性, 直接显示内部的原始值. */
BDI_SHOW(min_ratio_fine, bdi->min_ratio)

/* --- max_ratio --- */
/*
* max_ratio_store: 'max_ratio' sysfs文件的写操作处理函数.
* 'max_ratio' 是一个百分比, 它定义了系统总内存中, 这个设备可以拥有的脏页的最大比例.
* 超过这个比例, 生成脏页的进程将会被阻塞, 直到一些脏页被回写为止.
*/
static ssize_t max_ratio_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t count)
{
struct backing_dev_info *bdi = dev_get_drvdata(dev);
unsigned int ratio;
ssize_t ret;

ret = kstrtouint(buf, 10, &ratio);
if (ret < 0)
return ret;

ret = bdi_set_max_ratio(bdi, ratio);
if (!ret)
ret = count;

return ret;
}
/* 使用 BDI_SHOW 定义 'max_ratio' 属性. */
BDI_SHOW(max_ratio, bdi->max_ratio / BDI_RATIO_SCALE)

/* --- max_ratio_fine --- */
/* 'max_ratio_fine' 的 store 和 show 函数, 与 min_ratio_fine 类似, 用于精细控制. */
static ssize_t max_ratio_fine_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t count)
{
struct backing_dev_info *bdi = dev_get_drvdata(dev);
unsigned int ratio;
ssize_t ret;

ret = kstrtouint(buf, 10, &ratio);
if (ret < 0)
return ret;

ret = bdi_set_max_ratio_no_scale(bdi, ratio);
if (!ret)
ret = count;

return ret;
}
BDI_SHOW(max_ratio_fine, bdi->max_ratio)

/* --- min_bytes --- */
/* 'min_bytes' 允许用户设置一个绝对的字节数作为最小脏页阈值, 而非百分比. */
static ssize_t min_bytes_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct backing_dev_info *bdi = dev_get_drvdata(dev);

return sysfs_emit(buf, "%llu\n", bdi_get_min_bytes(bdi));
}

static ssize_t min_bytes_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t count)
{
struct backing_dev_info *bdi = dev_get_drvdata(dev);
u64 bytes;
ssize_t ret;

ret = kstrtoull(buf, 10, &bytes);
if (ret < 0)
return ret;

ret = bdi_set_min_bytes(bdi, bytes);
if (!ret)
ret = count;

return ret;
}
static DEVICE_ATTR_RW(min_bytes);

/* --- max_bytes --- */
/* 'max_bytes' 允许用户设置一个绝对的字节数作为最大脏页阈值, 而非百分比. */
static ssize_t max_bytes_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct backing_dev_info *bdi = dev_get_drvdata(dev);

return sysfs_emit(buf, "%llu\n", bdi_get_max_bytes(bdi));
}

static ssize_t max_bytes_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t count)
{
struct backing_dev_info *bdi = dev_get_drvdata(dev);
u64 bytes;
ssize_t ret;

ret = kstrtoull(buf, 10, &bytes);
if (ret < 0)
return ret;

ret = bdi_set_max_bytes(bdi, bytes);
if (!ret)
ret = count;

return ret;
}
static DEVICE_ATTR_RW(max_bytes);

/* --- stable_pages_required (已废弃) --- */
static ssize_t stable_pages_required_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
/* 这个属性已被移除. 每次访问时, 打印一条警告信息. */
dev_warn_once(dev,
"the stable_pages_required attribute has been removed. Use the stable_writes queue attribute instead.\n");
return sysfs_emit(buf, "%d\n", 0);
}
/* DEVICE_ATTR_RO 定义一个只读属性. */
static DEVICE_ATTR_RO(stable_pages_required);

/* --- strict_limit --- */
/*
* strict_limit: 一个开关, 用于控制脏页限制是否为"硬性"限制.
* 开启后, 脏页的限制会更加严格地被执行.
*/
static ssize_t strict_limit_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t count)
{
struct backing_dev_info *bdi = dev_get_drvdata(dev);
unsigned int strict_limit;
ssize_t ret;

ret = kstrtouint(buf, 10, &strict_limit);
if (ret < 0)
return ret;

ret = bdi_set_strict_limit(bdi, strict_limit);
if (!ret)
ret = count;

return ret;
}

static ssize_t strict_limit_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct backing_dev_info *bdi = dev_get_drvdata(dev);

/* 检查 bdi 的能力位掩码, 返回 1 或 0. */
return sysfs_emit(buf, "%d\n",
!!(bdi->capabilities & BDI_CAP_STRICTLIMIT));
}
static DEVICE_ATTR_RW(strict_limit);

/*
* 定义一个静态的 attribute 指针数组, 收集上面定义的所有属性.
*/
static struct attribute *bdi_dev_attrs[] = {
&dev_attr_read_ahead_kb.attr,
&dev_attr_min_ratio.attr,
&dev_attr_min_ratio_fine.attr,
&dev_attr_max_ratio.attr,
&dev_attr_max_ratio_fine.attr,
&dev_attr_min_bytes.attr,
&dev_attr_max_bytes.attr,
&dev_attr_stable_pages_required.attr,
&dev_attr_strict_limit.attr,
NULL, /* 数组必须以 NULL 结尾. */
};
/*
* ATTRIBUTE_GROUPS 宏将属性数组包装成一个属性组.
*/
ATTRIBUTE_GROUPS(bdi_dev);

/*
* 定义一个常量 class 结构体, 描述 "bdi" 这个设备类.
*/
static const struct class bdi_class = {
.name = "bdi", /* 类的名称, 将在 sysfs 中创建 /sys/class/bdi/ */
.dev_groups = bdi_dev_groups, /* 将上面定义的属性组与这个类关联起来. */
};

/*
* bdi_class_init: BDI 类的初始化函数.
*/
static __init int bdi_class_init(void)
{
int ret;

/* 调用 class_register, 正式向内核注册 bdi_class. */
ret = class_register(&bdi_class);
if (ret)
return ret;

/* 初始化 BDI 的 debugfs 接口 (代码未显示). */
bdi_debug_init();

return 0;
}
/*
* postcore_initcall 确保这个初始化函数在内核核心组件初始化之后,
* 在大部分设备驱动加载之前执行.
*/
postcore_initcall(bdi_class_init);

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
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
/* bdi_wq serves all asynchronous writeback tasks */
/* 注释: bdi_wq 服务于所有异步回写任务 */
struct workqueue_struct *bdi_wq;

/*
* 静态的 __init 函数, 表明它是一个仅在内核初始化期间执行的函数.
* __init 宏会将此函数放入特殊的内存节, 这段内存在初始化完成后可以被释放以节省内存.
*/
static int __init default_bdi_init(void)
{
/*
* 调用 alloc_workqueue 来创建一个新的工作队列.
*
* @name: "writeback"
* 工作队列的名称. 这个名称会出现在内核日志和调试工具中(例如 /proc/workqueues 或 top),
* 用于识别和调试.
*
* @flags: WQ_MEM_RECLAIM | WQ_UNBOUND | WQ_SYSFS
* 这是一组按位或组合的标志, 用于定义工作队列的行为:
*
* - WQ_MEM_RECLAIM: 这是一个至关重要的标志. 它告诉内核的内存管理子系统,
* 这个工作队列的工作者线程是用于"内存回收"(memory reclaim)的. 回写脏页的本质就是
* 将数据移出RAM, 从而释放内存。在系统内存极度紧张时, 内核可能会限制新线程的创建,
* 但带有此标志的工作队列是个例外, 它保证了即使在内存压力下, 回写任务也能够被创建和执行,
* 从而打破"需要内存来释放内存"的死锁.
*
* - WQ_UNBOUND: 这个标志创建了一个"非绑定"的工作队列. 这意味着它的工作者线程
* 是普通的内核线程, 由全局的CFS调度器管理, 可以在任何可用的CPU核心上运行.
* 这对于I/O密集型任务是最佳选择, 因为这些任务大部分时间都在睡眠等待硬件,
* 将它们绑定到特定CPU核心是一种浪费. 在单核的STM32H750上, 虽然没有多核优势,
* 但它仍然符合工作队列的通用模型, 工作者线程会被调度器在唯一的CPU核心上运行.
*
* - WQ_SYSFS: 这个标志使得工作队列的信息可以通过sysfs文件系统(通常在
* /sys/bus/workqueue/devices/ 下)暴露给用户空间, 便于监控和调试.
*
* @max_active: 0
* 每个CPU上可以同时处于"活跃"(非睡眠)状态的工作者线程的最大数量.
* 设置为0表示使用系统默认值, 这通常是一个比较大的数字, 允许高度并发的I/O操作.
*/
bdi_wq = alloc_workqueue("writeback", WQ_MEM_RECLAIM | WQ_UNBOUND |
WQ_SYSFS, 0);
/*
* 检查 alloc_workqueue 的返回值. 如果失败(例如, 内存不足), 它会返回NULL.
*/
if (!bdi_wq)
return -ENOMEM; /* 返回"内存不足"错误码. */
/*
* 初始化成功, 返回0.
*/
return 0;
}
/*
* subsys_initcall() 是一个宏, 用于将一个函数注册为内核启动过程中的一个初始化回调.
* 内核启动时会按照预定的顺序(如 core_initcall, postcore_initcall, subsys_initcall,
* device_initcall 等)依次调用这些注册的函数.
* subsys_initcall 确保了 default_bdi_init 会在一个比较早的阶段被调用,
* 在大多数设备驱动程序(它们可能会需要进行回写)初始化之前, 保证 bdi_wq 已经准备就绪.
*/
subsys_initcall(default_bdi_init);

include/linux/backing-dev.h

mapping_can_writeback 用于检查页面缓存(address_space)是否支持写回操作

  • 写回操作是文件系统中将脏数据从内存同步到磁盘的关键机制。该函数通过检查 address_space 所属的 inode 的后备设备(backing device)的能力,决定是否可以执行写回操作
1
2
3
4
5
6
7
8
9
static inline bool mapping_can_writeback(struct address_space *mapping)
{
/* 函数通过 mapping->host 获取与页面缓存关联的 inode。
使用 inode_to_bdi 获取 inode 的后备设备(backing device,简称 bdi)。
后备设备是文件系统与存储设备之间的抽象层,负责管理写回、缓存等操作 */
/* 检查后备设备的 capabilities 字段,判断是否支持写回操作。
BDI_CAP_WRITEBACK 是一个标志,表示后备设备具有写回能力 */
return inode_to_bdi(mapping->host)->capabilities & BDI_CAP_WRITEBACK;
}

inode_to_wb 将 inode 转换为写回块设备结构体

1
2
3
4
static inline struct bdi_writeback *inode_to_wb(struct inode *inode)
{
return &inode_to_bdi(inode)->wb;
}

inode_to_wb_wbc 将 inode 转换为写回控制结构体中的写回块设备

1
2
3
4
5
6
static inline struct bdi_writeback *inode_to_wb_wbc(
struct inode *inode,
struct writeback_control *wbc)
{
return inode_to_wb(inode);
}