以理智,安全,高效的方式复制文件


304

我正在寻找一种复制文件(二进制或文本)的好方法。我写了几个样本,每个人都在工作。但是我想听听经验丰富的程序员的意见。

我错过了很好的例子,并寻找一种与C ++兼容的方式。

ANSI C路

#include <iostream>
#include <cstdio>    // fopen, fclose, fread, fwrite, BUFSIZ
#include <ctime>
using namespace std;

int main() {
    clock_t start, end;
    start = clock();

    // BUFSIZE default is 8192 bytes
    // BUFSIZE of 1 means one chareter at time
    // good values should fit to blocksize, like 1024 or 4096
    // higher values reduce number of system calls
    // size_t BUFFER_SIZE = 4096;

    char buf[BUFSIZ];
    size_t size;

    FILE* source = fopen("from.ogv", "rb");
    FILE* dest = fopen("to.ogv", "wb");

    // clean and more secure
    // feof(FILE* stream) returns non-zero if the end of file indicator for stream is set

    while (size = fread(buf, 1, BUFSIZ, source)) {
        fwrite(buf, 1, size, dest);
    }

    fclose(source);
    fclose(dest);

    end = clock();

    cout << "CLOCKS_PER_SEC " << CLOCKS_PER_SEC << "\n";
    cout << "CPU-TIME START " << start << "\n";
    cout << "CPU-TIME END " << end << "\n";
    cout << "CPU-TIME END - START " << end - start << "\n";
    cout << "TIME(SEC) " << static_cast<double>(end - start) / CLOCKS_PER_SEC << "\n";

    return 0;
}

POSIX-WAY(K&R在“ C编程语言”中使用此语言,更底层)

#include <iostream>
#include <fcntl.h>   // open
#include <unistd.h>  // read, write, close
#include <cstdio>    // BUFSIZ
#include <ctime>
using namespace std;

int main() {
    clock_t start, end;
    start = clock();

    // BUFSIZE defaults to 8192
    // BUFSIZE of 1 means one chareter at time
    // good values should fit to blocksize, like 1024 or 4096
    // higher values reduce number of system calls
    // size_t BUFFER_SIZE = 4096;

    char buf[BUFSIZ];
    size_t size;

    int source = open("from.ogv", O_RDONLY, 0);
    int dest = open("to.ogv", O_WRONLY | O_CREAT /*| O_TRUNC/**/, 0644);

    while ((size = read(source, buf, BUFSIZ)) > 0) {
        write(dest, buf, size);
    }

    close(source);
    close(dest);

    end = clock();

    cout << "CLOCKS_PER_SEC " << CLOCKS_PER_SEC << "\n";
    cout << "CPU-TIME START " << start << "\n";
    cout << "CPU-TIME END " << end << "\n";
    cout << "CPU-TIME END - START " << end - start << "\n";
    cout << "TIME(SEC) " << static_cast<double>(end - start) / CLOCKS_PER_SEC << "\n";

    return 0;
}

KISS-C ++-流缓冲方式

#include <iostream>
#include <fstream>
#include <ctime>
using namespace std;

int main() {
    clock_t start, end;
    start = clock();

    ifstream source("from.ogv", ios::binary);
    ofstream dest("to.ogv", ios::binary);

    dest << source.rdbuf();

    source.close();
    dest.close();

    end = clock();

    cout << "CLOCKS_PER_SEC " << CLOCKS_PER_SEC << "\n";
    cout << "CPU-TIME START " << start << "\n";
    cout << "CPU-TIME END " << end << "\n";
    cout << "CPU-TIME END - START " <<  end - start << "\n";
    cout << "TIME(SEC) " << static_cast<double>(end - start) / CLOCKS_PER_SEC << "\n";

    return 0;
}

COPY-ALGORITHM-C ++-WAY

#include <iostream>
#include <fstream>
#include <ctime>
#include <algorithm>
#include <iterator>
using namespace std;

