在Linux中复制文件的最有效的方法

我正在一个操作系统无关的文件pipe理器,我正在寻找最有效的方法来复制文件的Linux。 Windows有一个内置的函数CopyFileEx() ,但是从我注意到的,Linux没有这样的标准函数。 所以我想我将不得不执行我自己的。 显而易见的方法是fopen / fread / fwrite,但有没有更好(更快)的方法呢? 我还必须有能力每隔一段时间停下来,以便我可以更新文件进度菜单的“复制到目前为止”计数。

不幸的是,你不能在这里使用sendfile() ,因为目的地不是套接字。 (名称sendfile()来自send() +“file”)。

对于零拷贝,您可以使用@Dave建议的splice() 。 (除非它不是零拷贝;它将是源文件页面缓存到目标文件页面缓存的“一个拷贝”。)

但是…(a) splice()是Linux专用的; 和(b)你几乎可以肯定地使用便携式接口,只要你正确地使用它们。

总之,用一个小的临时缓冲区使用open() + read() + write() 。 我建议8K。 所以你的代码看起来像这样:

 int in_fd = open("source", O_RDONLY); assert(in_fd >= 0); int out_fd = open("dest", O_WRONLY); assert(out_fd >= 0); char buf[8192]; while (1) { ssize_t result = read(in_fd, &buf[0], sizeof(buf)); if (!result) break; assert(result > 0); assert(write(out_fd, &buf[0], result) == result); } 

有了这个循环,你将从in_fd页面缓存复制8K到CPU L1缓存,然后从L1缓存写入out_fd页面缓存。 然后你将用文件中的下一个8K块覆盖L1缓存的那部分,依此类推。 最终的结果是buf中的数据永远不会真正存储在主存中(除了最后一次除外)。 从系统RAM的角度来看,这和使用“zero-copy” splice() 。 另外,它可以完美的移植到任何POSIX系统。

请注意,这里的小缓冲区是关键。 对于L1数据缓存,典型的现代CPU有32K左右,所以如果缓冲区太大,这种方法会比较慢。 可能多得多,慢得多。 所以保持在“几千字节”范围内的缓冲区。

当然,除非你的磁盘子系统速度非常快,否则内存带宽可能不是你的限制因素。 所以我会推荐posix_fadvise让内核知道你在做什么:

 posix_fadvise(in_fd, 0, 0, POSIX_FADV_SEQUENTIAL); 

这将暗示Linux内核的预读机器应该是非常积极的。

我也建议使用posix_fallocate预先分配目标文件的存储空间。 这会提前告诉你是否会耗尽磁盘。 而对于现代文件系统(如XFS)的现代内核,它将有助于减少目标文件中的碎片。

我推荐的最后一件事是mmap 。 这通常是TLB抖动的最慢的方法。 (最新的内核使用“透明的巨大页面”可能会减轻这一点;我最近没有尝试,但是它肯定是非常糟糕的,所以如果你有足够的时间来测试基准和一个非常近的内核的话,我只会麻烦测试mmap

[更新]

评论中有一个问题,关于从一个文件到另一个文件的splice是否为零拷贝。 Linux内核开发者称之为“页面窃取”。 splice的手册页和内核源代码中的注释都说SPLICE_F_MOVE标志应该提供这个功能。

不幸的是,对SPLICE_F_MOVE的支持在2.6.21被抽出(在2007年),并且从未被替换过。 (内核源代码中的注释从来没有得到更新。)如果搜索内核源代码,您会发现SPLICE_F_MOVE实际上并没有被引用。 我可以找到的最后一条消息 (从2008年)说,它是“等待替代”。

底线是从一个文件到另一个文件的splice调用memcpy来移动数据; 它不是零拷贝。 这在使用小缓冲区read / write用户空间中可以做得并不好,所以你不妨坚持使用标准的便携式接口。

如果“页面窃取”被添加回Linux内核,那么splice的好处就会大得多。 (即使在今天,当目标是一个套接字时,你也可以得到真正的零拷贝,使得splice更具吸引力。)但是就这个问题而言, splice不会给你带来太多的收益。

如果你知道他们将使用Linux> 2.6.17, splice()是在Linux中进行零拷贝的方法:

  //using some default parameters for clarity below. Don't do this in production. #define splice(a, b, c) splice(a, 0, b, 0, c, 0) int p[2]; pipe(p); int out = open(OUTFILE, O_WRONLY); int in = open(INFILE, O_RDONLY) while(splice(p[0], out, splice(in, p[1], 4096))>0); 

使用open / read / write – 他们避免fopen和朋友所做的libc级别的缓冲。

另外,如果你使用的是GLib,你可以使用它的g_copy_file函数。

最后,什么可能会更快,但应该进行测试以确保:使用openmmap对输入文件进行内存映射,然后从内存区域write输出文件。 您可能希望保持打开/读取/写入状态作为回退,因为此方法仅限于进程的地址空间大小。

编辑:原来的答案建议映射两个文件; @bdonlan在评论中只提出了一个好的建议。

您可能需要对dd命令进行基准测试