Самый эффективный способ скопировать файл в Linux - PullRequest
27 голосов
/ 18 сентября 2011

Я работаю в независимом от ОС файловом менеджере и ищу наиболее эффективный способ копирования файла для Linux.В Windows есть встроенная функция CopyFileEx () , но, как я заметил, такой стандартной функции для Linux нет.Так что, думаю, мне придется реализовать свой собственный.Очевидным способом является fopen / fread / fwrite, но есть ли лучший (более быстрый) способ сделать это?У меня также должна быть возможность периодически останавливаться, чтобы я мог обновлять счетчик «скопировано на данный момент» для меню прогресса файла.

Ответы [ 5 ]

34 голосов
/ 19 сентября 2011

К сожалению, вы не можете использовать sendfile() здесь, потому что пункт назначения не является сокетом. (Имя sendfile() происходит от send() + "файл").

Для нулевого копирования вы можете использовать splice(), как предложено @Dave. (За исключением того, что это не будет нулевая копия; это будет "одна копия" из кэша страниц исходного файла в кэш страницы конечного файла.)

Однако ... (a) splice() зависит от Linux; и (b) вы почти наверняка можете сделать то же самое, используя переносные интерфейсы, при условии, что вы используете их правильно.

Короче говоря, используйте open() + read() + write() с временным буфером small . Я предлагаю 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);
}

С помощью этого цикла вы будете копировать 8K из кэша страниц in_fd в кэш L1 ЦП, а затем записывать его из кэша L1 в кэш страниц out_fd. Затем вы перезапишете эту часть кэша L1 с помощью следующего фрагмента 8 КБ из файла и так далее. Конечным результатом является то, что данные в buf вообще никогда не будут храниться в основной памяти (за исключением, может быть, одного раза в конце); с точки зрения системного ОЗУ, это так же хорошо, как использование «нулевой копии» splice(). Кроме того, он идеально переносим на любую систему POSIX.

Обратите внимание, что здесь используется небольшой буфер. Типичные современные процессоры имеют 32K или около того для кэша данных L1, поэтому, если вы сделаете буфер слишком большим, этот подход будет медленнее. Возможно, намного, намного медленнее. Поэтому держите буфер в диапазоне «несколько килобайт».

Конечно, если ваша дисковая подсистема не очень быстра, пропускная способность памяти, вероятно, не является вашим ограничивающим фактором. Поэтому я бы порекомендовал posix_fadvise, чтобы сообщить ядру, что вы делаете:

posix_fadvise(in_fd, 0, 0, POSIX_FADV_SEQUENTIAL);

Это даст подсказку ядру Linux, что его механизм упреждающего чтения должен быть очень агрессивным.

Я бы также предложил использовать posix_fallocate для предварительного выделения хранилища для файла назначения. Это заранее скажет вам, закончится ли у вас диск. А для современного ядра с современной файловой системой (например, XFS) это поможет уменьшить фрагментацию в конечном файле.

Последнее, что я бы порекомендовал, это mmap. Обычно это самый медленный подход из-за обмолота TLB. (Очень недавние ядра с «прозрачными огромными страницами» могли бы смягчить это; я недавно не пробовал. Но это, конечно, было очень плохо. Поэтому я бы потрудился протестировать mmap только если у вас есть достаточно времени для тестирования и совсем недавнего ядра .)

[Update]

В комментариях есть вопрос о том, является ли splice из одного файла в другой нулевой копией. Разработчики ядра Linux называют это «воровством страниц». И man-страница для splice, и комментарии в исходном тексте ядра говорят, что флаг SPLICE_F_MOVE должен обеспечивать эту функциональность.

К сожалению, поддержка SPLICE_F_MOVE была восстановлена ​​в 2.6.21 (в 2007 году) и никогда не заменялась. (Комментарии в исходных текстах ядра никогда не обновлялись.) При поиске в исходных кодах ядра вы обнаружите, что SPLICE_F_MOVE фактически нигде не упоминается. Последнее сообщение, которое я могу найти (с 2008 года) говорит, что оно "ожидает замены".

Суть в том, что splice из одного файла в другой вызывает memcpy для перемещения данных; это не ноль-копия. Это не намного лучше, чем вы можете сделать в пользовательском пространстве, используя read / write с небольшими буферами, так что вы также можете придерживаться стандартных переносимых интерфейсов.

Если "воровство страниц" когда-либо будет добавлено обратно в ядро ​​Linux, тогда преимущества splice будут гораздо больше. (И даже сегодня, когда местом назначения является сокет, вы получаете истинную нулевую копию, что делает splice более привлекательным.) Но для целей этого вопроса splice не очень-то вас покупает.

5 голосов
/ 18 сентября 2011

Используйте open / read / write - они избегают буферизации на уровне libc, выполняемой fopen и друзьями.

В качестве альтернативы, если вы используете GLib, вы можете использовать ее g_copy_file function.

Наконец, что может быть быстрее, но это должно быть проверено, чтобы убедиться: используйте open и mmap для отображения в памяти входного файла, затем write из области памятив выходной файл.Вы, вероятно, захотите оставить открытым / читать / писать как запасной вариант, так как этот метод ограничен размером адресного пространства вашего процесса.

Редактировать: оригинальный ответ предложил сопоставить обафайлы;@bdonlan сделал отличное предложение в комментарии, чтобы отобразить только одну.

4 голосов
/ 18 сентября 2011

Если вы знаете, что они будут использовать 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);
0 голосов
/ 25 сентября 2018

Мой ответ из более поздней копии этого поста.

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

В зависимости от ОС, ваш пробег может варьироваться в зависимости от системных вызовов, таких как splice и sendfile , однако обратите внимание на комментарии на странице руководства:

Приложения могут захотеть использовать чтение (2) / запись (2) в случае сбоя sendfile () с EINVAL или ENOSYS.

0 голосов
/ 18 сентября 2011

Вы можете захотеть сравнить команду dd

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...