int main() {
    clock_t start, end;
    start = clock();

    ifstream source("from.ogv", ios::binary);
    ofstream dest("to.ogv", ios::binary);

    istreambuf_iterator<char> begin_source(source);
    istreambuf_iterator<char> end_source;
    ostreambuf_iterator<char> begin_dest(dest); 
    copy(begin_source, end_source, begin_dest);

    source.close();
    dest.close();

    end = clock();

    cout << "CLOCKS_PER_SEC " << CLOCKS_PER_SEC << "\n";
    cout << "CPU-TIME START " << start << "\n";
    cout << "CPU-TIME END " << end << "\n";
    cout << "CPU-TIME END - START " <<  end - start << "\n";
    cout << "TIME(SEC) " << static_cast<double>(end - start) / CLOCKS_PER_SEC << "\n";

    return 0;
}

OWN-BUFFER-C ++-WAY

#include <iostream>
#include <fstream>
#include <ctime>
using namespace std;

int main() {
    clock_t start, end;
    start = clock();

    ifstream source("from.ogv", ios::binary);
    ofstream dest("to.ogv", ios::binary);

    // file size
    source.seekg(0, ios::end);
    ifstream::pos_type size = source.tellg();
    source.seekg(0);
    // allocate memory for buffer
    char* buffer = new char[size];

    // copy file    
    source.read(buffer, size);
    dest.write(buffer, size);

    // clean up
    delete[] buffer;
    source.close();
    dest.close();

    end = clock();

    cout << "CLOCKS_PER_SEC " << CLOCKS_PER_SEC << "\n";
    cout << "CPU-TIME START " << start << "\n";
    cout << "CPU-TIME END " << end << "\n";
    cout << "CPU-TIME END - START " <<  end - start << "\n";
    cout << "TIME(SEC) " << static_cast<double>(end - start) / CLOCKS_PER_SEC << "\n";

    return 0;
}

LINUX-WAY //需要内核> = 2.6.33

#include <iostream>
#include <sys/sendfile.h>  // sendfile
#include <fcntl.h>         // open
#include <unistd.h>        // close
#include <sys/stat.h>      // fstat
#include <sys/types.h>     // fstat
#include <ctime>
using namespace std;

int main() {
    clock_t start, end;
    start = clock();

    int source = open("from.ogv", O_RDONLY, 0);
    int dest = open("to.ogv", O_WRONLY | O_CREAT /*| O_TRUNC/**/, 0644);

    // struct required, rationale: function stat() exists also
    struct stat stat_source;
    fstat(source, &stat_source);

    sendfile(dest, source, 0, stat_source.st_size);

    close(source);
    close(dest);

    end = clock();

    cout << "CLOCKS_PER_SEC " << CLOCKS_PER_SEC << "\n";
    cout << "CPU-TIME START " << start << "\n";
    cout << "CPU-TIME END " << end << "\n";
    cout << "CPU-TIME END - START " <<  end - start << "\n";
    cout << "TIME(SEC) " << static_cast<double>(end - start) / CLOCKS_PER_SEC << "\n";

    return 0;
}

环境

  • GNU / LINUX(Archlinux)
  • 内核3.3
  • GLIBC-2.15,LIBSTDC ++ 4.7(GCC-LIBS),GCC 4.7,Coreutils 8.16
  • 使用RUNLEVEL 3(多用户,网络,终端,无GUI)
  • INTEL SSD-Postville 80 GB,最多可填充50%
  • 复制270 MB的OGG视频文件

重现步骤

 1. $ rm from.ogg
 2. $ reboot                           # kernel and filesystem buffers are in regular
 3. $ (time ./program) &>> report.txt  # executes program, redirects output of program and append to file
 4. $ sha256sum *.ogv                  # checksum
 5. $ rm to.ogg                        # remove copy, but no sync, kernel and fileystem buffers are used
 6. $ (time ./program) &>> report.txt  # executes program, redirects output of program and append to file

结果(使用的CPU时间)

