Пробовал и правда простой код копирования файлов в C?

Это выглядит как простой вопрос, но я не нашел здесь ничего подобного.

Поскольку в C нет функции копирования файлов, мы должны реализовать копирование файлов сами, но я не люблю изобретать велосипед даже для таких тривиальных вещей, поэтому я хотел бы попросить облако:

  1. What code would you recommend for file copying using fopen()/fread()/fwrite()?
    • What code would you recommend for file copying using open()/read()/write()?

Этот код должен быть переносимым (windows/mac/linux/bsd/qnx/younameit), стабильным, проверенным временем, быстрым, эффективным с точки зрения памяти и т. д. Приветствуется проникновение во внутренние органы конкретной системы для повышения производительности (например, получение размера кластера файловой системы) .

Это кажется тривиальным вопросом, но, например, исходный код команды CP — это не 10 строк кода на C.


person Eugene Bujak    schedule 17.06.2009    source источник


Ответы (7)


Что касается фактического ввода-вывода, то код, который я писал миллион раз в разных обличьях для копирования данных из одного потока в другой, выглядит примерно так. Он возвращает 0 в случае успеха или -1 с установкой errno в случае ошибки (в этом случае может быть скопировано любое количество байтов).

Обратите внимание, что для копирования обычных файлов вы можете пропустить материал EAGAIN, поскольку обычные файлы всегда блокируют ввод-вывод. Но неизбежно, если вы напишете этот код, кто-то будет использовать его для других типов файловых дескрипторов, так что считайте это халявой.

В GNU cp есть оптимизация для конкретных файлов, о которой я здесь не упоминал: для длинных блоков по 0 байт вместо записи вы просто расширяете выходной файл, ища конец.

void block(int fd, int event) {
    pollfd topoll;
    topoll.fd = fd;
    topoll.events = event;
    poll(&topoll, 1, -1);
    // no need to check errors - if the stream is bust then the
    // next read/write will tell us
}

int copy_data_buffer(int fdin, int fdout, void *buf, size_t bufsize) {
    for(;;) {
       void *pos;
       // read data to buffer
       ssize_t bytestowrite = read(fdin, buf, bufsize);
       if (bytestowrite == 0) break; // end of input
       if (bytestowrite == -1) {
           if (errno == EINTR) continue; // signal handled
           if (errno == EAGAIN) {
               block(fdin, POLLIN);
               continue;
           }
           return -1; // error
       }

       // write data from buffer
       pos = buf;
       while (bytestowrite > 0) {
           ssize_t bytes_written = write(fdout, pos, bytestowrite);
           if (bytes_written == -1) {
               if (errno == EINTR) continue; // signal handled
               if (errno == EAGAIN) {
                   block(fdout, POLLOUT);
                   continue;
               }
               return -1; // error
           }
           bytestowrite -= bytes_written;
           pos += bytes_written;
       }
    }
    return 0; // success
}

// Default value. I think it will get close to maximum speed on most
// systems, short of using mmap etc. But porters / integrators
// might want to set it smaller, if the system is very memory
// constrained and they don't want this routine to starve
// concurrent ops of memory. And they might want to set it larger
// if I'm completely wrong and larger buffers improve performance.
// It's worth trying several MB at least once, although with huge
// allocations you have to watch for the linux 
// "crash on access instead of returning 0" behaviour for failed malloc.
#ifndef FILECOPY_BUFFER_SIZE
    #define FILECOPY_BUFFER_SIZE (64*1024)
#endif

int copy_data(int fdin, int fdout) {
    // optional exercise for reader: take the file size as a parameter,
    // and don't use a buffer any bigger than that. This prevents 
    // memory-hogging if FILECOPY_BUFFER_SIZE is very large and the file
    // is small.
    for (size_t bufsize = FILECOPY_BUFFER_SIZE; bufsize >= 256; bufsize /= 2) {
        void *buffer = malloc(bufsize);
        if (buffer != NULL) {
            int result = copy_data_buffer(fdin, fdout, buffer, bufsize);
            free(buffer);
            return result;
        }
    }
    // could use a stack buffer here instead of failing, if desired.
    // 128 bytes ought to fit on any stack worth having, but again
    // this could be made configurable.
    return -1; // errno is ENOMEM
}

