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

时间:2011-09-18 18:59:13

标签: c linux

我在一个独立于操作系统的文件管理器上工作,我正在寻找最有效的方法来复制Linux文件。 Windows有一个内置函数CopyFileEx(),但是从我注意到的,Linux没有这样的标准函数。所以我想我必须实现自己的。 显而易见的方法是fopen / fread / fwrite,但有更好(更快)的方法吗?我还必须能够偶尔停止每一次,以便我可以更新文件进度菜单的“复制到目前为止”计数。

5 个答案:

答案 0 :(得分:34)

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

对于零拷贝,您可以使用@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 read_result = read(in_fd, &buf[0], sizeof(buf));
    if (!read_result) break;
    assert(read_result > 0);
    ssize_t write_result = write(out_fd, &buf[0], read_result);
    assert(write_result == read_result);
}

使用此循环,您将从in_fd页面缓存复制8K到CPU L1缓存,然后将其从L1缓存写入out_fd页面缓存。然后,您将使用文件中的下一个8K块覆盖L1缓存的该部分,依此类推。最终结果是buf中的数据根本不会实际存储在主存储器中(除了可能在结束时一次);从系统RAM的角度来看,这与使用“零拷贝”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内核开发人员将此称为“页面窃取”。 splicecomments in the kernel source的手册页都说SPLICE_F_MOVE标志应该提供此功能。

不幸的是,对SPLICE_F_MOVE的支持是yanked in 2.6.21 (back in 2007),并且从未被替换过。 (内核源代码中的注释永远不会更新。)如果搜索内核源代码,您会发现SPLICE_F_MOVE实际上并未在任何地方引用。 last message I can find(自2008年起)表示它正在“等待替代”。

底线是splice从一个文件到另一个文件调用memcpy来移动数据;它是零拷贝。这比使用带有小缓冲区的read / write在用户空间中所做的要好得多,因此您可以坚持使用标准的可移植接口。

如果将“页面窃取”添加回Linux内核,那么splice的好处会更大。 (即使在今天,当目的地是一个套接字时,你会得到真正的零拷贝,使splice更有吸引力。)但就这个问题而言,splice并没有给你带来太大的收获。 / p>

答案 1 :(得分:5)

使用open / read / write - 它们可以避免fopen和朋友完成的libc级缓冲。

或者,如果您使用的是GLib,则可以使用其g_copy_file函数。

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

编辑:原始回答建议映射两个文件; @bdonlan在评论中提出了极好的建议,只有地图一。

答案 2 :(得分:4)

如果你知道他们将使用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);

答案 3 :(得分:-1)

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

答案 4 :(得分:-1)

我从这篇帖子的最新副本中得到的答案。

boost现在提供mapped_file_source,可移植地模拟内存映射文件。

效率可能不及CopyFileEx()splice(),但可移植且简洁。

该程序带有2个文件名参数。它将源文件的前一半复制到目标文件。

#include <boost/iostreams/device/mapped_file.hpp>
#include <iostream>
#include <fstream>
#include <cstdio>

namespace iostreams = boost::iostreams;
int main(int argc, char** argv)
{
    if (argc != 3)
    {
        std::cerr << "usage: " << argv[0] << " <infile> <outfile> - copies half of the infile to outfile" << std::endl;
        std::exit(100);
    }

    auto source = iostreams::mapped_file_source(argv[1]);
    auto dest = std::ofstream(argv[2], std::ios::binary);
    dest.exceptions(std::ios::failbit | std::ios::badbit);
    auto first = source. begin();
    auto bytes = source.size() / 2;
    dest.write(first, bytes);
}

根据操作系统的不同,您的里程可能会因系统调用而不同,例如splicesendfile,但是请注意手册页中的注释:

  

如果sendfile()因EINVAL或ENOSYS失败,应用程序可能希望退回到read(2)/ write(2)。