返回

POSIX Direct I/O 实现指南:跨平台优化大文件拷贝

Linux

POSIX 下实现 Direct I/O 的方法

开发过程中,有时我们需要直接操作磁盘,绕过操作系统缓存,提高大文件拷贝的性能。这种方式称为 Direct I/O。Windows 提供了 CreateFileA() 函数中的 FILE_FLAG_WRITE_THROUGHFILE_FLAG_NO_BUFFERING 标志来实现。Linux 2.4.10 及以上版本则在 open() 函数中支持 O_DIRECT 标志。 那么, 跨类 Unix 系统实现Direct IO 是否有一个一致的,可移植的办法呢?

问题产生原因

操作系统通常会使用缓存 (Page Cache) 来优化 I/O 操作。读写数据时,会先与缓存交互。读取时,如果数据在缓存中,则直接从缓存读取;如果不在,则从磁盘读取并放入缓存。写入时,数据会先写入缓存,操作系统再在合适的时机将缓存中的数据写回磁盘。

这种方式在大多数情况下都能提升性能。但是, 对某些大文件进行一次性拷贝时,缓存带来的效益反而会降低。 因为数据不会被再次访问, 进入缓存纯粹增加了CPU和内存开销。Direct I/O 可以避免这个问题,让数据直接在用户空间缓冲区和磁盘之间传输。

解决方案

要实现 Direct I/O,主要目标就是绕过操作系统缓存。遗憾的是,POSIX 标准本身并没有直接提供一个与 O_DIRECT 完全等价的、统一的标志或函数。 不过我们可以通过结合使用几个 POSIX API, 或使用平台特定的功能来实现类似的效果。

方案一:fcntl()F_NOCACHE (macOS/FreeBSD)

某些系统,比如 macOS 和 FreeBSD,提供了 fcntl() 函数的 F_NOCACHE 选项。 它可以用来关闭文件符的缓存。

原理与作用:

F_NOCACHE 告诉操作系统,对该文件符的 I/O 操作不应该使用缓存。虽然不叫"Direct I/O",但实际效果是类似的。

代码示例:

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    int fd = open("testfile.dat", O_RDWR | O_CREAT, 0644);
    if (fd == -1) {
        perror("open");
        exit(1);
    }

    int nocache = 1;
    if (fcntl(fd, F_NOCACHE, nocache) == -1) {
        perror("fcntl");
        exit(1);
    }

    // 进行 I/O 操作...
    char* buffer;
    // 确保buffer大小对齐
    posix_memalign((void**)&buffer, 512, 4096);

    ssize_t bytes_written = write(fd, buffer, 4096);

    if (bytes_written == -1) {
          perror("write");
          exit(1);
    }

    close(fd);
    free(buffer);
    return 0;
}

安全建议:

  • F_NOCACHE 是平台相关的。 在其他系统上可能没有这个选项。
  • 要注意错误处理, 文件打开/创建失败要能退出程序.
  • 进行Direct I/O 需要对buffer进行内存对齐.

方案二:posix_fadvise()

posix_fadvise() 函数可以向操作系统提供关于文件访问模式的建议。我们可以使用 POSIX_FADV_DONTNEED 来暗示操作系统,某些数据不再需要被缓存。

原理与作用:

POSIX_FADV_DONTNEED 告诉操作系统,指定范围的数据在不久的将来不太可能被再次访问,可以从缓存中释放。虽然这不能保证立即从缓存中删除数据, 但给内核清缓存提供了强烈信号.

代码示例:

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    int fd = open("testfile.dat", O_RDWR | O_CREAT, 0644);
    if (fd == -1) {
        perror("open");
        exit(1);
    }

    // 文件大小
    off_t file_size = 1024 * 1024 * 10; // 10MB

    // 对整个文件建议 DONTNEED
    if (posix_fadvise(fd, 0, file_size, POSIX_FADV_DONTNEED) != 0) {
        perror("posix_fadvise");
    }

    // 进行 I/O 操作...
    char* buffer;

    // 确保buffer大小对齐
    posix_memalign((void**)&buffer, 512, 4096);

    ssize_t bytes_written = write(fd, buffer, 4096);

    if(bytes_written == -1) {
          perror("write");
          exit(1);
    }

    close(fd);
    free(buffer);
    return 0;
}

安全建议:

  • 进行Direct I/O 需要对buffer进行内存对齐.

