[toc]

fs/sync.c 数据同步(Data Synchronization) VFS层缓存回写的核心控制

历史与背景

这项技术是为了解决什么特定问题而诞生的?

fs/sync.c 及其实现的一系列系统调用(sync, fsync, fdatasync)是为了解决操作系统设计中一个最根本的矛盾:性能与数据持久性(Durability)之间的权衡

  1. 性能需求:物理磁盘(无论是机械硬盘还是SSD)的读写速度远低于内存(RAM)。为了提高I/O性能,Linux内核引入了页缓存(Page Cache)。当应用程序写入文件时,数据通常只是被写入到内存中的页缓存,并被标记为“脏”(Dirty),然后系统调用会立即返回成功。这种异步写入或**写回缓存(Write-back Cache)**机制极大地提高了写入操作的响应速度。
  2. 数据持久性需求:仅将数据写入内存是不可靠的。如果此时系统突然断电或崩溃,所有存在于页缓存中但尚未写入磁盘的“脏”数据都将永久丢失。对于数据库、文件编辑器、事务日志等关键应用来说,这会导致数据损坏或丢失,是不可接受的。

fs/sync.c中的同步(Synchronization)机制就是为了解决这个问题而生。它为应用程序和系统管理员提供了一种强制性的手段,命令内核将缓存中的“脏”数据和元数据立即或尽快地**回写(Write back)**到底层的持久化存储设备上,从而确保数据的安全落地。

它的发展经历了哪些重要的里程碑或版本迭代?

同步机制的演进体现了从粗粒度到细粒度的控制优化过程。

  • sync(2)系统调用的诞生:这是最古老、最简单的同步机制,直接源于早期Unix。它是一个全局性的、重量级的操作,会启动对系统中所有文件系统的所有脏数据和元数据的回写,但它通常不会等待I/O操作完成就会返回。
  • fsync(2)系统调用的引入:随着应用对数据一致性要求的提高,需要一种更精确的控制。fsync(2)应运而生,它针对单个文件描述符操作。它不仅会将指定文件的脏数据和元数据(如文件大小、修改时间等)回写到磁盘,而且必须阻塞等待,直到物理I/O操作完全完成后才会返回。这为实现事务性操作提供了原子性的保证。
  • fdatasync(2)的优化fsync非常安全,但有时也过于“彻底”。例如,一个数据库在写入数据文件时,它只关心数据本身是否落盘,而不太关心文件的最后修改时间(mtime)是否也同步更新。更新元数据通常会引发额外的磁盘I/O。为此,fdatasync(2)被引入,它与fsync类似,但有一个关键优化:它只会回写那些为了保证数据可访问性所必需的元数据(例如,更新文件大小以确保新写入的数据能被读到),而可能会省略非关键的元数据(如mtime, atime)的同步,从而在某些场景下获得更好的性能。
  • 内核后台回写(Background Writeback):除了这些由用户触发的同步调用,fs/sync.c也与内核的后台回写机制紧密相关。内核有专门的flusher线程(现在是kworker的一部分),会周期性地、或在内存压力大时,自动将脏页回写到磁盘,以释放内存并防止脏数据在内存中停留过久。

目前该技术的社区活跃度和主流应用情况如何?

fs/sync.c是VFS和存储子系统中极其核心和稳定的部分。

  • 主流应用
    • 所有数据库系统(PostgreSQL, MySQL/InnoDB等):在提交事务时,必须调用fsync()fdatasync()来确保事务日志(WAL, Redo Log)已持久化。
    • 文件编辑器和办公软件:当用户点击“保存”时,程序会在写入文件后调用fsync()来防止数据丢失。
    • 文件系统工具mkfs, fsck等在对文件系统进行重大修改后会调用同步操作。
    • 系统管理员:在执行关机、重启或卸载文件系统等操作前,会手动执行sync命令,以确保所有缓存数据都被清空,防止文件系统损坏。

核心原理与设计

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