Program  Description                 UNBUFFERED|BUFFERED
ANSI C   (fread/frwite)                 490,000|260,000  
POSIX    (K&R, read/write)              450,000|230,000  
FSTREAM  (KISS, Streambuffer)           500,000|270,000 
FSTREAM  (Algorithm, copy)              500,000|270,000
FSTREAM  (OWN-BUFFER)                   500,000|340,000  
SENDFILE (native LINUX, sendfile)       410,000|200,000  

文件大小不变。
sha256sum打印相同的结果。
该视频文件仍然可以播放。

问题

  • 您想要哪种方法?
  • 您知道更好的解决方案吗?
  • 您在我的代码中看到任何错误吗?
  • 您知道避免解决方案的原因吗?

  • FSTREAM(KISS,Streambuffer)
    我真的很喜欢这个,因为它确实很简短。据我所知,运算符<<对于rdbuf()已重载,并且不进行任何转换。正确?

谢谢

更新1
我以这种方式更改了所有示例中的源,即在clock()的度量中包括文件描述符的打开和关闭。它们在源代码中没有其他重大变化。结果没有改变!我还花时间仔细检查了结果。

更新2
ANSI C示例已更改:while循环的条件不再调用feof(),而是将fread()移入了条件。看起来,该代码现在运行速度快了10,000个时钟。

测量已更改:以前的结果总是被缓冲,因为我对每个程序重复了旧命令行rm to.ogv && sync && time ./program几次。现在,我为每个程序重新启动系统。无缓冲的结果是新的,并不令人惊讶。无缓冲的结果并没有真正改变。

如果我不删除旧副本,程序的反应会有所不同。使用POSIX和SENDFILE 覆盖已缓冲的现有文件更快,而其他所有程序则更慢。截断创建的选项可能对此行为有影响。但是,用相同的副本覆盖现有文件不是现实的用例。

使用cp执行复制需要0.44秒的未缓冲时间和0.30秒的缓冲时间。因此,cp比POSIX示例要慢一些。对我来说很好。

也许我还加样品及结果mmap()的,并copy_file()从boost ::文件系统。

更新3
我也将其放在博客页面上并对其进行了扩展。包括splice(),这是Linux内核中的底层函数。也许还会有更多的Java示例。 http://www.ttyhoney.com/blog/?page_id=69


5
fstream绝对是文件操作的一个不错的选择。
克里斯,2012年


28
您忘记了懒惰的方式:system(“ cp from.ogv to.ogv”);
fbafelipe 2012年

3
#include <copyfile.h> copyfile(const char *from, const char *to, copyfile_state_t state, copyfile_flags_t flags);
马丁·约克

3
很抱歉这么晚才开始,但是我不会将它们描述为“安全”的,因为它们没有任何错误处理。
理查德·基特韦尔

Answers:


258

以健全的方式复制文件:

#include <fstream>

int main()
{
    std::ifstream  src("from.ogv", std::ios::binary);
    std::ofstream  dst("to.ogv",   std::ios::binary);

    dst << src.rdbuf();
}

阅读起来如此简单直观,值得额外花费。如果我们做了很多事情,最好退回OS对文件系统的调用。我确定boost它的文件系统类中有一个复制文件方法。

有一种用于与文件系统进行交互的C方法:

#include <copyfile.h>

int
copyfile(const char *from, const char *to, copyfile_state_t state, copyfile_flags_t flags);

28
copyfile不便携;我认为它特定于Mac OSX。它在Linux上当然不存在。boost::filesystem::copy_file这可能是通过本机文件系统复制文件的最可移植的方法。
Mike Seymour 2012年

4
@MikeSeymour:copyfile()似乎是BSD扩展。
马丁·约克

10
@ duedl0r:否。对象具有析构函数。流的析构函数自动调用close()。codereview.stackexchange.com/q/540/507
马丁·约克

11
@ duedl0r:是的。但这就像在说“如果太阳下山”。您可以向西快速行驶,并且可能会使您的一天变得更长一点,但太阳将会落山。除非您有错误和泄漏内存(否则将超出范围)。但是由于这里没有动态内存管理,因此不会发生泄漏,它们将超出范围(就像太阳落山一样)。
马丁·约克

6
然后,只需把它包装在{}块范围
paulm

62

