一尘不染

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

linux

我在与OS无关的文件管理器中工作,并且正在寻找为Linux复制文件的最有效方法。Windows有一个内置函数CopyFileEx(),但是据我所知,Linux没有这种标准函数。所以我想我将必须实现自己的。明显的方法是fopen
/ fread / fwrite,但是是否有更好(更快)的方法呢?我还必须有能力每隔一段时间停止一次,以便可以更新文件进度菜单的“到目前为止”。


阅读 252

收藏
2020-06-03

共1个答案

一尘不染

不幸的是,您不能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);
}

通过此循环,您将把8k从in_fd页面缓存复制到CPU
L1缓存中,然后将其从L1缓存写入out_fd页面缓存中。然后,您将用文件中的下一个8K块覆盖L1缓存的该部分,依此类推。最终结果是,输入的数据buf根本不会真正存储在主存储器中(最后可能一次除外);从系统RAM的角度来看,这与使用“
zero-copy”一样好splice()。另外,它非常适合任何POSIX系统。

请注意,此处的小缓冲区是关键。典型的现代CPU的L1数据高速缓存大约为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并不能为您带来多少好处。

2020-06-03