fs/sync.c是VFS层的一个协调者,它将上层的系统调用请求转换为对底层文件系统和块设备的回写操作。

  1. 标记“脏”状态:当用户通过write()修改文件时,VFS和页缓存会将对应的内存页(struct page)标记为Dirty,同时也会将该文件对应的inode标记为脏。
  2. sync()的工作流程
    • 调用sync()系统调用时,内核会触发一个系统范围的回写。
    • 它会遍历所有已挂载的文件系统(超级块列表),并对每个文件系统的脏inode列表、脏数据页等发起回写。
    • 这是一个异步发起的过程,sync()调用本身会很快返回,而回写操作则在后台进行。
  3. fsync(fd)的工作流程
    • 调用fsync(fd)时,内核会从文件描述符fd找到对应的struct file对象,再找到struct inode
    • 它会调用具体文件系统实现的file_operations->fsync函数。
    • 这个函数负责两件事:
      1. 将该inode关联的所有脏数据页提交给块设备层进行写入。
      2. 将该inode自身的脏元数据也提交写入。
    • 最关键的是,这个函数必须阻塞等待,直到它确认相关的I/O请求已经被物理设备报告完成
  4. fdatasync(fd)的工作流程
    • 流程与fsync非常相似,但它调用的文件系统回调函数在处理元数据时会进行区分。它只会回写那些影响数据访问的元数据(主要是文件大小),而可能会跳过那些不影响的元数据(如时间戳)。

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

  • 提供数据持久性保证:为需要高可靠性的应用程序提供了控制数据落盘时机的能力。
  • 灵活性:提供了从系统级到文件级的、不同强度和性能开销的多种同步选项。
  • 解耦:将应用程序的同步需求与底层文件系统和硬件的具体实现解耦。

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

  • 巨大的性能影响:同步调用,特别是fsync,是性能杀手。因为它会强制将异步的、高效的内存操作,转变为同步的、缓慢的磁盘操作,导致应用程序阻塞。
  • I/O风暴sync()调用会一次性提交大量I/O请求,可能导致系统I/O负载瞬间飙升,影响其他正在运行的应用。
  • 硬件谎言fsync的安全性保证依赖于底层存储设备(磁盘、SSD)及其缓存的诚实。一些消费级硬盘可能会向操作系统报告写入已完成,但实际上数据只写入了其易失性的DRAM缓存中。在这种情况下,即使调用了fsync,断电依然可能导致数据丢失。

使用场景

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

  • 数据库事务日志:在数据库确认一个事务(COMMIT)之前,必须调用fsyncfdatasync将该事务的日志记录持久化。这是保证数据库ACID特性中“D”(Durability,持久性)的关键。
  • 关键文件保存:当文本编辑器保存文件时,典型的“安全保存”流程是:写入新文件 -> fsync新文件 -> rename新文件覆盖旧文件 -> fsync父目录(以持久化rename操作)。
  • 系统状态变更:在安装软件、更新系统配置或准备关机时,调用sync来确保所有变更都已写入磁盘。

是否有不推荐使用该技术的场景?为什么?

  • 高性能流式写入:对于日志收集、视频录制等需要持续高速写入的场景,频繁调用fsync会严重扼杀性能。更好的策略是让内核的后台回写机制去处理,或者只在关键的检查点(例如每隔几秒或几MB数据)调用一次fsync
  • 非关键的临时文件:对于临时文件或缓存文件,程序崩溃导致其丢失是可以接受的,因此完全没有必要对其进行同步操作。

对比分析

请将其 与 其他相似技术 进行详细对比。

同步调用与O_DIRECTO_SYNC等文件打开标志在目标上相似(控制数据持久性),但实现机制完全不同。

特性 sync(), fsync(), fdatasync() open() with O_SYNC / O_DSYNC open() with O_DIRECT
核心机制 按需冲刷 (On-demand Flushing)。应用程序决定何时将页缓存中的内容冲刷到磁盘。 同步写入 (Synchronous Writes)每一次write()系统调用都会像fsync/fdatasync一样阻塞,直到数据和元数据落盘。 绕过缓存 (Cache Bypass)。应用程序的write()read()直接在用户空间缓冲区和存储设备间传输数据,完全绕过内核页缓存。
操作粒度 sync是系统级,fsync/fdatasync是文件级,在需要时调用。 文件级,但作用于每一次I/O操作。 文件级,作用于每一次I/O操作。
性能影响 sync影响全局。fsync在调用时产生一次性的、较大的性能开销。 持续性的、巨大的性能开销。几乎每次写入都会阻塞。 性能影响复杂。对于大型、对齐的I/O可以获得高吞吐和低CPU占用;对于小型、不对齐的I/O则性能极差。
与页缓存关系 利用并管理页缓存。 利用并强制同步页缓存。 完全绕过页缓存。
典型用途 数据库、文件编辑器等需要在特定检查点保证持久性的应用。 极少数需要极高实时性和简单性的数据记录设备,但性能极差,非常罕见。 需要自己实现缓存和I/O调度的超高性能应用,如大型数据库(Oracle)。

