39

Java如何真正将写入文件的数据与块设备刷新/同步。

我用 NIO 尝试了这段代码:

FileOutputStream s = new FileOutputStream(filename)
Channel c = s.getChannel()
while(xyz)
    c.write(buffer)
c.force(true)
s.getFD().sync()
c.close()

我认为 c.force(true) 与 s.getFD().sync() 应该就足够了,因为状态的文档

强制将此通道文件的任何更新写入包含它的存储设备。如果此通道的文件驻留在本地存储设备上,则当此方法返回时,可以保证自创建此通道或自上次调用此方法以来对文件所做的所有更改都已写入该设备。这对于确保在系统崩溃时不会丢失关键信息很有用。

要同步的文档指出:

强制所有系统缓冲区与底层设备同步。此方法在此 FileDescriptor 的所有修改数据和属性都已写入相关设备后返回。特别是,如果此 FileDescriptor 指的是物理存储介质,例如文件系统中的文件,则在与此 FileDesecriptor 关联的所有内存中修改的缓冲区副本都已写入物理介质之前,同步将不会返回。同步旨在供需要物理存储(例如文件)处于已知状态的代码使用。

这两个调用应该足够了。是吗?我猜他们不是。

背景:我使用 C/Java 做了一个小的性能比较(2 GB,顺序写入),Java 版本的速度是 C 版本的两倍,并且可能比硬件(单个 HD 上 120 MB/s)快。我还尝试使用 Runtime.getRuntime().exec("sync") 执行命令行工具同步,但这并没有改变行为。

导致 70 MB/s 的 C 代码是(使用低级 API(打开、写入、关闭)并没有太大变化):

FILE* fp = fopen(filename, "w");
while(xyz) {
    fwrite(buffer, 1, BLOCK_SIZE, fp);
}
fflush(fp);
fclose(fp);
sync();

没有最后的同步调用;我得到了不切实际的值(超过 1 GB 又名主内存性能)。

为什么C和Java之间有这么大的区别?有两种可能性:我没有在 Java 中正确同步数据,或者 C 代码由于某种原因不是最理想的。

更新:我已经使用“strace -cfT cmd”完成了 strace 运行。结果如下:

C(低级 API):MB/s 67.389782

% time seconds usecs/call 调用错误 syscall
------ ----------- ------------ --------- --------- ---- ------------
 87.21 0.200012 200012 1 fdatasync
 11.05 0.025345 1 32772 写
  1.74 0.004000 4000 1 同步

C(高级 API):MB/s 61.796458

% time seconds usecs/call 调用错误 syscall
------ ----------- ------------ --------- --------- ---- ------------
 73.19 0.144009 144009 1 同步
 26.81 0.052739 1 65539 ​​写

Java(1.6 SUN JRE、java.io API):MB/s 128.6755466197537

% time seconds usecs/call 调用错误 syscall
------ ----------- ------------ --------- --------- ---- ------------
 80.07 105.387609 3215 32776 写
  2.58 3.390060 3201 1059 读
  0.62 0.815251 815251 1 同步

Java(1.6 SUN JRE、java.nio API):MB/s 127.45830221558376

  5.52 0.980061 490031 2 同步
  1.60 0.284752 9 32774 写入
  0.00 0.000000 0 80 关闭

时间值似乎只是系统时间,因此毫无意义。

更新 2:我切换到另一台服务器,重新启动,并使用全新格式化的 ext3。现在,Java 和 C 之间只有 4% 的差异。我根本不知道出了什么问题。有时事情很奇怪。在写这个问题之前,我应该用另一个系统尝试测量。对不起。

更新 3:总结答案:

  • 使用 c.force(true) 后跟 s.getFD().sync() 用于 Java NIO 和 s.flush() 和 s.getFD().sync() 用于 Java 的流 API。对于 C 中的高级 API,不要忘记同步。fflush 将数据提交给操作系统,但不会将您的数据带到块设备。
  • 使用 strace 分析命令完成的系统调用
  • 在发布问题之前交叉检查您的结果。

更新 4:请注意以下后续问题

4

5 回答 5

11

实际上,在 C 语言中,您只想调用fsync()一个文件描述符,而不是sync()(或“同步”命令),它将内核flush向磁盘系统范围内的所有缓冲区发出信号。

如果您strace(在此处获取特定于 Linux 的)JVM,您应该能够观察到对输出文件进行的fsync()fdatasync()系统调用。这将是我所期望的getFD()sync()打电话去做。我假设只是在每次写入后都应该调用c.force(true)NIO 的标志。fsync()可能只是您使用的 JVM 实际上没有实现sync()调用?

