Linux内核架构浅谈87 - Linux文件数据同步:sync、fsync与fdatasync的区别
1. 为什么需要文件数据同步?
在Linux系统中,为了提升I/O性能,内核会使用页缓存(Page Cache)和块缓存(Block Cache)暂存文件数据。当应用程序写入文件时,数据并不会直接写入磁盘,而是先存入缓存;只有当缓存满、触发特定时间阈值,或显式调用同步接口时,内核才会将缓存中的数据刷写到磁盘(这个过程称为“回写”)。
这种“延迟写入”机制虽然提升了性能,但也带来了数据丢失风险——如果系统突然断电或崩溃,缓存中未刷写的数据会永久丢失。因此,Linux提供了三个核心的文件同步接口:sync、fsync和fdatasync,用于在需要保证数据持久性时,主动触发缓存数据的刷写。
内核底层支撑:根据内核文档,页缓存和块缓存的回写机制由pdflush线程(或 newer 内核的 flusher 线程)管理,这些线程会周期性地将“脏页”(已修改但未刷写的页)同步到磁盘。而同步接口的核心作用,就是强制触发这些回写操作,或等待回写完成。

2. 三个同步接口的核心区别
虽然sync、fsync和fdatasync都用于数据同步,但它们的同步范围、粒度和性能开销差异显著。下面从原理、用法和场景三个维度展开分析。
2.1 sync:全局粗粒度同步
sync是最基础的同步接口,其核心特点是“全局触发、不等待”——它会遍历系统中所有挂载的文件系统,标记所有“脏页”为“待回写”状态,然后立即返回,不会等待回写操作完成。
核心原理
内核中,sync的实现依赖于sync_filesystems()函数,该函数会遍历所有注册的文件系统,调用每个文件系统的sync_fs()方法,将文件系统的脏页和脏元数据(如inode属性、目录结构)标记为“需要回写”。之后,pdflush线程会异步处理这些回写请求。
代码示例
// 头文件
#include <unistd.h>
int main() {
// 触发全局同步,不等待回写完成
sync();
printf("已触发全局数据同步(不等待完成)\n");
return 0;
}
适用场景
sync适用于对数据持久性要求不严格、仅需“尽力触发同步”的场景,例如:
- 系统关机前的预处理(实际关机流程中会有更严格的同步逻辑);
- 定期备份脚本中,作为“轻量级触发”,避免大量I/O阻塞主线程;
- 对数据丢失容忍度较高的日志打印(如调试日志)。
注意:由于sync不等待回写完成,调用后立即断电仍可能导致数据丢失。例如,调用sync()后1秒内断电,此时部分脏页可能仍未刷写到磁盘。
2.2 fsync:文件级全量同步
fsync是文件级的同步接口,它需要传入一个文件描述符(fd),仅同步该文件对应的“脏页”和“脏元数据”,并且会等待所有回写操作完成后才返回。
核心原理
与sync的“全局触发”不同,fsync的实现会聚焦于单个文件:
- 通过文件描述符
fd找到对应的struct file和struct address_space(内核中描述文件地址空间的结构); - 调用
filemap_write_and_wait()函数,等待该文件的所有脏页回写完成; - 调用文件系统的
fsync_inode()方法,同步该文件的元数据(如文件大小、修改时间、权限等); - 等待元数据回写完成后,返回调用者。
代码示例
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
int fd = open("data.txt", O_WRONLY | O_CREAT, 0644);
if (fd < 0) {
perror("open failed");
return -1;
}
// 写入数据(此时数据仅在缓存)
const char* data = "需要持久化的数据";
write(fd, data, strlen(data));
// 同步该文件的数据和元数据,等待完成
if (fsync(fd) == 0) {
printf("data.txt 的数据和元数据已同步到磁盘\n");
} else {
perror("fsync failed");
}
close(fd);
return 0;
}
适用场景
fsync适用于对单个文件的“数据+元数据”持久性要求严格的场景,例如:
- 数据库事务提交(如MySQL的
innodb_flush_log_at_trx_commit=1配置,会调用fsync确保日志持久化); - 文件编辑器保存文件(如Vim的
:w命令,会隐式调用fsync); - 配置文件写入后,需要确保修改立即生效且不丢失。
2.3 fdatasync:文件级增量同步(仅数据+必要元数据)
fdatasync是fsync的“性能优化版”——它同样针对单个文件、等待同步完成,但仅同步文件数据和“影响数据访问的元数据”,跳过“不影响数据访问的元数据”(如文件修改时间mtime、访问时间atime)。
核心原理
内核中,fdatasync的实现与fsync的区别在于“元数据同步范围”:
- 必须同步的元数据:文件大小(
i_size)、数据块映射(inode中的块指针)——这些元数据若不同步,即使数据已刷写,应用也无法正确访问数据; - 可跳过的元数据:修改时间(
mtime)、访问时间(atime)、权限(mode)——这些元数据不影响数据的读取,可延迟同步。
因此,fdatasync的性能开销通常低于fsync,尤其在频繁修改文件数据但元数据变化较少的场景。
代码示例
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main() {
int fd = open("log.txt", O_WRONLY | O_APPEND | O_CREAT, 0644);
if (fd < 0) {
perror("open failed");
return -1;
}
// 追加日志(仅数据变化,元数据中mtime会变,但atime可能不变)
const char* log = "2025-10-09: 系统正常运行\n";
write(fd, log, strlen(log));
// 同步数据和必要元数据(如文件大小),跳过mtime等非必要元数据
if (fdatasync(fd) == 0) {
printf("log.txt 的数据和必要元数据已同步到磁盘\n");
} else {
perror("fdatasync failed");
}
close(fd);
return 0;
}
适用场景
fdatasync适用于“数据必须持久化,但元数据可延迟”的高频写入场景,例如:
- 应用日志写入(如Web服务器的访问日志,仅需确保日志内容不丢失,修改时间延迟同步不影响);
- 数据流处理(如实时计算框架的中间结果写入,仅需数据持久化,元数据可后续同步);
- 数据库的WAL(Write-Ahead Log)写入(部分数据库会用
fdatasync平衡性能和持久性)。
3. 三个接口的关键参数对比
为了更清晰地理解三者的差异,下面通过表格总结它们的同步范围、等待行为、性能开销和适用场景:
| 接口 | 同步范围 | 是否等待回写完成 | 元数据同步范围 | 性能开销 | 典型场景 |
|---|---|---|---|---|---|
sync | 全局所有文件系统 | 否(异步触发) | 所有元数据 | 低(仅触发,不等待) | 系统关机预处理、轻量级备份 |
fsync(fd) | 单个文件(由fd指定) | 是(同步等待) | 所有元数据(mtime、size等) | 高(等待数据+全量元数据) | 数据库事务、文件编辑器保存 |
fdatasync(fd) | 单个文件(由fd指定) | 是(同步等待) | 仅必要元数据(size、块映射) | 中(等待数据+增量元数据) | 日志写入、数据流处理 |
4. 实战建议:如何选择同步接口?
在实际开发中,选择哪个同步接口,核心取决于“数据持久性要求”和“性能开销容忍度”。以下是具体的决策建议:

