以明智、安全和高效的方式复制文件

345

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

我缺少好的示例,并且正在寻找一种适用于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;
}

复制算法C++实现方法

#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;
}

自有缓冲区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);

    // 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)
  • Kernel 3.3
  • GLIBC-2.15,LIBSTDC++4.7(GCC-LIBS),GCC 4.7,Coreutils8.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,Streambuffer)
    我真的很喜欢这个方法,因为它非常简短和简单。据我所知,运算符<<被重载为rdbuf(),不会转换任何内容。正确吗?

谢谢

更新1
我已经按照这种方式更改了所有示例中的源代码,即将文件描述符的打开和关闭包含在clock()的测量中。其它方面没有显著的源代码改动。结果没有改变!我还使用time再次检查我的结果。

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

测量已更改:以前的结果总是缓冲的,因为我为每个程序重复了旧的命令行rm to.ogv && sync && time ./program。现在我重新启动系统运行每个程序。未缓冲的结果是新的,并且没有什么意外。未缓冲的结果并没有真正改变。

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

使用cp进行复制需要0.44秒未缓冲和0.30秒缓冲。因此,cp比POSIX示例略慢。对我来说看起来很好。

也许我还会添加使用mmap()copy_file()从boost::filesystem的样例和结果。

更新3
我将其放在博客页面上并进行了扩展。包括splice(),它是Linux内核的低级函数。可能会有更多使用Java的示例。 http://www.ttyhoney.com/blog/?page_id=69


5
fstream 绝对是进行文件操作的良好选择。 - chris
36
你忘记了一种懒人的方式:system("cp from.ogv to.ogv"); - fbafelipe
3
#include <copyfile.h> 函数copyfile(const char *from, const char *to, copyfile_state_t state, copyfile_flags_t flags)用于将文件从一个位置复制到另一个位置,其中from参数是源文件的路径,to参数是目标文件的路径,state参数是指向copyfile_state_t类型结构体的指针,该结构体存储了复制进度和其他信息,flags参数是控制复制行为的一组标志。 - Martin York
1
博客页面已经不存在了。你的文章还能在其他地方找到吗? - ardnew
5
很抱歉我来晚了,但是我认为这些都不属于“安全”,因为它们没有任何错误处理。 - Richard Kettlewell
显示剩余18条评论
9个回答

296

以合理的方式复制文件:

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

34
copyfile不可移植,我认为它只适用于Mac OS X系统,在Linux系统中并不存在。使用boost::filesystem::copy_file函数是最通用的复制文件到本地文件系统的方法。 - Mike Seymour
4
src.close(); dst.close(); ? - duedl0r
12
不。对象有析构函数。流的析构函数会自动调用close()函数。 - Martin York
11
@duedl0r说:“是的。但这就像在说“如果太阳落山”。你可以向西跑得很快,也许会稍微延长你的一天,但太阳还是会落山。除非你有Bug和泄漏内存(它将超出范围)。但由于这里没有动态内存管理,因此不可能发生泄漏,它们将超出范围(就像太阳会落山一样)。” - Martin York
7
那么,只需将其放入一个{ }范围块中即可。 - paulm
显示剩余25条评论

86

使用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
2
@Jepessen 我不确定这个。也许它并不重要 - manlio
1
在标准库中,干净的代码至关重要。使用重载(而不是带有默认参数的一个函数)可以使程序员的意图更加清晰明了。 - Marc.2377
1
@Peter,鉴于C++17已经可用,现在这可能应该成为被接受的答案。 - Martin York

21

太多了!

"ANSI C"的方式是冗余的,因为FILE已经有缓冲区了。(这个内部缓冲区的大小就是BUFSIZ所定义的。)

"OWN-BUFFER-C++-WAY"会很慢,因为它通过fstream进行了许多虚拟分派,并且还维护每个流对象的内部缓冲区。(而"COPY-ALGORITHM-C++-WAY"则不会受此影响,因为streambuf_iterator类绕过了流层。)

我更喜欢"COPY-ALGORITHM-C++-WAY",但在不需要实际格式化时,只需创建裸的std::filebuf实例即可。

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

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

编辑: 啊,“本地Linux”可能会通过异步I/O交错读写以提高性能。让命令积攒起来可以帮助磁盘驱动器决定何时最好进行寻址。你可以尝试使用Boost Asio或pthread进行比较。至于“无法击败POSIX文件描述符”...嗯,如果你对数据进行了任何操作而不仅仅是盲目复制,那么这是真的。


