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

StackOverflow https://stackoverflow.com/questions/1006797

  •  06-07-2019
  •  | 
  •  

Вопрос

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

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

  1. Какой код вы бы порекомендовали для копирования файлов с помощью fopen()/fread()/fwrite()?
    • Какой код вы бы порекомендовали для копирования файлов с помощью open()/read()/write()?

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

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

Это было полезно?

Решение

Что касается фактического ввода-вывода, код, который я писал миллион раз в различных вариантах для копирования данных из одного потока в другой, выглядит примерно так.Он возвращает 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".

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

Другие советы

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

/*
@(#)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 по вашему выбору.

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

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

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

В C++ есть библиотека файлов в способствовать росту

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top