以理智、安全和有效的方式复制文件

Posted

技术标签:

【中文标题】以理智、安全和有效的方式复制文件【英文标题】:Copy a file in a sane, safe and efficient way 【发布时间】:2012-04-29 00:50:57 【问题描述】:

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

我错过了很好的例子并寻找一种适用于 C++ 的方法。

ANSI-C-WAY

#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++-Streambuffer-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);

    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-VIDEO-FILE

复制步骤

 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,流缓冲区) 我真的很喜欢这个,因为它真的很短很简单。据我所知,运算符

谢谢

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

更新 2 ANSI C 示例更改:while-loop 的条件不再调用 feof() 而是我将 fread() 移到健康)状况。看起来,代码现在运行速度快了 10,000 个时钟。

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

如果我不删除旧副本,程序的反应会有所不同。使用 POSIX 和 SENDFILE 覆盖现有文件 缓冲 更快,所有其他程序都更慢。也许选项 truncatecreate 对此行为有影响。但是用相同的副本覆盖现有文件并不是现实世界的用例。

使用 cp 执行复制需要 0.44 秒无缓冲和 0.30 秒缓冲。所以 cp 比 POSIX 示例慢一点。对我来说看起来不错。

也许我还添加了来自 boost::filesystem 的 mmap()copy_file() 的样本和结果。

更新 3 我也把它放在了一个博客页面上,并对其进行了一些扩展。包括 splice(),这是一个来自 Linux 内核的低级函数。也许会有更多的 Java 示例。 http://www.ttyhoney.com/blog/?page_id=69

【问题讨论】:

fstream 绝对是文件操作的好选择。 richelbilderbeek.nl/CppCopy_file.htm 你忘了偷懒方式:system("cp from.ogv to.ogv"); #include &lt;copyfile.h&gt; copyfile(const char *from, const char *to, copyfile_state_t state, copyfile_flags_t flags); 很抱歉这么晚才加入,但我认为这些都不是“安全的”,因为它们没有任何错误处理。 【参考方案1】:

以合理的方式复制文件:

#include <fstream>

int main()

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

    dst << src.rdbuf();

阅读起来非常简单直观,值得额外付费。如果我们经常这样做,最好依靠操作系统对文件系统的调用。我确信boost 在其文件系统类中有一个复制文件方法。

有一种与文件系统交互的C方法:

#include <copyfile.h>

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

【讨论】:

copyfile 不可移植;我认为它是 Mac OS X 特有的。它在 Linux 上肯定不存在。 boost::filesystem::copy_file 可能是通过本机文件系统复制文件的最便携方式。 @MikeSeymour: copyfile() 似乎是一个 BSD 扩展。 @duedl0r:不,对象有析构函数。流的析构函数自动调用 close()。 codereview.stackexchange.com/q/540/507 @duedl0r:是的。但这就像在说“如果太阳下山”。你可以跑得非常快,你可以让你的一天稍微长一点,但太阳要下山了。除非您有错误和泄漏内存(它将超出范围)。但是由于这里没有动态内存管理,所以不会有泄漏,它们会超出范围(就像太阳下山一样)。 然后简单地将其包装在一个 范围块中【参考方案2】:

使用 C++17 复制文件的标准方法是包含 &lt;filesystem&gt; 标头并使用:

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++。

【讨论】:

为什么没有一个带有默认参数的函数,比如bool copy_file( const std::filesystem::path&amp; from, const std::filesystem::path&amp; to, std::filesystem::copy_options options = std::filesystem::copy_options::none); @Jepessen 我不确定。也许是doesn't really matter。 @Jepessen 在标准库中,干净的代码是最重要的。具有重载(与一个具有默认参数的函数相反)使程序员的意图更加清晰。 @Peter 鉴于 C++17 可用,这现在应该是公认的答案。【参考方案3】:

太多了!

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

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

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

对于原始性能,您无法击败 POSIX 文件描述符。它很丑,但在任何平台上都可移植且快速。

Linux 的方式似乎非常快——也许操作系统让函数在 I/O 完成之前返回?无论如何,这对于许多应用程序来说都不够便携。

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

【讨论】:

