【问题标题】:Most efficient way to copy a file in Linux在 Linux 中复制文件的最有效方法
【发布时间】:2011-09-18 18:59:12
【问题描述】:

我在一个独立于操作系统的文件管理器中工作,我正在寻找为 Linux 复制文件的最有效方法。 Windows 有一个内置函数CopyFileEx(),但据我所知,Linux 没有这样的标准函数。所以我想我将不得不实现我自己的。 显而易见的方法是 fopen/fread/fwrite,但是有没有更好(更快)的方法呢?我还必须能够每隔一段时间停止一次,以便我可以更新文件进度菜单的“已复制”计数。

【问题讨论】:

  • 可能调用系统cp命令?
  • 太复杂了,我必须解析命令的输出来查找错误,并且不确定它是否有进度指示器。
  • 可能重复。看看stackoverflow.com/questions/3680730/… - 我想这就是你要问的。
  • CopyFileEx 有一个回调函数,每隔一段时间就会调用一次,它会更新目前复制的数量。
  • 另见 copy_file_range(2) 手册页。该函数需要GNU_SOURCE。但手册页还说,“copy_file_range() 系统调用最早出现在 Linux 4.5 中,但 glibc 2.27 在不可用时提供了用户空间模拟。”

标签: c linux


【解决方案1】:

很遗憾,您不能在此处使用sendfile(),因为目标不是套接字。 (名称sendfile()来自send()+“文件”)。

对于零拷贝,您可以按照@Dave 的建议使用splice()。 (除了它不会是零拷贝;它将是从源文件的页面缓存到目标文件的页面缓存的“一个副本”。)

但是... (a) splice() 是特定于 Linux 的;并且 (b) 几乎可以肯定,使用便携式接口也可以做到这一点,只要您正确使用它们。

简而言之,使用带有 small 临时缓冲区的 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 系统。

请注意,小缓冲区是这里的关键。典型的现代 CPU 有 32K 左右的 L1 数据缓存,因此如果缓冲区太大,这种方法会更慢。可能慢得多,慢得多。所以将缓冲区保持在“几千字节”范围内。

当然,除非您的磁盘子系统非常非常快,否则内存带宽可能不是您的限制因素。所以我建议posix_fadvise 让内核知道你在做什么:

posix_fadvise(in_fd, 0, 0, POSIX_FADV_SEQUENTIAL);

这将提示 Linux 内核,它的预读机制应该非常激进。

我还建议使用posix_fallocate 为目标文件预分配存储空间。这将提前告诉您是否会用完磁盘。对于具有现代文件系统(如 XFS)的现代内核,它将有助于减少目标文件中的碎片。

我最不推荐的是mmap。由于 TLB 抖动,这通常是最慢的方法。 (带有“透明大页”的最新内核可能会缓解这种情况;我最近没有尝试过。但它肯定曾经非常糟糕。所以如果你有很多时间进行基准测试和最近的内核,我只会费心测试mmap .)

[更新]

cmets 中有一些关于 splice 从一个文件到另一个文件是否是零拷贝的问题。 Linux 内核开发人员将此称为“页面窃取”。 splicecomments in the kernel source 的手册页都说 SPLICE_F_MOVE 标志应该提供此功能。

不幸的是,对SPLICE_F_MOVE 的支持是yanked in 2.6.21 (back in 2007),并且从未被替换。 (内核源代码中的 cmets 从未更新。)如果您搜索内核源代码,您会发现 SPLICE_F_MOVE 实际上并没有在任何地方引用。 last message I can find(从 2008 年开始)说它正在“等待替换”。

底线是splice从一个文件到另一个调用memcpy来移动数据;它不是零拷贝。这并不比您在用户空间中使用带有小缓冲区的read/write 做得更好,因此您不妨坚持使用标准的可移植接口。

如果“页面窃取”被重新添加到 Linux 内核中,那么splice 的好处会更大。 (即使在今天,当目标是一个套接字时,你也会得到真正的零拷贝,这让splice 更具吸引力。)但就这个问题而言,splice 并不买你。

