我在一个独立于操作系统的文件管理器上工作,我正在寻找最有效的方法来复制Linux文件。 Windows有一个内置函数CopyFileEx(),但是从我注意到的,Linux没有这样的标准函数。所以我想我必须实现自己的。 显而易见的方法是fopen / fread / fwrite,但有更好(更快)的方法吗?我还必须能够偶尔停止每一次,以便我可以更新文件进度菜单的“复制到目前为止”计数。
答案 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内核开发人员将此称为“页面窃取”。 splice
和comments 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
函数。
最后,可能会更快,但应该进行测试以确保:使用open
和mmap
对输入文件进行内存映射,然后从内存区域对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);
}
根据操作系统的不同,您的里程可能会因系统调用而不同,例如splice和sendfile,但是请注意手册页中的注释:
如果sendfile()因EINVAL或ENOSYS失败,应用程序可能希望退回到read(2)/ write(2)。