使用C ++ 17时,复制文件的标准方法是包括<filesystem>标题并使用:

bool copy_file( const std::filesystem::path& from,
                const std::filesystem::path& to);

bool copy_file( const std::filesystem::path& from,
                const std::filesystem::path& to,
                std::filesystem::copy_options options);

第一种形式与copy_options::none用作选项的第二种形式等效(另请参见copy_file)。

filesystem库最初boost.filesystem是从C ++ 17开始开发的,最终合并为ISO C ++。


2
为什么没有一个带有默认参数的函数,例如bool copy_file( const std::filesystem::path& from, const std::filesystem::path& to, std::filesystem::copy_options options = std::filesystem::copy_options::none);
Jepessen '17

2
@杰普森我对此不太确定。也许并不重要
manlio

@Jepessen在标准库中,干净的代码至关重要。具有重载(与具有默认参数的一个函数相反)使程序员的意图更加清晰。
Marc.2377'9

@Peter鉴于C ++ 17可用,现在应该应该是可接受的答案。
马丁·约克

21

太多!

“ ANSI C”方式缓冲区是多余的,因为a FILE已被缓冲。(此内部缓冲区的大小BUFSIZ实际上是定义的。)

“ OWN-BUFFER-C ++-WAY”执行过程会很慢,它会fstream执行大量的虚拟调度,并再次维护内部缓冲区或每个流对象。(“ COPY-ALGORITHM-C ++-WAY”不会受到此影响,因为streambuf_iterator该类会绕过流层。)

我更喜欢“ COPY-ALGORITHM-C ++-WAY”,但如果不构建fstream,只需std::filebuf在不需要实际格式时创建裸实例。

为了获得原始性能,您无法击败POSIX文件描述符。它很丑陋,但可在任何平台上快速移植。

Linux的方式似乎非常快-也许OS在I / O完成之前让函数返回?无论如何,对于许多应用程序来说,它的便携性不足。

编辑:啊,“本地Linux”可能通过将读取和写入与异步I / O交错来提高性能。让命令堆积起来可以帮助磁盘驱动程序确定何时最佳查找。您可以尝试使用Boost Asio或pthreads进行比较。至于“无法击败POSIX文件描述符”……那么,如果您要对数据做任何事情,而不仅仅是盲目地复制,那确实是正确的。


ANSI C:但是我必须给函数fread / fwrite一个大小?pubs.opengroup.org/onlinepubs/9699919799/toc.htm
Peter

@PeterWeber好吧,是的,确实BUFSIZ的价值和其他任何东西一样好,并且相对于一个或“几个”字符而言,可能会加快速度。无论如何,性能评估表明,在任何情况下它都不是最佳方法。
Potatoswatter 2012年

1
我对此没有深入的了解,因此我应该谨慎对待假设和观点。Linux-Way在Kernelspace afaik中运行。这应该避免在内核空间和用户空间之间进行缓慢的上下文切换吗?明天我将再次查看sendfile的联机帮助页。前一段时间,Linus Torvalds表示他不喜欢繁琐的用户空间文件系统。也许sendfile是他观点的一个积极例子?
彼得

5
sendfile()一个文件描述符与另一个之间拷贝数据,因为这种复制在内核中完成的,sendfile()比的组合更有效read(2)write(2),这需要数据传输和从用户空间。”: kernel.org/doc/man-pages /online/pages/man2/sendfile.2.html
Max Lybbert,2012年

1
您可以发表使用原始filebuf对象的示例吗?
Kerrek SB 2014年

14

我要特别指出的是,使用sendfile()的LINUX方法有一个主要问题,即它不能复制大小超过2GB的文件!我已在此问题之后实施了该程序,但遇到了问题,因为我正在使用它来复制大小为GB的HDF5文件。

http://man7.org/linux/man-pages/man2/sendfile.2.html

sendfile()将最多传输0x7ffff000(2,147,479,552)字节,返回实际传输的字节数。(在32位和64位系统上都是如此。)


1
sendfile64()是否有相同的问题?
graywolf