fs/sync.c

sync_inodes_one_sb: 同步单个文件系统的 Inode 元数据

此函数是一个回调函数, 其唯一的职责是触发对一个指定文件系统的所有”脏” Inode 的写回操作。Inode (索引节点) 存储了文件的元数据, 如权限、所有者、大小、时间戳以及指向实际数据块的指针。当这些元数据在内存中被修改后, Inode 就被标记为”脏”(dirty), 意味着它需要被写回到持久化存储设备上。此函数就是启动这个写回过程的命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* 静态函数声明: sync_inodes_one_sb
* 这是一个回调函数, 其函数签名与 iterate_supers 所需的回调函数类型相匹配.
* @sb: 指向 struct super_block 的指针, 代表由 iterate_supers 传入的当前正在处理的文件系统.
* @arg: 一个 void 指针, 它是从 iterate_supers 传递过来的通用参数. 在此函数中并未使用.
*/
static void sync_inodes_one_sb(struct super_block *sb, void *arg)
{
/*
* 检查文件系统是否为只读.
* sb_rdonly(sb) 是一个宏, 用于检查超级块的 s_flags 成员是否设置了 SB_RDONLY 标志.
* 对于一个以只读方式挂载的文件系统 (例如, 来自CD-ROM的iso9660或只读挂载的ext4),
* 其内存中的任何 Inode 都不可能变为"脏".
* 因此, 尝试对其进行同步是没有意义且不安全的. 这个检查可以避免无效操作并提高效率.
*/
if (!sb_rdonly(sb))
/*
* 如果文件系统是可写的, 则调用 VFS (虚拟文件系统) 层的核心函数 sync_inodes_sb(sb).
* 这个函数会遍历指定超级块(sb)内部维护的"脏"Inode列表,
* 并将这些 Inode 提交给底层的块设备I/O层进行写回(writeback).
* 这个过程通常是异步的, 即函数调用本身会很快返回, 而实际的I/O操作会在后台进行.
*/
sync_inodes_sb(sb);
}

sync_fs_one_sb: 同步单个文件系统的特定元数据

此函数也是一个回调函数, 但它处理的是比 Inode 更高层次或更特殊的文件系统级同步。它的核心作用是为具体的文件系统类型(如ext4, btrfs, xfs等)提供一个钩子(hook), 以执行它们特有的同步操作。这通常涉及到文件系统日志(Journal)、超级块(Superblock)自身的更新, 或其他特定于该文件系统实现的元数据结构。

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
/*
* 静态函数声明: sync_fs_one_sb
* 同样, 这是一个为 iterate_supers 设计的回调函数.
* @sb: 指向当前正在处理的文件系统的超级块.
* @arg: 一个 void 指针. 在这里, 它被假定指向一个整型, 通常用于传递同步标志 (例如, 是否等待I/O完成).
*/
static void sync_fs_one_sb(struct super_block *sb, void *arg)
{
/*
* 这是一个复合条件检查, 只有所有条件都满足时, 才会执行同步操作.
*
* 条件1: !sb_rdonly(sb)
* 与 sync_inodes_one_sb 中的检查原因相同. 只读文件系统不需要也无法进行文件系统级别的同步.
*
* 条件2: !(sb->s_iflags & SB_I_SKIP_SYNC)
* 检查一个内部标志 SB_I_SKIP_SYNC. VFS 或文件系统自身在某些特殊情况下
* (例如, 文件系统正在被强制卸载或处于某种错误状态)可能会设置此标志,
* 以指示应跳过本次同步操作.
*
* 条件3: sb->s_op->sync_fs
* 这是最关键的检查. sb->s_op 是一个指向 super_operations 结构体的指针,
* 这个结构体包含了由具体文件系统驱动实现的一系列回调函数.
* 此条件检查该文件系统是否实现了 sync_fs 这个可选的回调函数.
* 对于一些简单的文件系统(如vfat), 它们可能没有特殊的文件系统级同步需求,
* 此时这个函数指针就会是 NULL. 这个检查可以防止内核调用一个空指针.
*/
if (!sb_rdonly(sb) && !(sb->s_iflags & SB_I_SKIP_SYNC) &&
sb->s_op->sync_fs)
/*
* 如果所有检查都通过, 就调用该文件系统自己的 sb->s_op->sync_fs 函数.
* - 第一个参数 sb 告诉函数要同步哪个文件系统实例.
* - 第二个参数 *(int *)arg 将通用指针 arg 强制转换为整型指针, 然后解引用.
* 这通常用于传递一个布尔值, 指示本次同步是否需要等待I/O操作完成 (sync vs async).
* 例如, 对于ext4, 这个函数会触发一次日志提交(journal commit), 确保所有最近的操作都被永久记录.
*/
sb->s_op->sync_fs(sb, *(int *)arg);
}

