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


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


41
2018-02-01 21:05


起源


为了不重新发明轮子编译GNU coreutils,AFAIK它有一个静态库,用于在其构建树中复制文件,由 cp 和别的。它支持稀疏和btrfs牛 - basin


答案:


没有必要像这样调用非便携式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;
}

43
2018-02-01 23:16



@Caf:OMG .... g.o.t.o ..... :)你的代码比我的更加理智......;)带有读/写的旧循环是最便携的......来自我的+1 ... - t0mm13b
我发现控制使用 goto 可以在一个地方合并错误处理路径。 - caf
不可用于一般用途。文件的副本不仅仅是数据流。稀疏文件或扩展属性怎么样?这又是为什么Windows API像Linux一样丑陋的原因 - Lothar
问题:如果fd_to未设置为O_NONBLOCK,您还需要第二个循环吗? - jasongregori
你处理 EINTR 在里面 write() 循环,但不在 read() 循环。 - Jonathon Reinhart


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

直接调用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 */
        {
        }
    }
}

20
2018-02-01 21:54



+另一个长长的,详细的,艰难的。真的让你欣赏perl中system()的“vector”/ list形式。嗯。也许带有argv数组的system-ish函数会很好用吗?!? - Roboprog
@Roboprog和plinth,听说过 posix_spawnp... - Antti Haapala
...毕竟它是17年前在glibc实施的,并且在你的答案写完之前成为标准功能10耳... - Antti Haapala
@AnttiHaapala - 秀(不说)? - Roboprog


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

更新:

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

编辑:通过调用替换了目的地的开头 creat() 因为我们想要旗帜 O_TRUNC 要指定。见下面的评论。


18
2018-02-01 21:17



根据手册页,输出参数 sendfile 必须是一个插座。你确定这有效吗? - Jay Conrod
对于Linux,Jay Conrod是对的 - out_fd 的 sendfile 可能是2.4内核中的常规文件,但它现在必须支持 sendpage 内部内核API(实质上是指管道或套接字)。 sendpage 在不同的UNIX上实现不同 - 它没有标准语义。 - caf
Linux下的原型与OSX不同,因此您会认为(我也是这么认为)当我看到您的实现并看到sendfile的额外参数时......它是依赖于平台的 - 值得注意的事项! - t0mm13b
fyi - 你可以用if(PathsMatch(source,destination))返回1来节省大量的工作; / *其中PathsMatch是语言环境* /的适当路径比较例程,否则我想第二次打开会失败。 - plinth
+1 man sendfile 说自2.6.33以来,这再次得到支持。 sendfile() 优于 CopyFile() 因为它允许偏移。这对于从文件中剥离标头信息很有用。 - artless noise


sprintf( cmd, "/bin/cp -p \'%s\' \'%s\'", old, new);

system( cmd);

添加一些错误检查......

否则,打开两个并循环读/写,但可能不是你想要的。

...

更新以解决有效的安全问题:

而不是使用“system()”,执行fork / wait,并在子代中调用execv()或execl()。

execl( "/bin/cp", "-p", old, new);

6
2018-02-01 21:11



Dang,我必须学会更快地“提交”:-) - Roboprog
这不适用于名称中包含空格(或引号,反斜杠,美元符号等)的文件。我经常在文件名中使用空格。 - Dietrich Epp
哎哟。那就对了。在sprintf()中的文件名周围添加反斜杠单引号。 - Roboprog
好的,这是瑞士奶酪(参见其他地方的评论中的有效安全问题),但如果你有一个相对受控的环境,它可能会有一些用处。 - Roboprog
是的,简单明显和错误,在许多情况下。这就是为什么我投票给一些更精细的例子。 - Roboprog


有一种方法可以做到这一点,而不是诉诸于 system 调用,你需要包含这样的包装:

#include <sys/sendfile.h>
#include <fcntl.h>
#include <unistd.h>

/* 
** http://www.unixguide.net/unix/programming/2.5.shtml 
** About locking mechanism...
*/

int copy_file(const char *source, const char *dest){
   int fdSource = open(source, O_RDWR);

   /* Caf's comment about race condition... */
   if (fdSource > 0){
     if (lockf(fdSource, F_LOCK, 0) == -1) return 0; /* FAILURE */
   }else return 0; /* FAILURE */

   /* Now the fdSource is locked */

   int fdDest = open(dest, O_CREAT);
   off_t lCount;
   struct stat sourceStat;
   if (fdSource > 0 && fdDest > 0){
      if (!stat(source, &sourceStat)){
          int len = sendfile(fdDest, fdSource, &lCount, sourceStat.st_size);
          if (len > 0 && len == sourceStat.st_size){
               close(fdDest);
               close(fdSource);

               /* Sanity Check for Lock, if this is locked -1 is returned! */
               if (lockf(fdSource, F_TEST, 0) == 0){
                   if (lockf(fdSource, F_ULOCK, 0) == -1){
                      /* WHOOPS! WTF! FAILURE TO UNLOCK! */
                   }else{
                      return 1; /* Success */
                   }
               }else{
                   /* WHOOPS! WTF! TEST LOCK IS -1 WTF! */
                   return 0; /* FAILURE */
               }
          }
      }
   }
   return 0; /* Failure */
}