我不确定为什么在将“sync”作为命令调用时没有看到任何区别:但显然,在第一次同步调用之后,后续调用通常要快得多。再一次,我倾向于将strace(在 Solaris 上的桁架)作为“这里实际发生的事情?”来爆发。工具。

于 2009-04-08T18:48:34.703 回答
5

使用同步 I/O 数据完整性完成是一个好主意。但是,您的 C 示例使用了错误的方法。您使用sync(),用于同步整个操作系统。

如果要将单个文件的块写入磁盘,则需要在 C中使用fsync(2)or fdatasync(2).

fdatasync()如果文件在您同步后没有更改名称或大小,则该变体会更有效。但它也可能不会保留所有元数据。如果你想编写自己的事务安全数据库系统,你需要观察更多的东西(比如 fsyncing 父目录)。

于 2014-05-23T20:22:23.360 回答
1

您需要告诉我们更多关于硬件和操作系统的信息,以及具体的 Java 版本。你如何衡量这个吞吐量?

您是正确的,强制/同步应该将数据强制输出到物理媒体。


这是副本的原始版本。在 Intel Mac 上使用 gcc 4.0 编译,应该是干净的。

/* rawcopy -- pure C, system calls only, copy argv[1] to argv[2] */

/* This is a test program which simply copies from file to file using
 * only system calls (section 2 of the manual.)
 *
 * Compile:
 *
 *      gcc -Wall -DBUFSIZ=1024 -o rawcopy rawcopy.c
 *
 * If DIRTY is defined, then errors are interpreted with perror(3).
 * This is ifdef'd so that the CLEAN version is free of stdio.  For
 * convenience I'm using BUFSIZ from stdio.h; to compile CLEAN just
 * use the value from your stdio.h in place of 1024 above.
 *
 * Compile DIRTY:
 *
 *      gcc -DDIRTY -Wall -o rawcopy rawcopy.c
 *
 */
#include <fcntl.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <stdlib.h>
#include <unistd.h>
#if defined(DIRTY)
#   if defined(BUFSIZ)
#       error "Don't define your own BUFSIZ when DIRTY"
#   endif
#   include <stdio.h>
#   define PERROR perror(argv[0])
#else
#   define CLEAN
#   define PERROR
#   if ! defined(BUFSIZ)
#       error "You must define your own BUFSIZ with -DBUFSIZ=<number>"
#   endif
#endif

char * buffer[BUFSIZ];          /* by definition stdio BUFSIZ should
                                   be optimal size for read/write */

extern int errno ;              /* I/O errors */

int main(int argc, char * argv[]) {
    int fdi, fdo ;              /* Input/output file descriptors */
    ssize_t len ;               /* length to read/write */
    if(argc != 3){
        PERROR;
        exit(errno);
    }

    /* Open the files, returning perror errno as the exit value if fails. */
    if((fdi = open(argv[1],O_RDONLY)) == -1){
        PERROR;
        exit(errno);
    }
    if((fdo = open(argv[2], O_WRONLY|O_CREAT)) == -1){
        PERROR;
        exit(errno);
    }

    /* copy BUFSIZ bytes (or total read on last block) fast as you
       can. */
    while((len = read(fdi, (void *) buffer, BUFSIZ)) > -1){
        if(len == -1){
            PERROR;
            exit(errno);
        }
        if(write(fdo, (void*)buffer, len) == -1){
            PERROR;
            exit(errno);
        }
    }
    /* close and fsync the files */
    if(fsync(fdo) ==-1){
        PERROR;
        exit(errno);
    }
    if(close(fdo) == -1){
        PERROR;
        exit(errno);
    }
    if(close(fdi) == -1){
        PERROR;
        exit(errno);
    }

    /* if it survived to here, all worked. */
    exit(0);
}
于 2009-04-08T15:47:28.057 回答
0

(我知道这是一个很晚的回复,但我在谷歌搜索时遇到了这个线程,这可能也是你最终来到这里的原因。)

您在 Java 中对单个文件描述符调用 sync(),因此只有与该文件相关的缓冲区才会刷新到磁盘。

在 C 和命令行中,您在整个操作系统上调用 sync() - 因此每个文件缓冲区都会被刷新到磁盘,因为您的操作系统正在执行所有操作。

为了比较,C 调用应该是 syncfs(fp);

从 Linux 手册页:

   sync() causes all buffered modifications to file metadata and data to
   be written to the underlying file systems.

   syncfs() is like sync(), but synchronizes just the file system contain‐
   ing file referred to by the open file descriptor fd.
于 2014-04-01T23:51:01.583 回答
-1

C 代码可能不是最理想的,因为它使用 stdio 而不是原始 OS write()。但是,java 可能会更优化,因为它分配了更大的缓冲区?

无论如何,您只能信任 APIDOC。其余的超出你的职责。

于 2009-04-08T15:36:36.477 回答