【讨论】:

  • 我希望任何现代操作系统都能提前阅读您的要求。例如,我确信 Linux、Solaris 和各种 BSD 都可以……所以,是的,如果您的磁盘足够快以至于内存成为瓶颈,我很确定较小的块会更快。如果您无论如何都超过了磁盘,那么没关系。但是除非你的操作系统非常愚蠢,否则大块永远不会比小块快。优化现在和未来,而不是过去 :-)
  • @Gabe:好吧,你错了。我实际上已经对此进行了基准测试。 Linux 系统调用的速度足以摊销几千字节,因此系统调用成本是不可察觉的。另一方面,缓存和内存效应是非常容易检测到的。我不是在说百分之几。我说的是倍数。在现代系统中,局部性就是一切,每一代 CPU 都更是如此。
  • @Gabe:另外,锤击主内存会损害所有内核的性能,而现在你可能拥有不止一个内核。所以保持缓存友好——即在小块上操作——不仅对你的线程有好处,对你可能正在运行的任何其他线程也有好处。理论和我的经验都证实了这一点。当然,我可能会对我的经历撒谎。
  • 请注意 - 在 2.6.33 及更高版本中,out_fd 可以是任何文件。查看手册页。
  • @jww 我刚刚编辑了代码以从 assert 调用中删除具有副作用的代码。它仍然不“正确”,但至少它不再遭受那个错误。
【解决方案2】:

如果您知道他们将使用 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);

【讨论】:

  • 感谢您的建议,但我想让它尽可能便携。
  • @Radu - 在最近的 Linux 上,至少 splice() 是正确答案,因为sendfile() 不再支持文件->文件副本
  • splicesendfile 都不是标准化的。如果性能非常重要,请为每个环境编写函数的优化副本,然后从标准 c 退回到 fread/frwite。 glib 函数可能会做类似的事情。
  • 嗯,那么我想我必须有 3 个版本,一个使用打开/读/写,一个发送文件和一个拼接?哪个内核版本停止支持文件到文件的 sendfile()?
【解决方案3】:

使用open/read/write——它们避免了fopen和朋友们完成的libc级缓冲。

或者,如果您使用的是 GLib,则可以使用它的 g_copy_file 函数。

最后,什么可能更快,但应该进行测试以确保:使用openmmap 对输入文件进行内存映射,然后将write 从内存区域映射到输出文件。您可能希望保持打开/读取/写入作为后备,因为此方法受限于您的进程的地址空间大小。

编辑: 原始答案建议映射两个文件; @bdonlan 在评论中提出了极好的建议,只映射一张。

【讨论】:

  • 你能mmap()一个空文件吗?
  • 谢谢。我认为使用这里建议的 sendfile():stackoverflow.com/questions/3680730/… 会更好,因为它是在内核中完成的。
  • 实际上考虑到sendfile() 的复杂性以及输出是套接字的要求,看起来简单的答案实际上比我意识到的要多得多。
  • 我自己不会映射这两个文件 - 映射源,然后从映射区域映射 write()。无论哪种方式,您都必须复制它,所以不妨在内核中进行复制,避免目标页面错误。
  • 使用mmap 是个好主意,但您仍然需要分块进行写入才能显示进度。
【解决方案4】:

我从这篇文章的更新副本中得到的回答。

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

根据操作系统,您的里程可能会因splicesendfile 等系统调用而异,但请注意手册页中的 cmets:

在 sendfile() 因 EINVAL 或 ENOSYS 失败的情况下,应用程序可能希望回退到 read(2)/write(2)。