iterate_supers 及其辅助函数: 安全地遍历所有文件系统

这一组函数共同构成了一个健壮的机制, 用于遍历当前Linux系统中所有已挂载的文件系统, 并对每一个文件系统的超级块(super_block)执行一个指定的操作。这是内核中许多全局文件系统操作(例如 sync 同步)的基础。

其核心原理是通过一个精心设计的”锁-增引用-解锁-操作-重锁”循环, 来解决在遍历全局链表时可能遇到的各种并发问题, 即使在单核抢占式系统(如运行在STM32H750上的Linux)中, 这些问题也同样存在。


iterate_supers: 公共遍历接口

这是一个暴露给内核其他部分使用的公共API。它提供了一个最简单、最常用的遍历方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* iterate_supers: 遍历所有超级块.
* @f: 一个函数指针, 代表将要对每个超级块执行的操作.
* @arg: 一个 void 指针, 将作为第二个参数传递给函数 f.
*
* 这是对 __iterate_supers 的一个简单封装.
*/
void iterate_supers(void (*f)(struct super_block *, void *), void *arg)
{
/*
* 调用核心的遍历函数 __iterate_supers, 并传入默认标志 0.
* 标志 0 意味着:
* - 正向遍历.
* - 在调用 f 之前, 会为每个超级块获取其内部锁.
*/
__iterate_supers(f, arg, 0);
}

__iterate_supers: 核心遍历引擎