ANSI C:但我必须给函数fread/fwrite一个大小吗?http://pubs.opengroup.org/onlinepubs/9699919799/toc.htm - Peter
@PeterWeber 嗯,是的,BUFSIZ确实是一个不错的值,相对于一次或“仅有几个”字符,它可能会加快速度。无论如何,性能测量表明,在任何情况下都不是最佳方法。 - Potatoswatter
1
我对此没有深入的了解,所以在做出假设和意见时应该小心。据我了解,Linux-Way 在内核空间运行。这应该可以避免内核空间和用户空间之间的慢速上下文切换?明天我会再次查看 sendfile 的 manpage。一段时间以前,Linus Torvalds 表示他不喜欢适合繁重任务的用户空间文件系统。也许 sendfile 是他观点的一个积极例子? - Peter
6
sendfile()函数在两个文件描述符之间复制数据。由于该复制是在内核中完成的,因此相比于需要在用户空间中传输数据的read(2)write(2)组合,sendfile()更为高效。 - Max Lybbert
1
请您提供一下使用原始filebuf对象的示例吗? - Kerrek SB

18

我想要强调一个非常重要的问题,使用LINUX中的sendfile()方法存在一个主要问题,即无法复制超过2GB大小的文件!我按照这个问题实施了它,并因为我使用它来复制多GB大小的HDF5文件而遇到问题。

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

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


2
sendfile64()是否也有同样的问题? - graywolf
3
看起来sendfile64是为了解决这个限制而开发的。从手册上看:"""原始的Linux sendfile()系统调用无法处理大文件偏移量。因此,Linux 2.4添加了sendfile64(),其中偏移参数具有更宽的类型。glibc的sendfile()包装函数透明地处理内核之间的差异。""" - rveale
2
sendfile64似乎存在相同的问题。然而,使用偏移类型off64_t可以允许我们像链接问题中的答案所示那样使用循环来复制大文件。 - pcworld
请注意,成功调用sendfile()可能会写入比请求的字节数少的字节;如果有未发送的字节,调用者应准备重试调用。sendfile或sendfile64可能需要在循环内调用,直到完全复制完成。 - philippe lhardy

3

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

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

请注意,使用此功能需要 安装Qt(此处有说明),并将其包含在项目中(如果您正在使用Windows且不是管理员,则可以在此下载Qt 这里)。还可以参见此答案

1
由于其4K缓冲区,QFile ::copy速度非常慢。 - Nicolas Holthaus
1
Qt 的新版本已经解决了速度慢的问题。我正在使用 5.9.2 版本,速度与本地实现相当。顺便说一句,查看源代码后,Qt 实际上调用了本地实现。 - HiFile.app - best file manager

2

对于喜欢boost的人:

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

1
我不确定“好的方式”指的是什么,但假设“好”的意思是“快”,我可以稍微扩大一下话题。
当前操作系统早已针对普通文件复制进行了优化。没有什么聪明的代码能够超越它。也许你的某种复制技术变体在某些测试场景中会被证明更快,但很可能在其他情况下表现更差。
通常,sendfile函数可能会在写入提交之前返回,从而给人留下比其余方法更快的印象。我没有看过代码,但它肯定是因为它分配了自己的专用缓冲区,用时间换取内存。这也是为什么它无法处理大于2Gb的文件的原因。
只要你处理的文件数量较少,一切都发生在各种缓冲区内(如果使用iostream,则首先是C ++运行时的缓冲区,然后是操作系统内部的缓冲区,显然,在使用sendfile的情况下还有一个额外的文件大小缓冲区)。实际存储介质仅在移动足够的数据以值得旋转硬盘的麻烦时才被访问。
我想你可以在特定情况下稍微提高性能。我能想到的是:
  • 如果您在同一磁盘上复制大文件,使用比操作系统更大的缓冲区可能会稍微提高速度(但我们可能在谈论千兆字节级别)。
  • 如果您想将相同的文件复制到两个不同的物理目标上,同时打开三个文件可能比连续调用两个copy_file更快(尽管只要文件适合于操作系统缓存,您几乎不会注意到差异)。
  • 如果您正在处理HDD上的许多小文件,则可能希望批量读取它们以最小化寻道时间(尽管操作系统已经缓存了目录条目以避免像疯子一样寻找,并且小文件可能会显着降低磁盘带宽)。

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

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


1

在C++17及以上版本中,最简单的方法是:

使用#include <filesystem>copy()方法。该方法有4个重载。您可以在此链接中进行查看。