Чтобы открыть входной файл:

int fdin = open(infile, O_RDONLY|O_BINARY, 0);
if (fdin == -1) return -1;

Открыть выходной файл сложно. В качестве основы вы хотите:

int fdout = open(outfile, O_WRONLY|O_BINARY|O_CREAT|O_TRUNC, 0x1ff);
if (fdout == -1) {
    close(fdin);
    return -1;
}

Но есть мешающие факторы:

  • вам нужен особый случай, когда файлы одинаковы, и я не могу вспомнить, как это сделать переносимым.
  • если имя выходного файла является каталогом, вы можете скопировать файл в каталог.
  • если выходной файл уже существует (откройте с помощью O_EXCL, чтобы определить это, и проверьте EEXIST при ошибке), вы можете сделать что-то другое, как это делает cp -i.
  • вы можете захотеть, чтобы разрешения выходного файла отражали разрешения входного файла.
  • вы можете захотеть скопировать метаданные для других платформ.
  • вы можете или не хотите отсоединять выходной файл при ошибке.

Очевидно, что ответы на все эти вопросы могут быть такими: «делай то же, что и cp». В этом случае ответ на первоначальный вопрос: «игнорировать все, что я или кто-либо еще сказал, и использовать источник cp».

Кстати, получение размера кластера файловой системы практически бесполезно. Вы почти всегда будете видеть, как скорость увеличивается с размером буфера еще долго после того, как вы превысите размер дискового блока.

person Steve Jessop    schedule 17.06.2009
comment
В вашем образце не удается компенсировать buf на уже записанную сумму, что приведет к перезапуску незавершенной записи с самого начала. - person Hasturkun; 17.06.2009
comment
Спасибо. Всегда есть одна ошибка. - person Steve Jessop; 17.06.2009
comment
ОП попросил переносное решение, но я обнаружил, что оно не работает в Windows. Для начала poll() отсутствует, а ssize_t является расширением POSIX. Не является непреодолимым, но код определенно не работает как есть. - person j b; 28.04.2015

Это функция, которую я использую, когда мне нужно скопировать из одного файла в другой - с помощью тестового набора:

/*
@(#)File:           $RCSfile: fcopy.c,v $
@(#)Version:        $Revision: 1.11 $
@(#)Last changed:   $Date: 2008/02/11 07:28:06 $
@(#)Purpose:        Copy the rest of file1 to file2
@(#)Author:         J Leffler
@(#)Modified:       1991,1997,2000,2003,2005,2008
*/

/*TABSTOP=4*/

#include "jlss.h"
#include "stderr.h"

#ifndef lint
/* Prevent over-aggressive optimizers from eliminating ID string */
const char jlss_id_fcopy_c[] = "@(#)$Id: fcopy.c,v 1.11 2008/02/11 07:28:06 jleffler Exp $";
#endif /* lint */

void fcopy(FILE *f1, FILE *f2)
{
    char            buffer[BUFSIZ];
    size_t          n;

    while ((n = fread(buffer, sizeof(char), sizeof(buffer), f1)) > 0)
    {
        if (fwrite(buffer, sizeof(char), n, f2) != n)
            err_syserr("write failed\n");
    }
}

#ifdef TEST

int main(int argc, char **argv)
{
    FILE *fp1;
    FILE *fp2;

    err_setarg0(argv[0]);
    if (argc != 3)
        err_usage("from to");
    if ((fp1 = fopen(argv[1], "rb")) == 0)
        err_syserr("cannot open file %s for reading\n", argv[1]);
    if ((fp2 = fopen(argv[2], "wb")) == 0)
        err_syserr("cannot open file %s for writing\n", argv[2]);
    fcopy(fp1, fp2);
    return(0);
}

#endif /* TEST */

Очевидно, что эта версия использует указатели на файлы из стандартного ввода-вывода, а не файловые дескрипторы, но она достаточно эффективна и настолько переносима, насколько это возможно.