这个函数是所有遍历逻辑的核心, 它实现了安全遍历的关键机制。

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
/*
* __iterate_supers: 遍历所有超级块的核心实现.
* @f: 要执行的操作函数.
* @arg: 传递给 f 的参数.
* @flags: 控制遍历行为的标志 (如 SUPER_ITER_REVERSE, SUPER_ITER_UNLOCKED).
*/
static void __iterate_supers(void (*f)(struct super_block *, void *), void *arg,
enum super_iter_flags_t flags)
{
/*
* sb: 当前正在处理的超级块.
* p: 指向上一个已处理完毕的超级块, 用于延迟释放其引用.
*/
struct super_block *sb, *p = NULL;
/*
* excl: 一个布尔值, 判断是否需要获取超级块的排他锁.
*/
bool excl = flags & SUPER_ITER_EXCL;

/*
* guard(spinlock)(&sb_lock): 这是一个现代C的宏, 类似于C++的RAII.
* 它会在进入代码块时获取全局的 sb_lock 自旋锁, 并在退出代码块时自动释放.
* sb_lock 保护的是 super_blocks 这个全局链表本身, 防止在遍历时有其他任务正在挂载或卸载文件系统.
* 在单核系统上, 获取自旋锁会禁用内核抢占, 保证了链表遍历的原子性.
*/
guard(spinlock)(&sb_lock);

/*
* 开始 for 循环, 遍历 super_blocks 链表.
* - first_super(flags): 获取链表的第一个元素 (或最后一个, 如果是反向遍历).
* - !list_entry_is_head(...): 检查当前元素是否是链表头, 这是循环的终止条件.
* - next_super(sb, flags): 获取下一个元素 (或上一个).
*/
for (sb = first_super(flags);
!list_entry_is_head(sb, &super_blocks, s_list);
sb = next_super(sb, flags)) {
/*
* 如果超级块被标记为 SB_DYING, 说明它正在被卸载.
* 此时不应再对它进行任何操作, 直接跳过.
*/
if (super_flags(sb, SB_DYING))
continue;
/*
* *** 核心安全机制 1: 增加引用计数 ***
* 增加 sb 的引用计数 (s_count).
* 这保证了即使我们稍后释放了全局的 sb_lock, 这个 sb 结构体也不会被内核释放掉,
* 因为我们持有它的一个引用.
*/
sb->s_count++;
/*
* *** 核心并发策略: 临时释放全局锁 ***
* 释放保护全局链表的 sb_lock 锁.
* 这样做的目的是避免在执行一个可能耗时很长(甚至可能休眠)的操作 f 时,
* 长时间地阻塞系统中所有其他挂载/卸载文件系统的操作, 从而提高系统并发性.
*/
spin_unlock(&sb_lock);

/*
* 根据标志位, 决定如何调用操作函数 f.
*/
if (flags & SUPER_ITER_UNLOCKED) {
/*
* 如果设置了 SUPER_ITER_UNLOCKED, 则直接调用 f, 不获取超级块的内部锁.
*/
f(sb, arg);
} else if (super_lock(sb, excl)) {
/*
* 否则, 获取超级块的内部锁 sb->s_lock. 这个锁保护的是该文件系统自身的内部数据.
* 这确保了函数 f 在操作文件系统内部数据时是线程安全的.
*/
f(sb, arg);
/*
* 操作完成后, 释放超级块的内部锁.
*/
super_unlock(sb, excl);
}

/*
* *** 核心并发策略: 重新获取全局锁 ***
* 重新获取 sb_lock, 准备进行下一次循环迭代, 即安全地访问 super_blocks 链表.
*/
spin_lock(&sb_lock);
/*
* *** 核心安全机制 2: 延迟释放引用计数 ***
* 如果 p 不为 NULL (即这不是第一次循环),
* 调用 __put_super(p) 释放上一个超级块的引用计数.
* 此时释放是安全的, 因为我们正持有 sb_lock, 不会有竞争.
*/
if (p)
__put_super(p);
/*
* 将当前处理完的 sb 保存到 p 中, 它的引用将在下一次循环开始时被释放.
*/
p = sb;
}
/*
* 循环结束后, 释放最后一个被处理的超级块的引用计数.
*/
if (p)
__put_super(p);
} // guard(spinlock) 在此自动释放 sb_lock```

---

### `first_super` & `next_super`: 链表遍历辅助函数

这两个是简单的内联函数, 它们根据遍历方向标志, 抽象了获取链表头和下一个节点的具体操作。

