9

这看起来像一个简单的问题,但我在这里没有找到类似的东西。

由于C中没有文件复制功能,所以我们必须自己实现文件复制,但我不喜欢重新发明轮子,即使是这种琐碎的东西,所以我想问云:

  1. 对于使用 fopen()/fread()/fwrite() 进行文件复制,您会推荐什么代码?
    • 对于使用 open()/read()/write() 进行文件复制,您会推荐什么代码?

这段代码应该是可移植的(windows/mac/linux/bsd/qnx/younameit)、稳定、经过时间考验、快速、内存高效等。欢迎进入特定系统的内部以获取更多性能(比如获取文件系统集群大小) .

这似乎是一个微不足道的问题,但例如,CP 命令的源代码不是 10 行 C 代码。

4

7 回答 7

5

这是我需要从一个文件复制到另一个文件时使用的功能 - 使用测试工具:

/*
@(#)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 */

显然,这个版本使用来自标准 I/O 的文件指针而不是文件描述符,但它相当高效并且尽可能地可移植。


好吧,除了错误函数 - 这对我来说很独特。只要你干净地处理错误,你应该没问题。标题"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 供您选择。

于 2009-06-18T00:38:47.233 回答
3

就实际的 I/O 而言,我以各种形式编写了一百万次用于将数据从一个流复制到另一个流的代码是这样的。它在成功时返回 0,或者在错误时返回 -1 并设置 errno(在这种情况下,可能已经复制了任意数量的字节)。

请注意,对于复制常规文件,您可以跳过 EAGAIN 内容,因为常规文件总是阻塞 I/O。但不可避免地,如果您编写此代码,有人会在其他类型的文件描述符上使用它,因此将其视为免费赠品。

GNUcp做了一个特定于文件的优化,我在这里没有打扰,对于 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

顺便说一句,获取文件系统的集群大小几乎没有用。在超过磁盘块的大小之后,您几乎总是会看到速度随着缓冲区大小的增加而增加。

于 2009-06-17T15:32:16.107 回答
2

每次读取的大小需要是 512 的倍数(扇区大小) 4096 是一个不错的选择

于 2009-10-26T14:12:43.157 回答
1

这是一个非常简单明了的例子:复制文件。由于它是用 ANSI-C 编写的,没有任何特定的函数调用,我认为它非常便于移植。

于 2009-06-17T12:50:36.323 回答
1

根据您复制文件的意思,这肯定不是微不足道的。如果您的意思是仅复制内容,那么几乎无事可做。但通常,您需要复制文件的元数据,这肯定是平台相关的。我不知道有任何 C 库可以以可移植的方式执行您想要的操作。如果您关心可移植性,仅处理文件名本身就不是一件小事。

在 C++ 中,boost中有文件库

于 2009-06-17T12:54:10.310 回答
1

我在实现自己的文件副本时发现了一件事,这似乎很明显,但事实并非如此:I/O 很。您几乎可以通过复制多少来确定副本的速度。很明显,您需要尽可能少地做这些。

我发现最好的结果是当我给自己一个巨大的缓冲区时,在一次 I/O 中将整个源文件读入其中,然后在一次 I/O 中将整个缓冲区写回。如果我什至必须分 10 批进行,它会变得很慢。尝试读取和写入每个字节,就像一个天真的编码器可能首先尝试的那样,只是很痛苦。

于 2009-06-17T13:38:31.093 回答
0

Steve Jessop 写的接受的答案没有回答问题的第一部分,Jonathan Leffler 做了,但是做错了:代码应该写成

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 的大小为 1。注意,这里不是错误,而是额外的代码。
  2. The fwrite function writes upto nmemb (second argument) elements of specified size (third argument), it does not required to write exactly nmemb elements. To fix this you must write the rest of the data readed or just write one element of size n — let fwrite do all his work. (This item is in question, should fwrite write all data or not, but in my version short writes impossible until error occurs.)
  3. You should test for a read errors too: just test ferror(f1) at the end of loop.

Note, you probably need to disable buffering on both input and output files to prevent triple buffering: first on read to f1 buffer, second in our code, third on write to f2 buffer:

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

(Internal buffers should, probably, be of size BUFSIZ.)

于 2019-11-25T15:48:25.980 回答