如何使用C在Unix上复制文件?

Mot*_*tti 51 c unix copy

我正在寻找与Win32的CopyFile相当的Unix ,我不想通过编写自己的版本重新发明轮子.

caf*_*caf 53

无需调用非可移植API sendfile,也不需要外部实用程序.在70年代可以使用的相同方法现在仍然有效:

#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

int cp(const char *to, const char *from)
{
    int fd_to, fd_from;
    char buf[4096];
    ssize_t nread;
    int saved_errno;

    fd_from = open(from, O_RDONLY);
    if (fd_from < 0)
        return -1;

    fd_to = open(to, O_WRONLY | O_CREAT | O_EXCL, 0666);
    if (fd_to < 0)
        goto out_error;

    while (nread = read(fd_from, buf, sizeof buf), nread > 0)
    {
        char *out_ptr = buf;
        ssize_t nwritten;

        do {
            nwritten = write(fd_to, out_ptr, nread);

            if (nwritten >= 0)
            {
                nread -= nwritten;
                out_ptr += nwritten;
            }
            else if (errno != EINTR)
            {
                goto out_error;
            }
        } while (nread > 0);
    }

    if (nread == 0)
    {
        if (close(fd_to) < 0)
        {
            fd_to = -1;
            goto out_error;
        }
        close(fd_from);

        /* Success! */
        return 0;
    }

  out_error:
    saved_errno = errno;

    close(fd_from);
    if (fd_to >= 0)
        close(fd_to);

    errno = saved_errno;
    return -1;
}
Run Code Online (Sandbox Code Playgroud)

  • 我发现`goto`的受控使用对于在一个地方合并错误处理路径很有用. (13认同)
  • 不可用于一般用途.文件的副本不仅仅是数据流.稀疏文件或扩展属性怎么样?这又是为什么Windows API丑陋,因为它胜过Linux (7认同)
  • @Lothar Unix 文件在概念上只是一个字节序列。元数据(例如权限、ACL 等)与数据的实际复制正交处理。正如他们应该的那样。特定于应用程序的文件格式是应用程序的问题。正如他们应该的那样。 (3认同)
  • @AnttiHaapala如果您使用更大的缓冲区,您最好不要在堆栈上分配它(即使用malloc())。另外,由于 4096 是典型的页面大小(也是硬盘扇区大小的倍数),因此它不是一个不合理的值)——根据口味定制。 (2认同)
  • @wcochran 不,它们不是,ACL 可能是,但扩展属性、备用流等所有相关数据。这是你落后的观点。作为一名教授但不了解文件处理的现代需求的人感到羞耻。象牙塔里的白痴。如果我们继续前进而不是赞扬 1970 年的旧技术状态,那么 IT 世界将会变得更加容易。 (2认同)

Mah*_*dsi 21

API中没有与烘焙等效的CopyFile函数.但sendfile可用于以内核模式复制文件,这是一种更快更好的解决方案(出于多种原因),而不是打开文件,循环读取缓冲区,以及将输出写入另一个文件.

更新:

从Linux内核版本2.6.33开始,需要输出为sendfile套接字的限制被取消,原始代码可以在Linux上运行 - 但是,从OS X 10.9 Mavericks开始,sendfile在OS X上现在要求输出为套接字和代码将无法正常工作!

以下代码片段应适用于大多数OS X(截至10.5),(免费)BSD和Linux(截至2.6.33).对于所有平台,实现都是"零拷贝",这意味着所有平台都在内核空间中完成,并且不会在用户空间内外复制缓冲区或数据.几乎可以获得最佳性能.

#include <fcntl.h>
#include <unistd.h>
#if defined(__APPLE__) || defined(__FreeBSD__)
#include <copyfile.h>
#else
#include <sys/sendfile.h>
#endif

int OSCopyFile(const char* source, const char* destination)
{    
    int input, output;    
    if ((input = open(source, O_RDONLY)) == -1)
    {
        return -1;
    }    
    if ((output = creat(destination, 0660)) == -1)
    {
        close(input);
        return -1;
    }

    //Here we use kernel-space copying for performance reasons
#if defined(__APPLE__) || defined(__FreeBSD__)
    //fcopyfile works on FreeBSD and OS X 10.5+ 
    int result = fcopyfile(input, output, 0, COPYFILE_ALL);
#else
    //sendfile will work with non-socket output (i.e. regular file) on Linux 2.6.33+
    off_t bytesCopied = 0;
    struct stat fileinfo = {0};
    fstat(input, &fileinfo);
    int result = sendfile(output, input, &bytesCopied, fileinfo.st_size);
#endif

    close(input);
    close(output);

    return result;
}
Run Code Online (Sandbox Code Playgroud)

编辑:用调用替换了目的地的开头,creat()因为我们想要O_TRUNC指定标志.见下面的评论.

  • 根据手册页,`sendfile`的输出参数必须是套接字.你确定这个有效吗? (3认同)

pli*_*nth 20

直接使用fork/execl来运行cp来为你完成工作.这比系统更有优势,因为它不容易受到Bobby Tables攻击,并且您不需要对参数进行相同程度的清理.此外,由于system()要求您将命令参数拼凑在一起,因此不太可能因spprintf()检查草率而出现缓冲区溢出问题.