以上样本(省略错误检查!)使用 openclose 和 sendfile

编辑: 如 CAF 指出了一个 竞争条件 之间可能会发生 open 和 stat 所以我认为我会让它更健壮......请记住,锁定机制因平台而异......在Linux下,这种锁定机制与 lockf 就够了如果你想让这个便携,请使用 #ifdef用于区分不同平台/编译器的宏...感谢caf发现这个...有一个链接到一个网站,产生“通用锁定例程” 这里


4
2018-02-01 21:25



我不是100%肯定sendfile原型,我认为我有一个参数错误...请记住... :) - t0mm13b
+1,好的(可重复使用的例程和所有) - Roboprog
你有一个竞争条件 - 你打开的文件 fdSource 和你有的文件 stat()ed 不一定是一样的。 - caf
@caf:你能看到更详细的信息吗?怎么会有竞争条件?我会相应地修改答案..谢谢让我知道...... - t0mm13b
tommbieb75:简单 - 介于两者之间 open() 打电话和 stat() 调用,其他人可以重命名该文件并在该名称下放置一个不同的文件 - 因此您将复制第一个文件中的数据,但使用第二个文件的长度。 - caf


使用普通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;
}

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


4
2017-09-11 11:49



与中的错误相同 stackoverflow.com/questions/2180079/...。如果目标已存在并且大于源,则文件副本将仅部分覆盖目标,而不截断生成的文件; - Patrick Schlüter
(我意识到这是一个老问题,但是......)当映射文件的大小与可用内存和交换文件的大小相比时,mmap会发生什么?是否会在内存/交换情况下挂起系统? - Ben Slade
将文件映射到进程的地址范围本身不占用任何内存。就好像你说你的文件现在是交换空间的一部分。这意味着当您访问映射文件中的地址时,它将首先生成页面错误,因为内存中没有任何内容。操作系统从磁盘加载该地址的相应页面,并将控制权恢复到进程。如果没有可用的内存,则操作系统将从任何其他进程中释放一些其他映射页面;在优先级干净的页面(即不需要写入磁盘),但也脏页。 => - Patrick Schlüter
当映射页面的访问模式超过系统中的物理内存量并且必须始终读取和写入页面时,才会发生交换。 mmap可以被视为只是增加系统交换区域。带有选项MAP_SHARED的mmap也可以看作是使进程可以访问文件缓存的一种方法。 - Patrick Schlüter
因此,如果您对大文件进行mmap,然后访问大量文件,并且您访问的文件数量大于实际内存,操作系统将开始分页其他进程。如果这种情况发生得太多,操作系统将开始在交换活动中进行颠簸。我的观点是,对于与内存+交换相关的文件较大,您必须考虑被访问的mmap数据的大小不会导致问题 - Ben Slade


一种选择是你可以使用 system() 执行 cp。这只是重新使用了 cp(1) 命令做这项工作。如果您只需要在文件中创建另一个链接,则可以使用 link() 要么 symlink()


2
2018-02-01 21:10



请注意system()是一个安全漏洞。 - plinth
您说“如果”,但链接将无法跨文件系统工作,仅供参考。 - Roboprog
真?你会在生产代码中使用它吗?我想不出一个很好的理由,但不会让我感到沮丧 清洁 解。 - Motti
如果指定/ bin / cp的路径,则相对安全,除非攻击者设法破坏系统,以至于他们可以对/ bin中的任意系统shell实用程序进行修改。如果他们在一定程度上损害了系统,那么你就会遇到更大的问题。 - ConcernedOfTunbridgeWells
如果用户创建文件名如“somefile; rm / bin / *”会发生什么? system()用sh -c执行命令,所以shell执行整个字符串的文本,这意味着你在分号作为命令执行后得到任何东西 - 如果你的代码也运行setuid就会发臭。这与Bobby Tables不同(xkcd.com/327)。对于完全清理system()所需要的麻烦,您可以使用正确的参数直接在/ bin / cp上执行fork / exec对。 - plinth


此解决方案更像是一种解决方法,但它具有跨平台的优势。它包括读取第一个文件的每个字符并将其写入第二个文件。这是代码(没有文件打开错误处理):

void copyFile(char from[],char to[]) {
    FILE* copyFrom = fopen(from,"r");
    FILE* copyTo = fopen(to,"w");
    for (;;) {
        int caractereActuel = fgetc(copyFrom);
        if (caractereActuel != EOF) {
            fputc(caractereActuel,copyTo);
        }
        else {
            break;
        }
    }
    fclose(copyFrom);
    fclose(copyTo);
}

0
2017-07-13 12:04



这是最慢的方法。你可以使用缓冲区 fread / fwrite 并保持兼容性。 - i486