[toc]
fs/sync.c 数据同步(Data Synchronization) VFS层缓存回写的核心控制
历史与背景
这项技术是为了解决什么特定问题而诞生的?
fs/sync.c
及其实现的一系列系统调用(sync
, fsync
, fdatasync
)是为了解决操作系统设计中一个最根本的矛盾:性能与数据持久性(Durability)之间的权衡。
- 性能需求:物理磁盘(无论是机械硬盘还是SSD)的读写速度远低于内存(RAM)。为了提高I/O性能,Linux内核引入了页缓存(Page Cache)。当应用程序写入文件时,数据通常只是被写入到内存中的页缓存,并被标记为“脏”(Dirty),然后系统调用会立即返回成功。这种异步写入或**写回缓存(Write-back Cache)**机制极大地提高了写入操作的响应速度。
- 数据持久性需求:仅将数据写入内存是不可靠的。如果此时系统突然断电或崩溃,所有存在于页缓存中但尚未写入磁盘的“脏”数据都将永久丢失。对于数据库、文件编辑器、事务日志等关键应用来说,这会导致数据损坏或丢失,是不可接受的。
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
命令,以确保所有缓存数据都被清空,防止文件系统损坏。
- 所有数据库系统(PostgreSQL, MySQL/InnoDB等):在提交事务时,必须调用
核心原理与设计
它的核心工作原理是什么?
fs/sync.c
是VFS层的一个协调者,它将上层的系统调用请求转换为对底层文件系统和块设备的回写操作。
- 标记“脏”状态:当用户通过
write()
修改文件时,VFS和页缓存会将对应的内存页(struct page
)标记为Dirty
,同时也会将该文件对应的inode
标记为脏。 sync()
的工作流程:- 调用
sync()
系统调用时,内核会触发一个系统范围的回写。 - 它会遍历所有已挂载的文件系统(超级块列表),并对每个文件系统的脏inode列表、脏数据页等发起回写。
- 这是一个异步发起的过程,
sync()
调用本身会很快返回,而回写操作则在后台进行。
- 调用
fsync(fd)
的工作流程:- 调用
fsync(fd)
时,内核会从文件描述符fd
找到对应的struct file
对象,再找到struct inode
。 - 它会调用具体文件系统实现的
file_operations->fsync
函数。 - 这个函数负责两件事:
- 将该
inode
关联的所有脏数据页提交给块设备层进行写入。 - 将该
inode
自身的脏元数据也提交写入。
- 将该
- 最关键的是,这个函数必须阻塞等待,直到它确认相关的I/O请求已经被物理设备报告完成。
- 调用
fdatasync(fd)
的工作流程:- 流程与
fsync
非常相似,但它调用的文件系统回调函数在处理元数据时会进行区分。它只会回写那些影响数据访问的元数据(主要是文件大小),而可能会跳过那些不影响的元数据(如时间戳)。
- 流程与
它的主要优势体现在哪些方面?
- 提供数据持久性保证:为需要高可靠性的应用程序提供了控制数据落盘时机的能力。
- 灵活性:提供了从系统级到文件级的、不同强度和性能开销的多种同步选项。
- 解耦:将应用程序的同步需求与底层文件系统和硬件的具体实现解耦。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 巨大的性能影响:同步调用,特别是
fsync
,是性能杀手。因为它会强制将异步的、高效的内存操作,转变为同步的、缓慢的磁盘操作,导致应用程序阻塞。 - I/O风暴:
sync()
调用会一次性提交大量I/O请求,可能导致系统I/O负载瞬间飙升,影响其他正在运行的应用。 - 硬件谎言:
fsync
的安全性保证依赖于底层存储设备(磁盘、SSD)及其缓存的诚实。一些消费级硬盘可能会向操作系统报告写入已完成,但实际上数据只写入了其易失性的DRAM缓存中。在这种情况下,即使调用了fsync
,断电依然可能导致数据丢失。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?
- 数据库事务日志:在数据库确认一个事务(COMMIT)之前,必须调用
fsync
或fdatasync
将该事务的日志记录持久化。这是保证数据库ACID特性中“D”(Durability,持久性)的关键。 - 关键文件保存:当文本编辑器保存文件时,典型的“安全保存”流程是:写入新文件 ->
fsync
新文件 ->rename
新文件覆盖旧文件 ->fsync
父目录(以持久化rename
操作)。 - 系统状态变更:在安装软件、更新系统配置或准备关机时,调用
sync
来确保所有变更都已写入磁盘。
是否有不推荐使用该技术的场景?为什么?
- 高性能流式写入:对于日志收集、视频录制等需要持续高速写入的场景,频繁调用
fsync
会严重扼杀性能。更好的策略是让内核的后台回写机制去处理,或者只在关键的检查点(例如每隔几秒或几MB数据)调用一次fsync
。 - 非关键的临时文件:对于临时文件或缓存文件,程序崩溃导致其丢失是可以接受的,因此完全没有必要对其进行同步操作。
对比分析
请将其 与 其他相似技术 进行详细对比。
同步调用与O_DIRECT
和O_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 | /* |
sync_fs_one_sb
: 同步单个文件系统的特定元数据
此函数也是一个回调函数, 但它处理的是比 Inode 更高层次或更特殊的文件系统级同步。它的核心作用是为具体的文件系统类型(如ext4, btrfs, xfs等)提供一个钩子(hook), 以执行它们特有的同步操作。这通常涉及到文件系统日志(Journal)、超级块(Superblock)自身的更新, 或其他特定于该文件系统实现的元数据结构。
1 | /* |
iterate_supers
及其辅助函数: 安全地遍历所有文件系统
这一组函数共同构成了一个健壮的机制, 用于遍历当前Linux系统中所有已挂载的文件系统, 并对每一个文件系统的超级块(super_block
)执行一个指定的操作。这是内核中许多全局文件系统操作(例如 sync
同步)的基础。
其核心原理是通过一个精心设计的”锁-增引用-解锁-操作-重锁”循环, 来解决在遍历全局链表时可能遇到的各种并发问题, 即使在单核抢占式系统(如运行在STM32H750上的Linux)中, 这些问题也同样存在。
iterate_supers
: 公共遍历接口
这是一个暴露给内核其他部分使用的公共API。它提供了一个最简单、最常用的遍历方式。
1 | /* |
__iterate_supers
: 核心遍历引擎
这个函数是所有遍历逻辑的核心, 它实现了安全遍历的关键机制。
1 | /* |
emergency_sync
与 do_sync_work
: 执行紧急文件系统同步
这两个函数协同工作, 以便在紧急情况下(例如, 内核即将崩溃panic
或系统即将断电)触发一次尽力而为(best-effort)的文件系统同步。其核心原理是使用内核的工作队列(workqueue
)机制, 安全地将一个可能耗时很长且需要休眠的同步任务, 从一个可能不允许休眠的紧急上下文中, 推迟(defer)到一个安全的普通进程上下文中去执行。
emergency_sync
: 紧急同步的发起者
此函数本身不执行任何同步操作。它唯一的职责是创建并调度一个同步任务。这使得它可以被安全地从任何地方调用, 包括中断处理程序或持有自旋锁的代码——这些上下文都严禁休眠。
1 | /* |
do_sync_work
: 紧急同步的执行者
这个函数包含了实际的同步逻辑。它由内核的工作者线程在一个允许休眠的普通进程上下文中执行。
1 | /* |