4.1 优先用 fdatasync,而非 fsync
如果应用仅需保证“数据不丢失”,而元数据(如修改时间)的同步延迟可接受,优先使用fdatasync。例如,日志写入场景中,即使mtime延迟1秒同步,也不会影响日志的后续读取和分析,但fdatasync比fsync减少了元数据回写的I/O开销。
4.2 避免频繁调用 sync
sync的全局触发特性会导致大量I/O压力,频繁调用(如每秒一次)会严重影响系统整体性能。如果需要定期同步,建议针对具体文件使用fdatasync,而非全局sync。
4.3 结合内核参数优化性能
Linux内核提供了多个参数,可调整页缓存的回写策略,间接优化同步接口的性能:
vm.dirty_ratio:当脏页占总内存的比例超过该值时,pdflush会主动回写(默认20%);vm.dirty_expire_centisecs:脏页的过期时间(默认3000厘秒,即30秒),超过该时间的脏页会被强制回写;vm.dirty_writeback_centisecs:pdflush线程的唤醒间隔(默认500厘秒,即5秒)。
例如,对于高频写入场景,可适当降低vm.dirty_ratio(如10%),减少单次回写的数据量,从而降低fdatasync的等待时间。
4.4 注意“伪同步”陷阱
部分文件系统(如Ext3/Ext4的data=writeback模式)或磁盘控制器(开启了写缓存)可能会“欺骗”同步接口——即使fsync返回,数据也可能仍在控制器缓存中,未真正写入磁盘。此时需要:
- 文件系统挂载时添加
data=ordered或data=journal选项(Ext3/Ext4); - 禁用磁盘写缓存(
hdparm -W 0 /dev/sda),或启用“缓存刷新”功能(需硬件支持)。
5. 总结
Linux的sync、fsync和fdatasync接口,分别对应“全局粗粒度”“文件全量”和“文件增量”三种同步策略。选择时需把握两个核心维度:
- 持久性要求:是否需要等待同步完成?是否需要同步元数据?
- 性能开销:全局同步 vs 单个文件同步?全量元数据 vs 增量元数据?
最终,没有“最好”的接口,只有“最适合”的场景——理解内核的缓存回写机制,结合应用的实际需求,才能做出最优选择。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)