ANSI C:但我必须给函数 fread/fwrite 一个大小? pubs.opengroup.org/onlinepubs/9699919799/toc.htm @PeterWeber 嗯,是的,确实 BUFSIZ 与任何值一样好,并且可能会一次相对于一个或“仅几个”字符加快速度。无论如何,性能测量证明它在任何情况下都不是最好的方法。 我对此没有深入的了解,所以我应该小心假设和意见。 Linux-Way 在内核空间 afaik 中运行。这应该避免内核空间和用户空间之间的缓慢上下文切换?明天我会再看一下 sendfile 的手册页。不久前,Linus Torvalds 说他不喜欢用户空间文件系统来处理繁重的工作。也许 sendfile 是他观点的一个正面例子? "sendfile() 在一个文件描述符和另一个文件描述符之间复制数据。因为这种复制是在内核中完成的,所以sendfile()read(2)write(2) 的组合更有效,后者将需要在用户空间之间传输数据。”:kernel.org/doc/man-pages/online/pages/man2/sendfile.2.html 你能发布一个使用原始filebuf对象的例子吗?【参考方案4】:

我想让非常重要的一点是,使用 sendfile() 的 LINUX 方法的一个主要问题是它不能复制超过 2GB 的文件!我在这个问题之后实现了它并且遇到了问题,因为我正在使用它来复制许多 GB 大小的 HDF5 文件。

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

sendfile() 最多传输 0x7ffff000 (2,147,479,552) 个字节, 返回实际传输的字节数。 (这是真的 32 位和 64 位系统。)

【讨论】:

sendfile64() 有同样的问题吗? @Paladin 似乎 sendfile64 是为了解决这个限制而开发的。从手册页:“”“最初的 Linux sendfile() 系统调用不是为处理大文件偏移而设计的。因此,Linux 2.4 添加了 sendfile64(),为 offset 参数提供了更广泛的类型。glibc sendfile() 包装函数透明地处理内核差异。""" sendfile64 has the same issue 似乎。但是,使用偏移量类型off64_t 允许使用循环将大文件复制到链接问题中,如in an answer 所示。 这是在 man 中写入的:'请注意,成功调用 sendfile() 可能写入的字节数少于请求的字节数;如果有未发送的字节,调用者应该准备好重试调用。 sendfile 或 sendfile64 可能需要在循环中调用,直到完整复制完成。【参考方案5】:

Qt有复制文件的方法:

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

请注意,要使用它,您必须install Qt(说明here)并将其包含在您的项目中(如果您使用的是Windows并且您不是管理员,则可以下载Qt here) .另见this answer。

【讨论】:

QFile::copy 非常慢,因为它是4k buffering。 在较新版本的Qt 中已修复缓慢问题。我正在使用5.9.2,速度与本机实现相当。顺便提一句。看一下源码,Qt好像真的调用了native实现。【参考方案6】:

对于喜欢提升的人:

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 的文件系统库是需要编译的例外之一。仅供参考!【参考方案7】:

我不太确定复制文件的“好方法”是什么,但假设“好”意味着“快”,我可以稍微拓宽一下主题。

长期以来,当前的操作系统已针对处理磨机文件副本的运行进行了优化。没有任何聪明的代码可以打败它。您的复制技术的某些变体可能会在某些测试场景中被证明更快,但在其他情况下它们很可能会变得更糟。

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

只要您处理少量文件,所有内容都会发生在各种缓冲区中(如果您使用 iostream,C++ 运行时是第一个,操作系统内部的,显然是文件大小的额外缓冲区,如果sendfile)。实际存储介质只有在移动了足够多的数据以值得旋转硬盘的麻烦时才能访问。

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

如果您要在同一个磁盘上复制一个大文件,使用比操作系统更大的缓冲区可能会稍微改善一些情况(但我们在这里可能谈论的是千兆字节)。 如果您想将同一个文件复制到两个不同的物理目标上,您可能会比同时打开三个文件更快,而不是依次调用两个copy_file(尽管只要文件适合,您几乎不会注意到差异操作系统缓存) 如果您正在处理 HDD 上的大量小文件,您可能希望分批读取它们以最大限度地减少查找时间(尽管操作系统已经缓存了目录条目以避免疯狂查找,并且小文件可能会显着降低磁盘带宽反正)。

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

因此,在我可以说是经验丰富的程序员看来,C++ 文件复制应该只使用 C++17 file_copy 专用函数,除非对文件复制发生的上下文有更多了解,并且可以设计一些聪明的策略来超越操作系统。

【讨论】:

以上是关于以理智、安全和有效的方式复制文件的主要内容,如果未能解决你的问题,请参考以下文章

以编程方式复制和粘贴文件

有效获取数组子集

如何以异步方式有效地将变量从 Matlab 传递到 GPU?

如何以编程方式复制包含的文件

如何以依赖感知方式复制文件,同时考虑存在和时间戳

在 Windows 中以编程方式复制、重命名和粘贴文件的最佳方法