【讨论】:

    【解决方案5】:

    我编写了一些基准来测试这一点,发现copy_file_range 是最快的。否则,要么使用 128 KiB 缓冲区,要么对 src 数据使用只读 mmap,对 dest 数据使用 write 系统调用。

    文章:https://alexsaveau.dev/blog/performance/files/kernel/the-fastest-way-to-copy-a-file
    基准测试:https://github.com/SUPERCILEX/fuc/blob/fb0ec728dbd323f351d05e1d338b8f669e0d5b5d/cpz/benches/copy_methods.rs


    在链接断开时内联基准:

    use std::{
        alloc,
        alloc::Layout,
        fs::{copy, File, OpenOptions},
        io::{BufRead, BufReader, Read, Write},
        os::unix::{fs::FileExt, io::AsRawFd},
        path::{Path, PathBuf},
        thread,
        time::Duration,
    };
    
    use cache_size::l1_cache_size;
    use criterion::{
        criterion_group, criterion_main, measurement::WallTime, BatchSize, BenchmarkGroup, BenchmarkId,
        Criterion, Throughput,
    };
    use memmap2::{Mmap, MmapOptions};
    use rand::{thread_rng, RngCore};
    use tempfile::{tempdir, TempDir};
    
    // Don't use an OS backed tempfile since it might change the performance characteristics of our copy
    struct NormalTempFile {
        dir: TempDir,
        from: PathBuf,
        to: PathBuf,
    }
    
    impl NormalTempFile {
        fn create(bytes: usize, direct_io: bool) -> NormalTempFile {
            if direct_io && bytes % (1 << 12) != 0 {
                panic!("Num bytes ({}) must be divisible by 2^12", bytes);
            }
    
            let dir = tempdir().unwrap();
            let from = dir.path().join("from");
    
            let buf = create_random_buffer(bytes, direct_io);
    
            open_standard(&from, direct_io).write_all(&buf).unwrap();
    
            NormalTempFile {
                to: dir.path().join("to"),
                dir,
                from,
            }
        }
    }
    
    /// Doesn't use direct I/O, so files will be mem cached
    fn with_memcache(c: &mut Criterion) {
        let mut group = c.benchmark_group("with_memcache");
    
        for num_bytes in [1 << 10, 1 << 20, 1 << 25] {
            add_benches(&mut group, num_bytes, false);
        }
    }
    
    /// Use direct I/O to create the file to be copied so it's not cached initially
    fn initially_uncached(c: &mut Criterion) {
        let mut group = c.benchmark_group("initially_uncached");
    
        for num_bytes in [1 << 20] {
            add_benches(&mut group, num_bytes, true);
        }
    }
    
    fn empty_files(c: &mut Criterion) {
        let mut group = c.benchmark_group("empty_files");
    
        group.throughput(Throughput::Elements(1));
    
        group.bench_function("copy_file_range", |b| {
            b.iter_batched(
                || NormalTempFile::create(0, false),
                |files| {
                    // Uses the copy_file_range syscall on Linux
                    copy(files.from, files.to).unwrap();
                    files.dir
                },
                BatchSize::LargeInput,
            )
        });
    
        group.bench_function("open", |b| {
            b.iter_batched(
                || NormalTempFile::create(0, false),
                |files| {
                    File::create(files.to).unwrap();
    
                    files.dir
                },
                BatchSize::LargeInput,
            )
        });
    
        #[cfg(target_os = "linux")]
        group.bench_function("mknod", |b| {
            b.iter_batched(
                || NormalTempFile::create(0, false),
                |files| {
                    use nix::sys::stat::{mknod, Mode, SFlag};
                    mknod(files.to.as_path(), SFlag::S_IFREG, Mode::empty(), 0).unwrap();
    
                    files.dir
                },
                BatchSize::LargeInput,
            )
        });
    }
    
    fn just_writes(c: &mut Criterion) {
        let mut group = c.benchmark_group("just_writes");
    
        for num_bytes in [1 << 20] {
            group.throughput(Throughput::Bytes(num_bytes));
    
            group.bench_with_input(
                BenchmarkId::new("open_memcache", num_bytes),
                &num_bytes,
                |b, num_bytes| {
                    b.iter_batched(
                        || {
                            let dir = tempdir().unwrap();
                            let buf = create_random_buffer(*num_bytes as usize, false);
    
                            (dir, buf)
                        },
                        |(dir, buf)| {
                            File::create(dir.path().join("file"))
                                .unwrap()
                                .write_all(&buf)
                                .unwrap();
    
                            (dir, buf)
                        },
                        BatchSize::PerIteration,
                    )
                },
            );
    
            group.bench_with_input(
                BenchmarkId::new("open_nocache", num_bytes),
                &num_bytes,
                |b, num_bytes| {
                    b.iter_batched(
                        || {
                            let dir = tempdir().unwrap();
                            let buf = create_random_buffer(*num_bytes as usize, true);
    
                            (dir, buf)
                        },
                        |(dir, buf)| {
                            let mut out = open_standard(dir.path().join("file").as_ref(), true);
                            out.set_len(*num_bytes).unwrap();
    
                            out.write_all(&buf).unwrap();
    
                            (dir, buf)
                        },
                        BatchSize::PerIteration,
                    )
                },
            );
        }
    }
    
    fn add_benches(group: &mut BenchmarkGroup<WallTime>, num_bytes: u64, direct_io: bool) {
        group.throughput(Throughput::Bytes(num_bytes));
    
        group.bench_with_input(
            BenchmarkId::new("copy_file_range", num_bytes),
            &num_bytes,
            |b, num_bytes| {
                b.iter_batched(
                    || NormalTempFile::create(*num_bytes as usize, direct_io),
                    |files| {
                        // Uses the copy_file_range syscall on Linux
                        copy(files.from, files.to).unwrap();
                        files.dir
                    },
                    BatchSize::PerIteration,
                )
            },
        );
    
        group.bench_with_input(
            BenchmarkId::new("buffered", num_bytes),
            &num_bytes,
            |b, num_bytes| {
                b.iter_batched(
                    || NormalTempFile::create(*num_bytes as usize, direct_io),
                    |files| {
                        let reader = BufReader::new(File::open(files.from).unwrap());
                        write_from_buffer(files.to, reader);
                        files.dir
                    },
                    BatchSize::PerIteration,
                )
            },
        );
    
        group.bench_with_input(
            BenchmarkId::new("buffered_l1_tuned", num_bytes),
            &num_bytes,
            |b, num_bytes| {
                b.iter_batched(
                    || NormalTempFile::create(*num_bytes as usize, direct_io),
                    |files| {
                        let l1_cache_size = l1_cache_size().unwrap();
                        let reader =
                            BufReader::with_capacity(l1_cache_size, File::open(files.from).unwrap());
    
                        write_from_buffer(files.to, reader);
    
                        files.dir
                    },
                    BatchSize::PerIteration,
                )
            },
        );
    
        group.bench_with_input(
            BenchmarkId::new("buffered_readahead_tuned", num_bytes),
            &num_bytes,
            |b, num_bytes| {
                b.iter_batched(
                    || NormalTempFile::create(*num_bytes as usize, direct_io),
                    |files| {
                        let readahead_size = 1 << 17; // See https://eklitzke.org/efficient-file-copying-on-linux
                        let reader =
                            BufReader::with_capacity(readahead_size, File::open(files.from).unwrap());
    
                        write_from_buffer(files.to, reader);
    
                        files.dir
                    },
                    BatchSize::PerIteration,
                )
            },
        );
    
        group.bench_with_input(
            BenchmarkId::new("buffered_parallel", num_bytes),
            &num_bytes,
            |b, num_bytes| {
                b.iter_batched(
                    || NormalTempFile::create(*num_bytes as usize, direct_io),
                    |files| {
                        let threads = num_cpus::get() as u64;
                        let chunk_size = num_bytes / threads;
    
                        let from = File::open(files.from).unwrap();
                        let to = File::create(files.to).unwrap();
                        advise(&from);
                        to.set_len(*num_bytes).unwrap();
    
                        let mut results = Vec::with_capacity(threads as usize);
                        for i in 0..threads {
                            let from = from.try_clone().unwrap();
                            let to = to.try_clone().unwrap();
    
                            results.push(thread::spawn(move || {
                                let mut buf = Vec::with_capacity(chunk_size as usize);
                                // We write those bytes immediately after and dropping u8s does nothing
                                #[allow(clippy::uninit_vec)]
                                unsafe {
                                    buf.set_len(chunk_size as usize);
                                }
    
                                from.read_exact_at(&mut buf, i * chunk_size).unwrap();
                                to.write_all_at(&buf, i * chunk_size).unwrap();
                            }));
                        }
                        for handle in results {
                            handle.join().unwrap();
                        }
    
                        files.dir
                    },
                    BatchSize::PerIteration,
                )
            },
        );
    
        group.bench_with_input(
            BenchmarkId::new("buffered_entire_file", num_bytes),
            &num_bytes,
            |b, num_bytes| {
                b.iter_batched(
                    || NormalTempFile::create(*num_bytes as usize, direct_io),
                    |files| {
                        let mut from = File::open(files.from).unwrap();
                        let mut to = File::create(files.to).unwrap();
                        advise(&from);
                        to.set_len(*num_bytes).unwrap();
    
                        let mut buf = Vec::with_capacity(*num_bytes as usize);
                        from.read_to_end(&mut buf).unwrap();
                        to.write_all(&buf).unwrap();
    
                        files.dir
                    },
                    BatchSize::PerIteration,
                )
            },
        );
    
        group.bench_with_input(
            BenchmarkId::new("mmap_read_only", num_bytes),
            &num_bytes,
            |b, num_bytes| {
                b.iter_batched(
                    || NormalTempFile::create(*num_bytes as usize, direct_io),
                    |files| {
                        let from = File::open(files.from).unwrap();
                        let reader = unsafe { Mmap::map(&from) }.unwrap();
                        let mut to = File::create(files.to).unwrap();
                        advise(&from);
    
                        to.write_all(reader.as_ref()).unwrap();
    
                        files.dir
                    },
                    BatchSize::PerIteration,
                )
            },
        );
    
        group.bench_with_input(
            BenchmarkId::new("mmap_read_only_truncate", num_bytes),
            &num_bytes,
            |b, num_bytes| {
                b.iter_batched(
                    || NormalTempFile::create(*num_bytes as usize, direct_io),
                    |files| {
                        let from = File::open(files.from).unwrap();
                        let reader = unsafe { Mmap::map(&from) }.unwrap();
                        let mut to = File::create(files.to).unwrap();
                        advise(&from);
                        to.set_len(*num_bytes).unwrap();
    
                        to.write_all(reader.as_ref()).unwrap();
    
                        files.dir
                    },
                    BatchSize::PerIteration,
                )
            },
        );
    
        #[cfg(target_os = "linux")]
        group.bench_with_input(
            BenchmarkId::new("mmap_read_only_fallocate", num_bytes),
            &num_bytes,
            |b, num_bytes| {
                b.iter_batched(
                    || NormalTempFile::create(*num_bytes as usize, direct_io),
                    |files| {
                        let from = File::open(files.from).unwrap();
                        let reader = unsafe { Mmap::map(&from) }.unwrap();
                        let mut to = File::create(files.to).unwrap();
                        advise(&from);
                        allocate(&to, *num_bytes);
    
                        to.write_all(reader.as_ref()).unwrap();
    
                        files.dir
                    },
                    BatchSize::PerIteration,
                )
            },
        );
    
        group.bench_with_input(
            BenchmarkId::new("mmap_rw_truncate", num_bytes),
            &num_bytes,
            |b, num_bytes| {
                b.iter_batched(
                    || NormalTempFile::create(*num_bytes as usize, direct_io),
                    |files| {
                        let from = File::open(files.from).unwrap();
                        let to = OpenOptions::new()
                            .read(true)
                            .write(true)
                            .create(true)
                            .open(files.to)
                            .unwrap();
                        to.set_len(*num_bytes).unwrap();
                        advise(&from);
                        let reader = unsafe { Mmap::map(&from) }.unwrap();
                        let mut writer = unsafe { MmapOptions::new().map_mut(&to) }.unwrap();
    
                        writer.copy_from_slice(reader.as_ref());
    
                        files.dir
                    },
                    BatchSize::PerIteration,
                )
            },
        );
    }
    
    fn open_standard(path: &Path, direct_io: bool) -> File {
        let mut options = OpenOptions::new();
        options.write(true).create(true).truncate(true);
    
        #[cfg(target_os = "linux")]
        if direct_io {
            use nix::libc::O_DIRECT;
            use std::os::unix::fs::OpenOptionsExt;
            options.custom_flags(O_DIRECT);
        }
    
        let file = options.open(path).unwrap();
    
        #[cfg(target_os = "macos")]
        if direct_io {
            use nix::{
                errno::Errno,
                libc::{fcntl, F_NOCACHE},
            };
            Errno::result(unsafe { fcntl(file.as_raw_fd(), F_NOCACHE) }).unwrap();
        }
    
        file
    }
    
    fn write_from_buffer(to: PathBuf, mut reader: BufReader<File>) {
        advise(reader.get_ref());
        let mut to = File::create(to).unwrap();
        to.set_len(reader.get_ref().metadata().unwrap().len())
            .unwrap();
    
        loop {
            let len = {
                let buf = reader.fill_buf().unwrap();
                if buf.is_empty() {
                    break;
                }
    
                to.write_all(buf).unwrap();
                buf.len()
            };
            reader.consume(len)
        }
    }
    
    #[cfg(target_os = "linux")]
    fn allocate(file: &File, len: u64) {
        use nix::{
            fcntl::{fallocate, FallocateFlags},
            libc::off_t,
        };
        fallocate(file.as_raw_fd(), FallocateFlags::empty(), 0, len as off_t).unwrap();
    }
    
    fn advise(_file: &File) {
        // Interestingly enough, this either had no effect on performance or made it slightly worse.
        // posix_fadvise(file.as_raw_fd(), 0, 0, POSIX_FADV_SEQUENTIAL).unwrap();
    }
    
    fn create_random_buffer(bytes: usize, direct_io: bool) -> Vec<u8> {
        let mut buf = if direct_io {
            let layout = Layout::from_size_align(bytes, 1 << 12).unwrap();
            let ptr = unsafe { alloc::alloc(layout) };
            unsafe { Vec::<u8>::from_raw_parts(ptr, bytes, bytes) }
        } else {
            let mut v = Vec::with_capacity(bytes);
            // We write those bytes immediately after and dropping u8s does nothing
            #[allow(clippy::uninit_vec)]
            unsafe {
                v.set_len(bytes);
            }
            v
        };
        thread_rng().fill_bytes(buf.as_mut_slice());
        buf
    }
    
    criterion_group! {
        name = benches;
        config = Criterion::default().noise_threshold(0.02).warm_up_time(Duration::from_secs(1));
        targets =
        with_memcache,
        initially_uncached,
        empty_files,
        just_writes,
    }
    criterion_main!(benches);
    

    【讨论】:

      【解决方案6】:

      您可能想要对 dd 命令进行基准测试

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2020-04-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-07-02
        • 2013-01-21
        相关资源
        最近更新 更多