```c
/*
* first_super: 获取 super_blocks 链表的第一个条目.
* @flags: 遍历标志.
*/
static inline struct super_block *first_super(enum super_iter_flags_t flags)
{
/*
* 检查是否设置了反向遍历标志.
*/
if (flags & SUPER_ITER_REVERSE)
/*
* 如果是, 返回链表的最后一个条目.
*/
return list_last_entry(&super_blocks, struct super_block, s_list);
/*
* 否则, 返回链表的第一个条目.
*/
return list_first_entry(&super_blocks, struct super_block, s_list);
}

/*
* next_super: 获取给定超级块 sb 在链表中的下一个条目.
* @sb: 当前的超级块.
* @flags: 遍历标志.
*/
static inline struct super_block *next_super(struct super_block *sb,
enum super_iter_flags_t flags)
{
/*
* 检查是否设置了反向遍历标志.
*/
if (flags & SUPER_ITER_REVERSE)
/*
* 如果是, 返回 sb 的前一个条目.
*/
return list_prev_entry(sb, s_list);
/*
* 否则, 返回 sb 的后一个条目.
*/
return list_next_entry(sb, s_list);
}

emergency_syncdo_sync_work: 执行紧急文件系统同步

这两个函数协同工作, 以便在紧急情况下(例如, 内核即将崩溃panic或系统即将断电)触发一次尽力而为(best-effort)的文件系统同步。其核心原理是使用内核的工作队列(workqueue)机制, 安全地将一个可能耗时很长且需要休眠的同步任务, 从一个可能不允许休眠的紧急上下文中, 推迟(defer)到一个安全的普通进程上下文中去执行


emergency_sync: 紧急同步的发起者

此函数本身不执行任何同步操作。它唯一的职责是创建并调度一个同步任务。这使得它可以被安全地从任何地方调用, 包括中断处理程序或持有自旋锁的代码——这些上下文都严禁休眠。

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
/*
* emergency_sync: 发起一次紧急文件系统同步.
* 这个函数被设计为可以从任何上下文安全调用, 包括原子上下文.
*/
void emergency_sync(void)
{
/*
* 定义一个指向 work_struct 的指针 work.
* work_struct 是内核工作队列机制的基本单元.
*/
struct work_struct *work;

/*
* 调用 kmalloc 动态分配一个 work_struct 结构体所需的内存.
*
* 关键点: 使用了 GFP_ATOMIC 标志.
* - GFP_ATOMIC 告诉内存分配器, 本次分配绝对不能休眠等待内存.
* - 它会尝试从内核的紧急内存池中分配, 即使在内存极度紧张的情况下也有更高的成功率.
* - 这使得 emergency_sync 可以在中断处理程序等原子上下文中被安全调用, 因为这些上下文不允许休眠.
*/
work = kmalloc(sizeof(*work), GFP_ATOMIC);
/*
* 检查内存分配是否成功.
*/
if (work) {
/*
* 如果分配成功, 调用 INIT_WORK 宏来初始化这个工作项.
* - 第一个参数 work 是要初始化的工作项.
* - 第二个参数 do_sync_work 是一个函数指针, 它指定了当这个工作项被执行时, 应该调用哪个函数.
*/
INIT_WORK(work, do_sync_work);
/*
* 调用 schedule_work, 将初始化好的工作项提交到系统全局的工作队列中.
* 内核的一个"工作者线程"(worker thread)会在稍后的某个时间点从队列中取出这个工作项并执行它.
* 这一步完成了任务的"推迟", 将实际工作交给了另一个线程.
*/
schedule_work(work);
}
}

do_sync_work: 紧急同步的执行者

这个函数包含了实际的同步逻辑。它由内核的工作者线程在一个允许休眠的普通进程上下文中执行。

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
/*
* do_sync_work: 工作队列函数, 执行实际的同步操作.
* @work: 指向被执行的 work_struct 的指针.
*/
static void do_sync_work(struct work_struct *work)
{
/*
* 定义一个整型变量 nowait, 并初始化为0.
* 它的指针会被传递给同步函数, 以影响它们的行为(尽管在这里它未被用作"不等待"的标志).
*/
int nowait = 0;

/*
* 关键点: 执行两次完整的同步流程.
* 注释解释了原因: "Sync twice to reduce the possibility we skipped some inodes / pages
* because they were temporarily locked".
* 第一次同步时, 某些文件或元数据可能因为被其他进程锁定而无法被写入磁盘.
* 通过立即进行第二次同步, 可以极大地增加这些之前被锁定的项目现在已经解锁并能被成功写入的机会.
*
* --- 第一轮同步 ---
*/
/*
* 遍历所有已挂载的文件系统 (iterate_supers).
* 对每个文件系统, 调用 sync_inodes_one_sb, 将所有脏的(被修改过的) inode 元数据写入磁盘.
* Inode 包含了文件的权限, 时间戳, 大小等信息.
*/
iterate_supers(sync_inodes_one_sb, &nowait);
/*
* 再次遍历所有文件系统.
* 对每个文件系统, 调用 sync_fs_one_sb, 将文件系统自身的元数据(如超级块)写入磁盘.
*/
iterate_supers(sync_fs_one_sb, &nowait);
/*
* 调用 sync_bdevs(false), 将所有块设备中缓存的脏数据块(文件内容)写入磁盘.
* 参数 false 表示同步是异步发起的, 但函数通常会等待其完成.
*/
sync_bdevs(false);

/*
* --- 第二轮同步 (重复上述流程) ---
*/
iterate_supers(sync_inodes_one_sb, &nowait);
iterate_supers(sync_fs_one_sb, &nowait);
sync_bdevs(false);

/*
* 向内核日志打印一条消息, 表明紧急同步已完成.
*/
printk("Emergency Sync complete\n");

/*
* 释放 work_struct 结构体占用的内存.
* 因为这个结构体是在 emergency_sync 中通过 kmalloc 动态分配的,
* 所以在工作完成后必须由执行者(本函数)负责释放, 以防止内存泄漏.
*/
kfree(work);
}