void copy( const std::filesystem::path& from,

           const std::filesystem::path& to );
void copy( const std::filesystem::path& from,
           const std::filesystem::path& to,
           std::error_code& ec );
    
void copy( const std::filesystem::path& from,

           const std::filesystem::path& to,
           std::filesystem::copy_options options );
           
void copy( const std::filesystem::path& from,
           const std::filesystem::path& to,
           std::filesystem::copy_options options,
           std::error_code& ec );

使用 copy() 方法可以复制文件和目录,并提供一些选项,如递归、非递归、仅复制目录或覆盖或跳过现有文件等。您可以在此链接中了解更多有关复制选项的信息。
这是一个来自此处的示例代码,并进行了一些编辑:
#include <cstdlib>
#include <iostream>
#include <fstream>
#include <filesystem>
namespace fs = std::filesystem;
 
int main()
{
    // create directories. create all directories if not exist. 
    fs::create_directories("sandbox/dir/subdir");
    
    // create file with content 'a'
    std::ofstream("sandbox/file1.txt").put('a');
    
    // copy file
    fs::copy("sandbox/file1.txt", "sandbox/file2.txt");
    
    // copy directory (non-recursive)
    fs::copy("sandbox/dir", "sandbox/dir2"); 
    
    // copy directory (recursive)
    const auto copyOptions = fs::copy_options::update_existing
                           | fs::copy_options::recursive
                           ;
    fs::copy("sandbox", "sandbox_copy", copyOptions); 
    
    // remove sanbox directory and all sub directories and sub files.
    fs::remove_all("sandbox");
}

1

更新

在Unix平台上复制文件的最方便和高效的方法是使用sendfile系统调用,它内部使用内存映射以在内核模式下完全复制文件。请注意,sendfile一次只能复制2GB,因此我们应该在循环中使用它。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/sendfile.h>
#include <sys/stat.h>

int main(int argc, char **argv) {
    if (argc != 3) {
        fprintf(stderr, "usage: %s <source> <target>\n", argv[0]);
        return EXIT_FAILURE;
    }
    int source_fd = open(argv[1], O_RDONLY, 0);
    if (source_fd < 0) {
        perror("open source");
        return EXIT_FAILURE;
    }
    int target_fd = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0666);
    if (target_fd < 0) {
        perror("open target");
        return EXIT_FAILURE;
    }
    struct stat stat;
    int r = fstat(source_fd, &stat);
    if (r < 0) {
        perror("fstat");
        return EXIT_FAILURE;
    }
    off_t offset = 0;
    ssize_t bytes_sent = 0;
    ssize_t total_bytes_sent = 0;
    while (offset < stat.st_size) {
        bytes_sent = sendfile(target_fd, source_fd, &offset, stat.st_size - offset);
        total_bytes_sent += bytes_sent;
        if (bytes_sent < 0) {
            perror("sendfile");
            return EXIT_FAILURE;
        }
    }
    if (total_bytes_sent != stat.st_size) {
        fprintf(stderr, "sendfile: copied file truncated to %zd bytes\n", bytes_sent);
        return EXIT_FAILURE;
    } else {
        printf("sendfile: %zd bytes copied\n", total_bytes_sent);
    }
    close(source_fd);
    close(target_fd);
    return EXIT_SUCCESS;
}

复制一个大约3.2GB的文件,所需时间为:

real    0m1.894s
user    0m0.000s
sys     0m1.880s

这是Python版本:

import sys
import os

if len(sys.argv) != 3:
    print(f'Usage: {sys.argv[0]} <source> <destination>')
    sys.exit(1)

with open(sys.argv[1], 'rb') as src, open(sys.argv[2], 'wb') as dst:
    total_bytes_sent = 0
    while total_bytes_sent < os.path.getsize(sys.argv[1]):
        bytes_sent = os.sendfile(dst.fileno(), src.fileno(), offset=None, count=2**31-1)
        total_bytes_sent += bytes_sent
    print(f"{total_bytes_sent} bytes written")

复制一个大约3.2GB的文件,所需时间为:

real    0m2.015s
user    0m0.010s
sys     0m1.973s

在Windows上复制文件的最方便和高效的方法是使用CopyFile API。 实时时间系统时间非常低。也许通过调用执行异步DMA的低级驱动程序函数进行了优化,因为当源驱动器和目标驱动器具有不同的文件系统,并且从较慢的USB 2.0 HDD复制文件时,系统时间没有完全记录。
#include <stdio.h>
#include <windows.h>

