programing

Linux에서 파일을 복사하는 가장 효율적인 방법

itmemos 2023. 9. 24. 12:22
반응형

Linux에서 파일을 복사하는 가장 효율적인 방법

저는 OS 독립 파일 관리자에서 일하고 있으며 리눅스용 파일을 복사하는 가장 효율적인 방법을 찾고 있습니다.윈도우에는 CopyFileEx()라는 기능이 내장되어 있지만, 제가 인지한 바로는 리눅스용으로 그런 표준 기능은 없습니다.그래서 저는 저만의 것을 구현해야 할 것 같습니다.명백한 방법은 fopen/fread/fwrite인데, 더 나은 (빠른) 방법이 있을까요?파일 진행 메뉴에 대한 "지금까지 복사한" 카운트를 업데이트할 수 있도록 가끔 멈출 수 있는 기능도 있어야 합니다.

는 할 수 .sendfile()에 표시됩니다. (입니다.sendfile()msend()+ "파일")

영카피의 경우 다음을 사용할 수 있습니다.splice()@Dave가 제안한 대로입니다. (단, 제로 카피가 되지 않는 경우는 제외하고 소스 파일의 페이지 캐시에서 대상 파일의 페이지 캐시로 "한 개의 복사본"이 됩니다.)

하지만.. (a)splice()는 리눅스에 특화되어 있으며, (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 페이지 캐시에서 CPU L1 캐시로 8K를 복사한 다음 L1 캐시에서 out_fd 페이지 캐시로 씁니다.그런 다음 파일에서 다음 8K 청크로 L1 캐시의 해당 부분을 덮어씁니다. 것입니다.buf실제로 에 한 는 " 복사다외)를 시스템 RAM의 관점에서 이는 "제로 복사"를 사용하는 것과 마찬가지입니다.splice()은 어떤 할 수 있습니다 게다가 이것은 어떤 POSIX 시스템에서도 완벽하게 휴대할 수 있습니다.

여기서는 작은 버퍼가 핵심입니다.일반적인 최신 CPU는 L1 데이터 캐시에 대해 32K 정도를 가지고 있으므로 버퍼를 너무 크게 만들면 이 접근 방식이 느려집니다.아마 훨씬, 훨씬 더 느릴 겁니다.따라서 버퍼를 "수 킬로바이트" 범위로 유지합니다.

물론 디스크 서브시스템이 매우 빠르지 않는 한 메모리 대역폭은 제한 요소가 아닐 수도 있습니다.그래서 커널에 당신의 근황을 알려줄 것을 권합니다.

posix_fadvise(in_fd, 0, 0, POSIX_FADV_SEQUENTIAL);

이것은 리눅스 커널이 읽기 선행 기계가 매우 공격적이어야 한다는 것을 암시할 것입니다.

또한 대상 파일에 대한 저장소를 미리 할당하는 데 사용할 것을 제안합니다.이렇게 하면 디스크 부족 여부를 미리 알 수 있습니다.그리고 XFS와 같은 최신 파일 시스템을 갖춘 최신 커널의 경우 대상 파일의 조각화를 줄이는 데 도움이 될 것입니다.

은 은입니다.mmap 스레싱 한 페이지"를 할 수 해 본 적이 일반적으로 TLB 스레싱 덕분에 가장 느린 접근 방식입니다. ("투명한 거대한 페이지"가 있는 매우 최근의 커널은 이를 완화할 수 있습니다. 최근에는 시도하지 않았습니다.하지만 예전에는 확실히 매우 나빴습니다. 난 단지 만 할 .mmap벤치마크할 시간이 많고 매우 최근의 커널인 경우.)

[업데이트]

에 .splice한 파일에서 다른 파일로의 복사는 제로입니다.리눅스 커널 개발자들은 이것을 "페이지 도용"이라고 부릅니다. 모두 r니다.splice그리고 커널 소스에 있는 코멘트는 다음과 같이 말합니다.SPLICE_F_MOVE플래그가 이 기능을 제공해야 합니다.

에 대한 SPLICE_F_MOVE2.6.21(지난 2007년)에 강등되었고 교체되지 않았습니다.(커널 소스의 코멘트는 업데이트되지 않았습니다.)커널 소스를 검색하면 다음을 찾을 수 있을 것입니다.SPLICE_F_MOVE어디에서도 실제로 참조되지 않습니다.(2008년부터) 마지막으로 찾을 수 있는 메시지는 "교체를 기다리고 있다"는 것입니다.

그 .splicememcpy데이터를 이동합니다. 제로 복사가 아닙니다.이것은 사용자 공간에서 할 수 있는 것보다 훨씬 나은 것은 아닙니다.read/write작은 버퍼를 사용하기 때문에 표준 휴대용 인터페이스를 고수하는 것이 좋습니다.

""의 이 된다면됩니다. 합니다.splice 더 것입니다. (한 제로 , 에도,때,고,splice더 매력적입니다.)splice별로 돈을 벌지 못 사요.

만약 그들이 리눅스 > 2.6.17을 사용할 것이라는 것을 안다면,splice()입니다.

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

사용하다open/read/write libc다에서 수준 합니다.fopen친구들도 있고요.

인 GLib 에는 GLib 를 할 수 .g_copy_file기능.

마지막으로, 더 빠를 수도 있지만 확실하게 테스트해야 하는 것은 다음과 같습니다.open그리고.mmap입력 파일을 메모리 매핑한 다음write메모리 영역에서 출력 파일로 이동합니다.이 방법은 프로세스의 주소 공간 크기로 제한되므로 폴백으로 열려 있거나 읽거나 쓰고 싶을 수도 있습니다.

편집: 원래 답변에서 두 파일을 매핑할 것을 제안했습니다. @bdonlan은 하나만 매핑할 것을 제안했습니다.

이 게시물을 최근에 복제한 것에 대한 제 답변입니다.

지금 제안을 활성화합니다.mapped_file_source메모리 mapped 파일의 모델이 될 수 있습니다.

효율적이지 않을 수도 있습니다.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);
}

OS에 따라 마일리지는 스플라이스, 송신 파일 등의 시스템 호출에 따라 달라질 수 있지만 맨 페이지의 설명을 참고하십시오.

응용 프로그램은 EINVAL 또는 ENOSYS에서 sendfile()이 실패할 경우 read(2)/write(2)로 되돌아가기를 원할 수 있습니다.

이것을 테스트하기 위해 몇 가지 벤치마크를 작성했고,copy_file_range가장 빠른 속도로.그렇지 않으면 128 KiB 버퍼를 사용하거나 읽기 전용을 사용합니다.mmapsrc 데이터를 위해 그리고 사용합니다.write대상 데이터를 syscall합니다.

기사: https://alexsaveau.dev/blog/performance/files/커널/가장 빠른 복사-파일
벤치마크: 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);

dd 명령어를 벤치마크하는 것이 좋습니다.

언급URL : https://stackoverflow.com/questions/7463689/most-efficient-way-to-copy-a-file-in-linux

반응형