sync_file_range系统调用及示例

好的,我们来深入学习 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)

  • EBADFfd 不是有效的文件描述符,或者该文件不支持同步操作。
  • EINVALflags 参数无效,或者 offset 为负数。
  • EIO: I/O 错误。
  • ENOMEM: 内核内存不足。
  • ENOSPC: 设备上没有足够的空间。
  • ESPIPEfd 指向的是管道、套接字或 FIFO,这些不支持 sync_file_range

7. 相似函数或关联函数

  • fsync: 同步文件的所有数据和元数据(如修改时间、文件大小等)。它会等待所有操作完成。
  • fdatasync: 同步文件的数据和必要的元数据(不包括访问时间等非必要元数据),通常比 fsync 快一些。它也会等待完成。
  • msync: 用于同步 mmap 内存映射区域到文件。
  • sync: 同步所有已挂载文件系统上的缓冲数据到磁盘。
  • open with O_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 更好的性能。理解其标志位的含义(WRITEWAIT_BEFOREWAIT_AFTER)对于正确使用它至关重要。它是构建高性能、数据安全应用(如数据库、文件系统工具)的重要工具。

此条目发表在linux文章分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注