void PrintLastError(const char *name) {
    char *msg;
    FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (char *) &msg, 0, NULL);
    fprintf(stderr, "%s failed: %s", name, msg);
    LocalFree(msg);
    exit(EXIT_FAILURE);
}

int main(int argc, char *argv[]) {
    if (argc != 3) {
        printf("Usage: %s <source> <destination>\n", argv[0]);
        return 1;
    }
    if (!CopyFileA(argv[1], argv[2], TRUE)) {
        PrintLastError("CopyFile");
    }
    return EXIT_SUCCESS;
}

在本地SSD上复制一个大约3.2GB的文件,我的计时工具测量的时间使用情况为:

real    0.894460s
user    0.000000s
sys     0.734375s

从一个速度较慢的USB 2.0硬盘复制大约3.2GB的文件,使用我的计时工具测量的时间使用情况为:

real    90.149947s
user    0.015625s
sys     1.328125s

理论上,复制文件的最有效方法是使用内存映射,这样复制过程可以完全在内核模式下完成。
如果文件小于2GB,则可以在Unix平台上使用以下代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>

int main(int argc, char **argv) {
    if (argc != 3) {
        fprintf(stderr, "usage: %s <source> <target>\n", argv[0]);
        return EXIT_FAILURE;
    }
    int source_fd = open(argv[1], O_RDONLY, 0);
    if (source_fd < 0) {
        perror("open source");
        return EXIT_FAILURE;
    }
    int target_fd = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0666);
    if (target_fd < 0) {
        perror("open target");
        return EXIT_FAILURE;
    }
    struct stat stat;
    int r = fstat(source_fd, &stat);
    if (r < 0) {
        perror("fstat");
        return EXIT_FAILURE;
    }
    char *buf = mmap(NULL, stat.st_size, PROT_READ, MAP_PRIVATE, source_fd, 0);
    if (buf == MAP_FAILED) {
        perror("mmap");
        return EXIT_FAILURE;
    }
    r = write(target_fd, buf, stat.st_size);
    if (r < 0) {
        perror("write");
        return EXIT_FAILURE;
    } else if (r != stat.st_size) {
        fprintf(stderr, "write: copied file truncated to %d bytes\n", r);
        return EXIT_FAILURE;
    } else {
        printf("write: %d bytes copied\n", r);
    }
    munmap(buf, stat.st_size);
    close(source_fd);
    close(target_fd);
    return EXIT_SUCCESS;
}

复制一个大约2GB的文件,所需时间为:

real    0m1.457s
user    0m0.000s
sys     0m1.451s

但是如果文件大小大于2GB,write() 将截断文件到2GB,因此无法使用。我们必须映射目标文件并使用 memcpy 来复制文件。由于使用了 memcpy,因此我们可以看到用户模式下有时间消耗。
这里是通用版本:
import sys
import mmap

if len(sys.argv) != 3:
    print(f'Usage: {sys.argv[0]} <source> <destination>')
    sys.exit(1)

with open(sys.argv[1], 'rb') as src, open(sys.argv[2], 'wb') as dst:
    mmapped_src = mmap.mmap(src.fileno(), 0, access=mmap.ACCESS_READ)
    print(f"{dst.write(mmapped_src)} bytes written")
    mmapped_src.close()

复制一个大约3.2GB的文件,在Linux上的时间使用情况是:

real    0m2.050s
user    0m0.010s
sys     0m2.012s

复制一个大约3.2GB的文件,在Windows上使用我的计时工具测量的时间使用情况为:

real    3.520454s
user    0.031250s
sys     2.046875s

以下是Unix版本:

#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>

int main(int argc, char *argv[]) {
    int src_fd, dst_fd;
    void *src_map, *dst_map;
    struct stat src_stat;

    if (argc != 3) {
        printf("Usage: %s <source> <destination>\n", argv[0]);
        return 1;
    }

    src_fd = open(argv[1], O_RDONLY);
    if (src_fd == -1) {
        perror("open source");
        return 1;
    }

    if (fstat(src_fd, &src_stat) == -1) {
        perror("fstat");
        return 1;
    }

    src_map = mmap(NULL, src_stat.st_size, PROT_READ, MAP_PRIVATE, src_fd, 0);
    if (src_map == MAP_FAILED) {
        perror("mmap source");
        return 1;
    }

    dst_fd = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, src_stat.st_mode);
    if (dst_fd == -1) {
        perror("open destination");
        return 1;
    }

    if (ftruncate(dst_fd, src_stat.st_size) == -1) {
        perror("ftruncate");
        return 1;
    }

    dst_map = mmap(NULL, src_stat.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, dst_fd, 0);
    if (dst_map == MAP_FAILED) {
        perror("mmap destination");
        return 1;
    }

    memcpy(dst_map, src_map, src_stat.st_size);
    printf("Copied %ld bytes from %s to %s\n", src_stat.st_size, argv[1], argv[2]);

    munmap(src_map, src_stat.st_size);
    munmap(dst_map, src_stat.st_size);

    close(src_fd);
    close(dst_fd);

    return 0;
}

