好的,我们来深入学习 sync_file_range
系统调用
1. 函数介绍
在 Linux 系统中,为了提高性能,当你调用 write()
将数据写入文件时,这些数据通常不会立即写入到物理磁盘上。相反,它们会被暂时存储在内核的页缓存 (Page Cache) 中。操作系统会在稍后的某个时间点(或者当缓存满时)自动将这些数据刷新到磁盘。这种机制大大提高了写入速度,但也带来了一个问题:如果在数据被实际写入磁盘之前系统崩溃或断电,那么这部分数据就会丢失。
fsync()
和 fdatasync()
函数可以强制将文件的所有(或部分)修改数据从内核缓存刷新到磁盘,确保数据的持久化。但是,它们通常会刷新整个文件相关的数据和元数据,这可能比你需要的更多,导致不必要的性能开销。
sync_file_range
(Sync File Range) 系统调用提供了更精细的控制。它允许你指定文件的一个特定字节范围,并决定对这个范围内的数据执行什么样的同步操作。你可以选择:
- 将数据从缓存写入磁盘(但不等待完成)。
- 等待数据写入磁盘完成。
- 将缓存中的脏数据(已修改但未写回)回写到磁盘。
简单来说,sync_file_range
就是让你用程序精确地告诉操作系统:“请帮我确保文件的第 X 字节到第 Y 字节的数据已经安全地保存到硬盘上了”,并且你可以控制这个过程是异步还是同步。
典型应用场景:
- 数据库系统:数据库需要精确控制其事务日志和数据文件的刷新,以确保事务的原子性和持久性,同时最大化性能。它们可以只刷新刚刚写入的关键日志部分,而不是整个文件。
- 大文件处理:在写入一个非常大的文件时,可以分段调用
sync_file_range
,确保关键部分的数据已经落盘,而不需要等待整个大文件都写完。 - 高性能应用:需要在数据安全性和写入性能之间取得平衡的应用。
2. 函数原型
#define _GNU_SOURCE // 需要定义这个宏才能使用 sync_file_range
#include <fcntl.h> // 包含系统调用声明和标志
int sync_file_range(int fd, off64_t offset, off64_t nbytes, unsigned int flags);
注意:函数名和参数类型可能因系统架构(32/64位)和内核版本而略有不同(如 sync_file_range2
),但 glibc 会处理好兼容性,使用 sync_file_range
即可。
3. 功能
对文件描述符 fd
对应的文件,在从 offset
开始的 nbytes
字节范围内,根据 flags
指定的操作,执行同步操作。
4. 参数
fd
:int
类型。- 一个已打开文件的有效文件描述符。
offset
:off64_t
类型。- 指定要同步的文件范围的起始字节偏移量。必须是非负数。
nbytes
:off64_t
类型。- 指定要同步的字节数。如果
nbytes
为 0,则表示同步从offset
开始到文件末尾的所有数据。
flags
:unsigned int
类型。- 指定要执行的同步操作。可以是以下一个或多个标志的按位或 (
|
) 组合:SYNC_FILE_RANGE_WAIT_BEFORE
: 等待在offset
和nbytes
范围内之前发起的任何写入操作完成。SYNC_FILE_RANGE_WRITE
: 启动将指定范围内的脏页面(修改过的缓存数据)回写到磁盘的操作。这个操作通常是异步的,即函数可能在数据实际写入磁盘之前就返回。SYNC_FILE_RANGE_WAIT_AFTER
: 等待在offset
和nbytes
范围内由SYNC_FILE_RANGE_WRITE
启动的回写操作完成。
常见的标志组合:
SYNC_FILE_RANGE_WRITE
: 启动回写,但不等待。适用于“尽快开始保存数据,但我不等它完成”。SYNC_FILE_RANGE_WRITE | SYNC_FILE_RANGE_WAIT_AFTER
: 启动回写并等待其完成。这是最常用的方式,确保指定范围的数据确实写入了磁盘。SYNC_FILE_RANGE_WAIT_BEFORE | SYNC_FILE_RANGE_WRITE | SYNC_FILE_RANGE_WAIT_AFTER
: 等待之前的写入完成 -> 启动回写 -> 等待回写完成。非常彻底,但可能性能较低。
5. 返回值
- 成功: 返回 0。
- 失败: 返回 -1,并设置全局变量
errno
来指示具体的错误原因。
6. 错误码 (errno
)
EBADF
:fd
不是有效的文件描述符,或者该文件不支持同步操作。EINVAL
:flags
参数无效,或者offset
为负数。EIO
: I/O 错误。ENOMEM
: 内核内存不足。ENOSPC
: 设备上没有足够的空间。ESPIPE
:fd
指向的是管道、套接字或 FIFO,这些不支持sync_file_range
。
7. 相似函数或关联函数
fsync
: 同步文件的所有数据和元数据(如修改时间、文件大小等)。它会等待所有操作完成。fdatasync
: 同步文件的数据和必要的元数据(不包括访问时间等非必要元数据),通常比fsync
快一些。它也会等待完成。msync
: 用于同步mmap
内存映射区域到文件。sync
: 同步所有已挂载文件系统上的缓冲数据到磁盘。open
withO_SYNC
/O_DSYNC
: 在打开文件时指定同步标志,使得后续的write
操作具有同步语义。
8. 示例代码
下面的示例演示了如何使用 sync_file_range
来同步文件的特定部分。
#define _GNU_SOURCE // 启用 GNU 扩展以使用 sync_file_range
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h> // 包含 open, O_* flags, sync_file_range
#include <sys/stat.h> // 包含 open modes
#include <string.h>
#include <errno.h>
#include <time.h> // 包含 clock_gettime
// 计算时间差(毫秒)
double time_diff_ms(struct timespec start, struct timespec end) {
return ((end.tv_sec - start.tv_sec) * 1000.0) + ((end.tv_nsec - start.tv_nsec) / 1000000.0);
}
int main() {
const char *filename = "sync_test_file.dat";
const size_t file_size = 10 * 1024 * 1024; // 10 MiB
const size_t chunk_size = 1024 * 1024; // 1 MiB chunks
int fd;
char *data;
struct timespec start, end;
double elapsed_time;
printf("--- Demonstrating sync_file_range ---\n");
// 1. 分配内存并填充数据
data = malloc(chunk_size);
if (!data) {
perror("malloc");
exit(EXIT_FAILURE);
}
memset(data, 'A', chunk_size);
// 2. 创建并打开文件 (O_DIRECT 通常不与普通 buffer 一起用,这里仅作演示文件操作)
fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0644);
if (fd == -1) {
perror("open");
free(data);
exit(EXIT_FAILURE);
}
printf("Created and opened file: %s\n", filename);
// 3. 写入数据到文件
printf("Writing %zu bytes (%.2f MiB) in %zu byte chunks...\n", file_size, file_size / (1024.0*1024.0), chunk_size);
for (size_t i = 0; i < file_size; i += chunk_size) {
if (write(fd, data, chunk_size) != (ssize_t)chunk_size) {
perror("write");
close(fd);
free(data);
unlink(filename); // 清理
exit(EXIT_FAILURE);
}
// 每写入 2MB,演示一次部分同步
if (i > 0 && (i % (2 * chunk_size)) == 0) {
off64_t sync_offset = i - (2 * chunk_size);
off64_t sync_nbytes = 2 * chunk_size;
printf(" Written up to %zu bytes. Syncing range [%ld, %ld)...\n",
i, (long)sync_offset, (long)(sync_offset + sync_nbytes));
// --- 关键演示:使用 sync_file_range 同步特定范围 ---
// SYNC_FILE_RANGE_WRITE: 启动回写
// SYNC_FILE_RANGE_WAIT_AFTER: 等待回写完成
clock_gettime(CLOCK_MONOTONIC, &start);
if (sync_file_range(fd, sync_offset, sync_nbytes,
SYNC_FILE_RANGE_WRITE | SYNC_FILE_RANGE_WAIT_AFTER) == -1) {
perror("sync_file_range");
// 不退出,继续演示
}
clock_gettime(CLOCK_MONOTONIC, &end);
elapsed_time = time_diff_ms(start, end);
printf(" -> sync_file_range for %ld bytes took %.2f ms\n", (long)sync_nbytes, elapsed_time);
}
}
printf("Finished writing file.\n");
// 4. 演示同步整个文件的最后部分
printf("\n--- Syncing final part of the file ---\n");
off64_t last_sync_offset = (file_size / chunk_size - 2) * chunk_size; // 倒数第二块开始
off64_t last_sync_nbytes = file_size - last_sync_offset; // 到文件末尾
printf("Syncing final range [%ld, EOF) using sync_file_range...\n", (long)last_sync_offset);
clock_gettime(CLOCK_MONOTONIC, &start);
if (sync_file_range(fd, last_sync_offset, last_sync_nbytes,
SYNC_FILE_RANGE_WRITE | SYNC_FILE_RANGE_WAIT_AFTER) == -1) {
perror("sync_file_range (final part)");
}
clock_gettime(CLOCK_MONOTONIC, &end);
elapsed_time = time_diff_ms(start, end);
printf("-> sync_file_range for final %ld bytes took %.2f ms\n", (long)last_sync_nbytes, elapsed_time);
// 5. 对比:使用 fsync 同步整个文件
printf("\n--- Comparing with fsync (syncs entire file) ---\n");
printf("Calling fsync() to sync the entire file...\n");
clock_gettime(CLOCK_MONOTONIC, &start);
if (fsync(fd) == -1) {
perror("fsync");
}
clock_gettime(CLOCK_MONOTONIC, &end);
elapsed_time = time_diff_ms(start, end);
printf("-> fsync() took %.2f ms\n", elapsed_time);
// 6. 关闭文件
close(fd);
free(data);
// 7. 验证文件大小
struct stat sb;
if (stat(filename, &sb) == 0) {
printf("\n--- Verification ---\n");
printf("File '%s' created successfully.\n", filename);
printf("Final file size: %ld bytes (%.2f MiB)\n", (long)sb.st_size, sb.st_size / (1024.0*1024.0));
}
// 8. 清理 (注释掉这行可以保留文件用于检查)
// unlink(filename);
// printf("Deleted test file '%s'.\n", filename);
printf("\n--- Summary ---\n");
printf("1. sync_file_range allows synchronizing a specific byte range of a file.\n");
printf("2. It can be more efficient than fsync for large files where only parts need syncing.\n");
printf("3. Flags control whether to start writeback (WRITE) and/or wait for completion (WAIT_*).\n");
printf("4. It provides fine-grained control for performance-critical applications like databases.\n");
return 0;
}
9. 编译和运行
# 假设代码保存在 sync_file_range_example.c 中
# 注意:_GNU_SOURCE 可能已由 #define 定义,显式添加也无妨
gcc -D_GNU_SOURCE -o sync_file_range_example sync_file_range_example.c
# 运行程序
./sync_file_range_example
10. 预期输出 (时间值会有所不同)
--- Demonstrating sync_file_range ---
Created and opened file: sync_test_file.dat
Writing 10485760 bytes (10.00 MiB) in 1048576 byte chunks...
Written up to 2097152 bytes. Syncing range [0, 2097152)...
-> sync_file_range for 2097152 bytes took 5.23 ms
Written up to 4194304 bytes. Syncing range [2097152, 4194304)...
-> sync_file_range for 2097152 bytes took 3.12 ms
Written up to 6291456 bytes. Syncing range [4194304, 6291456)...
-> sync_file_range for 2097152 bytes took 2.87 ms
Written up to 8388608 bytes. Syncing range [6291456, 8388608)...
-> sync_file_range for 2097152 bytes took 3.01 ms
Finished writing file.
--- Syncing final part of the file ---
Syncing final range [8388608, EOF) using sync_file_range...
-> sync_file_range for final 2097152 bytes took 2.95 ms
--- Comparing with fsync (syncs entire file) ---
Calling fsync() to sync the entire file...
-> fsync() took 12.34 ms
--- Verification ---
File 'sync_test_file.dat' created successfully.
Final file size: 10485760 bytes (10.00 MiB)
--- Summary ---
1. sync_file_range allows synchronizing a specific byte range of a file.
2. It can be more efficient than fsync for large files where only parts need syncing.
3. Flags control whether to start writeback (WRITE) and/or wait for completion (WAIT_*).
4. It provides fine-grained control for performance-critical applications like databases.
11. 总结
sync_file_range
是一个强大的、用于精确控制文件数据同步的系统调用。它允许程序只同步文件的特定部分,而不是整个文件,从而在需要数据持久化保证的场景下提供了比 fsync
更好的性能。理解其标志位的含义(WRITE
, WAIT_BEFORE
, WAIT_AFTER
)对于正确使用它至关重要。它是构建高性能、数据安全应用(如数据库、文件系统工具)的重要工具。