Ну кроме функции ошибки - это мне свойственно. Пока вы корректно обрабатываете ошибки, все должно быть в порядке. Заголовок "jlss.h" объявляет fcopy(); заголовок "stderr.h" объявляет err_syserr() среди многих других подобных функций сообщения об ошибках. Далее следует простая версия функции — настоящая добавляет имя программы и выполняет некоторые другие действия.

#include "stderr.h"
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

void err_syserr(const char *fmt, ...)
{
    int errnum = errno;
    va_list args;
    va_start(args, fmt);
    vfprintf(stderr, fmt, args);
    va_end(args);
    if (errnum != 0)
        fprintf(stderr, "(%d: %s)\n", errnum, strerror(errnum));
    exit(1);
}

Приведенный выше код может рассматриваться как имеющий современную лицензию BSD или GPL v3 по вашему выбору.

person Jonathan Leffler    schedule 18.06.2009
comment
Мне нравится, просто, чисто, работает. Я использовал 4096 в качестве своего BUFSIZ, но я предполагаю, что любое число, кратное 512, должно работать хорошо. - person John Scipione; 06.08.2010
comment
@jonathan I этот код Каков размер, если BUFSIZ. ? Мой исходный файл может быть около 50 МБ? так какой размер мне подходит? - person user1089679; 20.03.2012
comment
BUFSIZ определен в <stdio.h> и является подходящим размером для файловых буферов на платформе. Если вы хотите взять на себя ответственность за размер буфера, используйте другое имя и укажите его размер: enum { BUFFER_SIZE = 4096 }; или любое другое значение, которое вы хотите использовать. В широких пределах больший размер буфера работает быстрее, но увеличение с 4 КБ до 256 КБ обычно не так уж велико, и вам приходится жертвовать пространством, используемым для буфера. В зависимости от вашей платформы (например, серверной или мобильной) вы можете настроить свой выбор. От 4 КиБ до 64 КиБ будет достаточно для большинства целей. - person Jonathan Leffler; 20.03.2012
comment
Я люблю простоту. Почему такая функция не включена напрямую в glibc? - person m-ric; 22.10.2012
comment
Очевидно, что этот подход проще и (намного) более переносим, ​​чем решение, предложенное в stackoverflow.com/a/1007799/172218. ...есть ли недостатки? - person j b; 29.04.2015

размер каждого чтения должен быть кратен 512 (размер сектора) 4096 является хорошим

person Mandrake    schedule 26.10.2009

Вот очень простой и понятный пример: Копировать файл. Поскольку он написан на ANSI-C без каких-либо конкретных вызовов функций, я думаю, что он будет в значительной степени переносимым.

person merkuro    schedule 17.06.2009
comment
К сожалению, он использует fgetc, что довольно неэффективно. - person David Schmitt; 17.06.2009
comment
Хорошая точка зрения! Хотя он очень четкий и портативный, ему определенно не хватает производительности. - person merkuro; 17.06.2009
comment
@David: fgetc() неэффективен? Stdio выполняет собственную буферизацию, используя буфер размером BUFSIZ (8192 байта в моей системе). Если вы используете MSVC++, #define _CRT_DISABLE_PERFCRIT_LOCKS в однопоточных программах. - person j_random_hacker; 17.06.2009
comment
getc() может быть немного быстрее, чем fgetc(), поскольку он реализован как макрос, однако я сомневаюсь, что ветвление ЦП будет узким местом - чтение с диска будет. - person j_random_hacker; 17.06.2009
comment
@j_random: по моему опыту, когда я тестировал это, буферы 8 КБ никогда не достигали оптимальной производительности ввода-вывода. Иногда 16-килобайтные буферы были буквально в два раза быстрее. Очевидно, что MS не хочет, чтобы каждый дескриптор файла имел огромные накладные расходы памяти, поэтому они скомпрометировали его, но при копировании файла вам может понадобиться другой компромисс. Кроме того, только накладные расходы на бухгалтерию могут быть значительными (проверка границ, обновление позиций файлового указателя и т.п.), если вы выполняете две операции дескриптора файла на символ. Единственный способ узнать это написать код и посмотреть, конечно. - person Steve Jessop; 17.06.2009
comment
Также, конечно, интуиция о файловом вводе-выводе зависит от того, думаете ли вы о файлах, как правило, о нескольких килобайтах или обычно о нескольких гигабайтах... - person Steve Jessop; 17.06.2009
comment
Только что нашел еще один пример, который утверждает, что имеет очень высокую скорость: веб-скрипты. softpedia.com/scriptDownload/ В любом случае я бы посоветовал начать с действительно простой и читаемой конструкции, инкапсулированной в функцию в начале, и если на вашем пути возникнут проблемы со скоростью, потратьте некоторое время на настройку. - person merkuro; 17.06.2009
comment
@merkuro: у кода java2s есть некоторые проблемы. Например, он не закрывает дескрипторы при ошибке и не очень эффективно обрабатывает EINTR. Если вам нужно создать процесс для копирования файла, то вы отклоняетесь от переносимого кода, и если этот код должен быть вызываемым, его необходимо исправить. - person Steve Jessop; 17.06.2009
comment
@David: я обнаружил, что буферизация, предоставляемая ОС, недостаточна. Смотрите мой ответ ниже. - person T.E.D.; 17.06.2009

