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

Rad*_*adu 27 c linux

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

Nem*_*emo 35

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

对于零拷贝,您可以splice()按照@Dave的建议使用.(除非它不是零拷贝;它将是从源文件的页面缓存到目标文件的页面缓存的"一个副本".)

但是......(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);
}
Run Code Online (Sandbox Code Playgroud)

使用此循环,您将从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);
Run Code Online (Sandbox Code Playgroud)

这将向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不会给你买得太多.

  • 抬头 - 在2.6.33以后,out_fd可以是任何文件.查看联机帮助页. (4认同)
  • @Nemo 请更新答案:https://man7.org/linux/man-pages/man2/sendfile.2.html `在 2.6.33 之前的 Linux 内核中,out_fd 必须引用套接字。从 Linux 2.6.33 开始,它可以是任何文件` (2认同)

Dav*_*ave 6

如果您知道他们将使用 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);
Run Code Online (Sandbox Code Playgroud)

  • `splice` 和 `sendfile` 都不是标准化的。如果性能非常重要,请为每个环境编写函数的优化副本,然后从标准 c 中使用 `fread/frwite`。glib 函数可能会做类似的事情。 (3认同)
  • @Radu - 在最近的 Linux 上,至少 `splice()` 是正确的答案,因为 `sendfile()` 不再支持文件->文件副本 (2认同)

Mic*_*and 5

使用open/ read/ write- 它们避免了fopen和朋友一起完成的libc级缓冲.

或者,如果您使用的是GLib,则可以使用其g_copy_file功能.

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

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

  • 我不会mmap这两个文件,我自己 - 映射源,然后从映射区域`write()`.你必须以任何一种方式复制它,所以不妨在内核中执行它并避免目标上的页面错误. (3认同)