1
@Paladin似乎sendfile64就是为了解决这一限制而开发的。在手册页中:“”“原始的Linux sendfile()系统调用并非旨在处理大文件偏移。因此,Linux 2.4添加了sendfile64(),其中offset参数的类型更大。glibc sendfile()包装函数透明地处理内核差异。“”“
rveale '16

sendfile64似乎有相同的问题。但是,使用偏移类型off64_t允许人们使用循环来复制大文件,如对链接问题的答案所示。
pcworld

这在男人中很明显:'请注意,成功调用sendfile()可能会写入少于请求的字节数;如果有未发送的字节,则呼叫者应准备重试该呼叫。在完成完整复制之前,可能需要在循环内调用sendfile或sendfile64。
菲利普·哈迪

2

Qt有一种复制文件的方法:

#include <QFile>
QFile::copy("originalFile.example","copiedFile.example");

请注意,要使用此功能,您必须安装Qt此处的说明)并将其包含在您的项目中(如果您使用的是Windows并且您不是管理员,则可以在此处下载Qt )。另请参阅此答案


1
QFile::copy由于具有4k缓冲,因此速度非常慢。
Nicolas Holthaus

1
较慢的版本已修复Qt。我正在使用5.9.2,速度与本地实现相当。顺便说一句。看一下源代码,Qt似乎实际上调用了本机实现。
VK

1

对于那些喜欢助推器的人:

boost::filesystem::path mySourcePath("foo.bar");
boost::filesystem::path myTargetPath("bar.foo");

// Variant 1: Overwrite existing
boost::filesystem::copy_file(mySourcePath, myTargetPath, boost::filesystem::copy_option::overwrite_if_exists);

// Variant 2: Fail if exists
boost::filesystem::copy_file(mySourcePath, myTargetPath, boost::filesystem::copy_option::fail_if_exists);

注意,boost :: filesystem :: path也可用作Unicode的wpath。而且您还可以使用

using namespace boost::filesystem

如果您不喜欢那些长名称


Boost的文件系统库是要求对其进行编译的例外之一。仅供参考!
SimonC

0

我不太确定复制文件的“好方法”是什么,但是假设“好”的意思是“快速”,那么我可以稍微扩大一下范围。

长期以来,当前的操作系统已进行了优化,以处理工厂文件副本的运行。没有什么聪明的代码能胜过这一点。您的复制技术的某些变体在某些测试情况下可能会证明速度更快,但是在其他情况下,它们的表现最可能会更差。

通常,该sendfile函数可能在提交写入之前返回,因此给人的印象是比其他函数要快。我还没有阅读代码,但是最肯定的是因为它分配了自己的专用缓冲区,将内存用于时间交换。以及它不适用于大于2Gb的文件的原因。

只要您处理的文件数量很少,所有内容都将在各种缓冲区内发生(如果使用C ++运行时,则是​​第一次出现iostream在OS内部缓冲区中,对于来说,显然是文件大小的额外缓冲区sendfile)。实际的存储介质只有在移动了足够的数据后才值得访问,这值得您旋转硬盘。

我想您可以在特定情况下稍微提高性能。从我的头顶上:

  • 如果要在同一磁盘上复制一个大文件,则使用比操作系统更大的缓冲区可能会有所改善(但在这里我们可能谈论的是千兆字节)。
  • 如果要在两个不同的物理目标上复制同一文件,则一次打开三个文件可能比copy_file顺序调用两个文件更快(尽管只要文件适合操作系统缓存,您几乎不会注意到它们之间的区别)
  • 如果您要处理HDD上的许多微小文件,则可能需要分批读取它们,以最大程度地减少查找时间(尽管操作系统已经缓存了目录条目,以避免像疯狂的微小文件那样寻找文件,反而可能会极大地减少磁盘带宽)。

但是所有这些超出了通用文件复制功能的范围。

因此,以我经验丰富的程序员的观点,C ++文件副本应仅使用C ++ 17 file_copy专用功能,除非对该文件副本发生的上下文有更多了解,并且可以设计出一些巧妙的策略以胜过OS。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.