复制一个大约3.2GB的文件,所需时间是:

real    0m2.978s
user    0m0.639s
sys     0m2.325s

这里是 Windows 版本:

#include <stdio.h>
#include <windows.h>

void PrintLastError(const char *name) {
    char *msg;
    FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (char *) &msg, 0, NULL);
    fprintf(stderr, "%s failed: %s", name, msg);
    LocalFree(msg);
    exit(EXIT_FAILURE);
}

int main(int argc, char *argv[]) {
    HANDLE hSrc, hDst;
    HANDLE hSrcMap, hDstMap;
    LPVOID lpSrcMap, lpDstMap;
    DWORD dwSrcSize, dwDstSize;

    if (argc != 3) {
        printf("Usage: %s <source> <destination>\n", argv[0]);
        return 1;
    }

    hSrc = CreateFileA(argv[1], GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hSrc == INVALID_HANDLE_VALUE) {
        PrintLastError("CreateFile");
        return 1;
    }

    dwSrcSize = GetFileSize(hSrc, NULL);
    if (dwSrcSize == INVALID_FILE_SIZE) {
        PrintLastError("GetFileSize");
        goto SRC_MAP_FAIL;
    }

    hSrcMap = CreateFileMappingA(hSrc, NULL, PAGE_READONLY, 0, 0, NULL);
    if (hSrcMap == NULL) {
        PrintLastError("CreateFileMapping");
        goto SRC_MAP_FAIL;
    }

    lpSrcMap = MapViewOfFile(hSrcMap, FILE_MAP_READ, 0, 0, 0);
    if (lpSrcMap == NULL) {
        PrintLastError("MapViewOfFile");
        goto SRC_VIEW_FAIL;
    }

    hDst = CreateFileA(argv[2], GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hDst == INVALID_HANDLE_VALUE) {
        PrintLastError("CreateFile");
        goto DEST_OPEN_FAIL;
    }

    dwDstSize = dwSrcSize;
    hDstMap = CreateFileMappingA(hDst, NULL, PAGE_READWRITE, 0, dwDstSize, NULL);
    if (hDstMap == NULL) {
        PrintLastError("CreateFileMapping");
        goto DEST_MAP_FAIL;
    }

    lpDstMap = MapViewOfFile(hDstMap, FILE_MAP_WRITE, 0, 0, 0);
    if (lpDstMap == NULL) {
        PrintLastError("MapViewOfFile");
        goto DEST_VIEW_FAIL;
    }

    memcpy(lpDstMap, lpSrcMap, dwSrcSize);
    printf("Copied %lu bytes from %s to %s", dwSrcSize, argv[1], argv[2]);

    UnmapViewOfFile(lpDstMap);
DEST_VIEW_FAIL:
    CloseHandle(hDstMap);
DEST_MAP_FAIL:
    CloseHandle(hDst);
DEST_OPEN_FAIL:
    UnmapViewOfFile(lpSrcMap);
SRC_VIEW_FAIL:
    CloseHandle(hSrcMap);
SRC_MAP_FAIL:
    CloseHandle(hSrc);

    return 0;
}

复制一个大约3.2GB的文件,我的计时工具测量的时间使用情况为:

real    3.223017s
user    0.906250s
sys     2.312500s

你能解释一下为什么 write 在文件大于 2 GiB 时无法工作(假设 size_t 足够大)吗?另外,为什么不只是通过指定适当的偏移量将源文件分成小块(而不是使用 memcpy)来规避这个问题呢?(当然,如果在复制过程中修改了文件,则该版本会遇到竞争条件,但我认为对于 所有 版本都是如此:mmap 文件不会锁定它,对吧?——这样做可能通常是一个好主意) - Konrad Rudolph
也许 write(和 sendfile)在内部使用了一个32位有符号的 int,因此它能表示的最大数字是2GB。 - Kevin Tan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接