直接调用cp而不是编写cp的优点是不必担心目标中存在的目标路径的元素.在自己动手的代码中执行此操作容易出错且乏味.

我在ANSI C中编写了这个示例,并且只删除了最简单的错误处理,而不是它的直接代码.

void copy(char *source, char *dest)
{
    int childExitStatus;
    pid_t pid;
    int status;
    if (!source || !dest) {
        /* handle as you wish */
    }

    pid = fork();

    if (pid == 0) { /* child */
        execl("/bin/cp", "/bin/cp", source, dest, (char *)0);
    }
    else if (pid < 0) {
        /* error - couldn't start process - you decide how to handle */
    }
    else {
        /* parent - wait for child - this has all error handling, you
         * could just call wait() as long as you are only expecting to
         * have one child process at a time.
         */
        pid_t ws = waitpid( pid, &childExitStatus, WNOHANG);
        if (ws == -1)
        { /* error - handle as you wish */
        }

        if( WIFEXITED(childExitStatus)) /* exit code in childExitStatus */
        {
            status = WEXITSTATUS(childExitStatus); /* zero is normal exit */
            /* handle non-zero as you wish */
        }
        else if (WIFSIGNALED(childExitStatus)) /* killed */
        {
        }
        else if (WIFSTOPPED(childExitStatus)) /* stopped */
        {
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


Pat*_*ter 5

复制功能的另一个变体,使用普通的POSIX调用且没有任何循环。代码源自caf答案的缓冲区副本变体。警告:mmap在32位系统上使用很容易失败,而在64位系统上使用该危险的可能性较小。

#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/mman.h>

int cp(const char *to, const char *from)
{
  int fd_from = open(from, O_RDONLY);
  if(fd_from < 0)
    return -1;
  struct stat Stat;
  if(fstat(fd_from, &Stat)<0)
    goto out_error;

  void *mem = mmap(NULL, Stat.st_size, PROT_READ, MAP_SHARED, fd_from, 0);
  if(mem == MAP_FAILED)
    goto out_error;

  int fd_to = creat(to, 0666);
  if(fd_to < 0)
    goto out_error;

  ssize_t nwritten = write(fd_to, mem, Stat.st_size);
  if(nwritten < Stat.st_size)
    goto out_error;

  if(close(fd_to) < 0) {
    fd_to = -1;
    goto out_error;
  }
  close(fd_from);

  /* Success! */
  return 0;
}
out_error:;
  int saved_errno = errno;

  close(fd_from);
  if(fd_to >= 0)
    close(fd_to);

  errno = saved_errno;
  return -1;
}
Run Code Online (Sandbox Code Playgroud)

编辑:更正了文件创建错误。请参阅http://stackoverflow.com/questions/2180079/how-can-i-copy-a-file-on-unix-using-c/21​​80157#2180157答案中的评论。


and*_*otn 5

逐字节复制文件确实有效,但在现代 UNIX 上速度缓慢且浪费。现代 UNIX 在文件系统中内置了 \xe2\x80\x9ccopy-on-write\xe2\x80\x9d 支持:系统调用创建一个指向磁盘上现有字节的新目录条目,并且磁盘上没有文件内容字节直到其中一个副本被修改,此时只有更改的块才会写入磁盘。这允许近乎即时的文件副本,无需使用额外的文件块,无论文件大小如何。例如,以下是有关xfs 中如何工作的一些详细信息。

\n

在 Linux 上,使用coreutils cp现在默认的方式FICLONE ioctl

\n
 #ifdef FICLONE\n   return ioctl (dest_fd, FICLONE, src_fd);\n #else\n   errno = ENOTSUP;\n   return -1;\n #endif\n
Run Code Online (Sandbox Code Playgroud)\n

在 macOS 上,使用clonefile(2)在 APFS 卷上进行即时复制。Apple\xe2\x80\x99s 使用的就是这个cp -c。文档并不完全清楚,但copyfile(3)COPYFILE_CLONE很可能也使用了它。如果您\xe2\x80\x99d 喜欢我测试一下,请发表评论。

\n

如果不支持这些写时复制操作\xe2\x80\x94无论操作系统太旧、底层文件系统不支持它,还是因为您正在不同的文件系统之间复制文件\xe2\x80\x94,您确实需要回退到尝试 sendfile,或者作为最后的手段,逐字节复制。但为了节省大家大量的时间和磁盘空间,请给予FICLONEclonefile(2)

\n


Con*_*lls 2

一种选择是您可以使用system()来执行cp. 这只是重新使用cp(1)命令来完成工作。link()如果您只需要建立另一个指向该文件的链接,可以使用或 来完成symlink()

  • 如果用户创建类似“somefile;rm /bin/*”的文件名会发生什么?system() 使用 sh -c 执行命令,因此整个字符串的文本由 shell 执行,这意味着在作为命令执行的分号之后您会得到任何内容 - 如果您的代码也运行 setuid,那就很糟糕了。这与 Bobby Tables (http://xkcd.com/327/) 没有什么不同。对于完全清理 system() 所带来的麻烦,您可以使用正确的参数直接在 /bin/cp 上执行 fork/exec 对。 (13认同)
  • 请注意 system() 是一个安全漏洞。 (4认同)