进阶使用技巧:

可以结合 POSIX_FADV_SEQUENTIAL 使用。如果你的程序是顺序访问文件,POSIX_FADV_SEQUENTIAL 可以提示操作系统进行预读优化。 在某些场景下可以提高非Direct IO 模式下的性能。

posix_fadvise(fd, 0, file_size, POSIX_FADV_SEQUENTIAL | POSIX_FADV_DONTNEED);

这样内核可能会采取更大的IO尺寸, 更少的IO次数来读取数据.

方案三:自定义块大小与同步

这种方案需要我们自己控制 I/O 的块大小,并且在每次写操作后使用 fsync()fdatasync() 强制同步数据到磁盘。

原理与作用:

Direct I/O 的一个关键特点是 I/O 操作的原子性以及数据直接与磁盘同步,不经过缓存。我们可以通过精心设计块大小(通常是文件系统块大小的倍数)来保证数据对齐,通过fsync/fdatasync保证立即同步。

代码示例:

#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>

int main() {
    int fd;
     // 这里我们还是先试试O_DIRECT,以发挥最佳效果。
     #ifdef O_DIRECT
          fd= open("testfile.dat", O_RDWR | O_CREAT | O_DIRECT, 0644);
    #else
          fd= open("testfile.dat", O_RDWR | O_CREAT, 0644);
    #endif

    if (fd == -1) {
        perror("open");
        exit(1);
    }

    // 文件系统块大小 (假设为4096)
    size_t block_size = 4096;
    char *buffer;

   // 申请对齐的内存
    if (posix_memalign((void **)&buffer, block_size, block_size) != 0) {
            perror("posix_memalign");
            exit(1);
     }

    // 写入一个块的数据
    ssize_t bytes_written = write(fd, buffer, block_size);
      if (bytes_written == -1) {
          perror("write");
          exit(1);
      }
    // 同步数据到磁盘
    if (fsync(fd) == -1) {  // 或者 fdatasync(fd)
        perror("fsync");
        exit(1);
    }
    //清理内存操作

    close(fd);
    free(buffer);
    return 0;
}

安全建议:

  • 为了正确进行对齐的I/O, 要使用posix_memalignaligned_alloc 来分配buffer。 使用普通的malloc 可能导致未定义行为!
  • 块大小需要与文件系统块大小对齐。
  • 频繁的 fsync()fdatasync() 调用会影响性能,但这是为了确保数据不经过缓存直接写入磁盘所必须的.
  • 尽量尝试打开文件时先使用 O_DIRECT , 这可以获得最优性能. 如果O_DIRECT不可用, 再退回上面代码展示的方法。

进阶使用技巧:

可以使用 fstat() 获取文件系统块大小 ( stat 结构体的 st_blksize 成员 )。这样更具有可移植性,因为不需要硬编码块大小。
可以使用 #ifdef O_DIRECT 等宏定义来处理支持与不支持O_DIRECT的情况。


#include <sys/stat.h>

struct stat file_stat;
fstat(fd, &file_stat);
size_t block_size = file_stat.st_blksize;

方案四 (仅适用特定情况): mmap() 并手动管理

在某些非常特殊的情况下,如果你需要对文件进行频繁的随机读写,并且对内存使用有严格控制, 可以考虑用 mmap 将文件映射到内存中。但这个方法一般 不适合 简单文件拷贝的需求。

原理及作用:
mmap() 会将文件映射到进程的虚拟内存。然后,你可以直接对这块内存进行读写,,就如同在操作一个大的数组。

注意事项:

这种方式要实现类似Direct I/O的效果需要进行额外的控制:

  • 你需要手动确保数据的同步. (使用msync()函数).
  • 你需要精心设计内存管理, 来模拟类似 Direct I/O的行为。

总结

尽管 POSIX 标准没有提供一个完全统一的 Direct I/O 接口,但是通过上述几种方式,我们可以在大多数类 Unix 系统上实现类似的功能. 如果你追求极致性能,首先应该在支持 O_DIRECT的平台上尝试使用O_DIRECT , 它能够提供最佳Direct I/O 的性能. 如果要求更好的跨平台兼容性, 那么根据各个平台实际提供的选项 (F_NOCACHEposix_fadvise()等) ,选择合适的技术组合. 记住,所有的Direct I/O 操作要确保buffer 内存对齐。