В зависимости от того, что вы подразумеваете под копированием файла, это далеко не тривиально. Если вы имеете в виду только копирование контента, то тут делать почти нечего. Но, как правило, вам нужно скопировать метаданные файла, и это, безусловно, зависит от платформы. Я не знаю ни одной библиотеки C, которая делает то, что вы хотите, переносимым образом. Просто обработка имени файла сама по себе не является тривиальной задачей, если вы заботитесь о переносимости.

В C++ библиотека файлов находится в boost

person David Cournapeau    schedule 17.06.2009

При реализации собственной копии файла я обнаружил одну вещь, которая кажется очевидной, но это не так: операции ввода-вывода медленные. Вы можете в значительной степени оценить скорость своей копии по тому, сколько из них вы сделаете. Поэтому очевидно, что вам нужно сделать как можно меньше из них.

Наилучшие результаты, которые я получил, были, когда я получил огромный буфер, прочитал в него весь исходный файл за один ввод-вывод, а затем записал из него весь буфер за один ввод-вывод. Если бы мне даже пришлось делать это в 10 партиях, это становилось бы очень медленным. Попытка прочитать и записать каждый байт, как это может сделать сначала наивный кодер, была просто болезненной.

person T.E.D.    schedule 17.06.2009

Принятый ответ, написанный Стивом Джессопом, не отвечает на первую часть вопроса, Джонатан Леффлер делает это, но делает это неправильно: код должен быть написан как

while ((n = fread(buffer, 1, sizeof(buffer), f1)) > 0)
    if (fwrite(buffer, n, 1, f2) != 1)
        /* we got write error here */

/* test ferror(f1) for a read errors */

Объяснение:

  1. sizeof(char) = 1 по определению, всегда: неважно, сколько в нем бит, 8 (в большинстве случаев), 9, 11 или 32 (на каком-то DSP, например) — размер char равен единице. Обратите внимание, здесь не ошибка, а лишний код.
  2. Функция fwrite записывает до nmemb (второй аргумент) элементов заданного размера (третий аргумент), она не требует записи ровно nmemb элементов. Чтобы исправить это, вы должны записать остальные прочитанные данные или просто записать один элемент размера n — пусть fwrite сделает всю свою работу. (Этот пункт под вопросом, должна fwrite записывать все данные или нет, но в моей версии короткая запись невозможна, пока не возникнет ошибка.)
  3. Вы также должны проверить наличие ошибок чтения: просто проверьте ferror(f1) в конце цикла.

Обратите внимание, вам, вероятно, нужно отключить буферизацию как для входных, так и для выходных файлов, чтобы предотвратить тройную буферизацию: первая при чтении в буфер f1, вторая в нашем коде, третья при записи в буфер f2:

setvbuf(f1, NULL, _IONBF, 0);
setvbuf(f2, NULL, _IONBF, 0);

(Внутренние буферы, вероятно, должны иметь размер BUFSIZ.)

person Alexei A. Smekalkine    schedule 25.11.2019