【发布时间】:2011-01-11 21:53:31
【问题描述】:
我正在寻找与 Win32 的 CopyFile 等效的 Unix,我不想通过编写自己的版本来重新发明轮子。
【问题讨论】:
-
为了不重新发明轮子编译 GNU coreutils,AFAIK 它有一个用于在其构建树中复制文件的静态库,供
cp和其他人使用。它支持稀疏和 btrfs 牛
我正在寻找与 Win32 的 CopyFile 等效的 Unix,我不想通过编写自己的版本来重新发明轮子。
【问题讨论】:
cp 和其他人使用。它支持稀疏和 btrfs 牛
好问题。与另一个好问题相关:
In C on linux how would you implement cp
对于 cp 的“最简单”实现有两种方法。一种方法使用某种文件复制系统调用函数——我们得到的最接近 Unix cp 命令的 C 函数版本的东西。另一种方法使用缓冲区和读/写系统调用函数,可以直接使用,也可以使用 FILE 包装器。
很可能只发生在内核拥有的内存中的文件复制系统调用比发生在内核和用户拥有的内存中的系统调用要快,尤其是在网络文件系统设置中(在机器之间复制)。但这需要测试(例如使用 Unix 命令时间),并且取决于编译和执行代码的硬件。
操作系统没有标准 Unix 库的人也可能想要使用您的代码。然后你想使用缓冲区读/写版本,因为它只依赖于
这是一个使用 unix 标准库 <unistd.h> 中的函数 copy_file_range 将源文件复制到(可能不存在的)目标文件的示例。复制发生在内核空间中。
/* copy.c
*
* Defines function copy:
*
* Copy source file to destination file on the same filesystem (possibly NFS).
* If the destination file does not exist, it is created. If the destination
* file does exist, the old data is truncated to zero and replaced by the
* source data. The copy takes place in the kernel space.
*
* Compile with:
*
* gcc copy.c -o copy -Wall -g
*/
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <unistd.h>
/* On versions of glibc < 2.27, need to use syscall.
*
* To determine glibc version used by gcc, compute an integer representing the
* version. The strides are chosen to allow enough space for two-digit
* minor version and patch level.
*
*/
#define GCC_VERSION (__GNUC__*10000 + __GNUC_MINOR__*100 + __gnuc_patchlevel__)
#if GCC_VERSION < 22700
static loff_t copy_file_range(int in, loff_t* off_in, int out,
loff_t* off_out, size_t s, unsigned int flags)
{
return syscall(__NR_copy_file_range, in, off_in, out, off_out, s,
flags);
}
#endif
/* The copy function.
*/
int copy(const char* src, const char* dst){
int in, out;
struct stat stat;
loff_t s, n;
if(0>(in = open(src, O_RDONLY))){
perror("open(src, ...)");
exit(EXIT_FAILURE);
}
if(fstat(in, &stat)){
perror("fstat(in, ...)");
exit(EXIT_FAILURE);
}
s = stat.st_size;
if(0>(out = open(dst, O_CREAT|O_WRONLY|O_TRUNC, 0644))){
perror("open(dst, ...)");
exit(EXIT_FAILURE);
}
do{
if(1>(n = copy_file_range(in, NULL, out, NULL, s, 0))){
perror("copy_file_range(...)");
exit(EXIT_FAILURE);
}
s-=n;
}while(0<s && 0<n);
close(in);
close(out);
return EXIT_SUCCESS;
}
/* Test it out.
*
* BASH:
*
* gcc copy.c -o copy -Wall -g
* echo 'Hello, world!' > src.txt
* ./copy src.txt dst.txt
* [ -z "$(diff src.txt dst.txt)" ]
*
*/
int main(int argc, char* argv[argc]){
if(argc!=3){
printf("Usage: %s <SOURCE> <DESTINATION>", argv[0]);
exit(EXIT_FAILURE);
}
copy(argv[1], argv[2]);
return EXIT_SUCCESS;
}
它基于我的 Ubuntu 20.x Linux 发行版的 copy_file_range 手册页中的示例。检查你的手册页:
> man copy_file_range
然后点击j 或Enter,直到您进入示例部分。或输入/example 进行搜索。
这是一个仅使用stdlib/stdio 的示例。缺点是它在用户空间中使用了一个中间缓冲区。
/* copy.c
*
* Compile with:
*
* gcc copy.c -o copy -Wall -g
*
* Defines function copy:
*
* Copy a source file to a destination file. If the destination file already
* exists, this clobbers it. If the destination file does not exist, it is
* created.
*
* Uses a buffer in user-space, so may not perform as well as
* copy_file_range, which copies in kernel-space.
*
*/
#include <stdlib.h>
#include <stdio.h>
#define BUF_SIZE 65536 //2^16
int copy(const char* in_path, const char* out_path){
size_t n;
FILE* in=NULL, * out=NULL;
char* buf = calloc(BUF_SIZE, 1);
if((in = fopen(in_path, "rb")) && (out = fopen(out_path, "wb")))
while((n = fread(buf, 1, BUF_SIZE, in)) && fwrite(buf, 1, n, out));
free(buf);
if(in) fclose(in);
if(out) fclose(out);
return EXIT_SUCCESS;
}
/* Test it out.
*
* BASH:
*
* gcc copy.c -o copy -Wall -g
* echo 'Hello, world!' > src.txt
* ./copy src.txt dst.txt
* [ -z "$(diff src.txt dst.txt)" ]
*
*/
int main(int argc, char* argv[argc]){
if(argc!=3){
printf("Usage: %s <SOURCE> <DESTINATION>\n", argv[0]);
exit(EXIT_FAILURE);
}
return copy(argv[1], argv[2]);
}
【讨论】:
我看到还没有人提到 copy_file_range,至少在 Linux 上受支持 and FreeBSD。这个的优点是它明确记录了利用 CoW 技术(如 reflinks)的能力。引用:
copy_file_range()为文件系统提供了实现“复制加速”技术的机会,例如使用 reflinks(即,两个或多个 inode 共享指向相同的写时复制磁盘块的指针) em> 或服务器端复制(在 NFS 的情况下)。
FWIW,我不确定老 sendfile 是否能够做到这一点。我发现的少数提及声称它没有。从这个意义上说,copy_file_range 优于 sendfile。
以下是使用调用的示例(从手册中逐字复制)。我还检查了在使用此代码在 BTRFS 文件系统中复制 bash 二进制文件后,该副本被重新链接到原始 (我通过在文件上调用 duperemove 并看到 Skipping - extents are already deduped. 消息来做到这一点)。
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
int
main(int argc, char **argv)
{
int fd_in, fd_out;
struct stat stat;
off64_t len, ret;
if (argc != 3) {
fprintf(stderr, "Usage: %s <source> <destination>\n", argv[0]);
exit(EXIT_FAILURE);
}
fd_in = open(argv[1], O_RDONLY);
if (fd_in == -1) {
perror("open (argv[1])");
exit(EXIT_FAILURE);
}
if (fstat(fd_in, &stat) == -1) {
perror("fstat");
exit(EXIT_FAILURE);
}
len = stat.st_size;
fd_out = open(argv[2], O_CREAT | O_WRONLY | O_TRUNC, 0644);
if (fd_out == -1) {
perror("open (argv[2])");
exit(EXIT_FAILURE);
}
do {
ret = copy_file_range(fd_in, NULL, fd_out, NULL, len, 0);
if (ret == -1) {
perror("copy_file_range");
exit(EXIT_FAILURE);
}
len -= ret;
} while (len > 0 && ret > 0);
close(fd_in);
close(fd_out);
exit(EXIT_SUCCESS);
}
【讨论】:
很简单:
#define BUF_SIZE 65536
int cp(const char *from, const char*to){
FILE *src, *dst;
size_t in, out;
char *buf = (char*) malloc(BUF_SIZE* sizeof(char));
src = fopen(from, "rb");
if (NULL == src) exit(2);
dst = fopen(to, "wb");
if (dst < 0) exit(3);
while (1) {
in = fread(buf, sizeof(char), BUF_SIZE, src);
if (0 == in) break;
out = fwrite(buf, sizeof(char), in, dst);
if (0 == out) break;
}
fclose(src);
fclose(dst);
}
适用于 windows 和 linux。
【讨论】:
逐字节复制文件确实有效,但在现代 UNIX 上是缓慢且浪费的。现代 UNIX 在文件系统中内置了“写时复制”支持:系统调用创建一个指向磁盘上现有字节的新目录条目,并且在修改其中一个副本之前不会触及磁盘上的文件内容字节,此时只有更改的块被写入磁盘。这允许不使用额外文件块的近乎即时的文件复制,而不管文件大小。例如,这里有一些关于how this works in xfs的详细信息。
在 linux 上,使用 the FICLONE ioctl as coreutils cp now does by default。
#ifdef FICLONE
return ioctl (dest_fd, FICLONE, src_fd);
#else
errno = ENOTSUP;
return -1;
#endif
在 macOS 上,使用 clonefile(2) 在 APFS 卷上进行即时复制。这就是 Apple 的 cp -c 使用的。文档并不完全清楚,但copyfile(3) with COPYFILE_CLONE 很可能也使用它。如果您希望我对此进行测试,请发表评论。
如果不支持这些写时复制操作——无论是操作系统太旧、底层文件系统不支持它,还是因为你在不同的文件系统之间复制文件——你确实需要回退到尝试sendfile,或者作为最后的手段,逐字节复制。但是为了给大家节省很多时间和磁盘空间,请先试试FICLONE和clonefile(2)。
【讨论】:
一种选择是您可以使用system() 来执行cp。这只是重新使用cp(1) 命令来完成这项工作。如果您只需要创建另一个文件链接,可以使用link() 或symlink() 完成。
【讨论】:
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#define print_err(format, args...) printf("[%s:%d][error]" format "\n", __func__, __LINE__, ##args)
#define DATA_BUF_SIZE (64 * 1024) //limit to read maximum 64 KB data per time
int32_t get_file_size(const char *fname){
struct stat sbuf;
if (NULL == fname || strlen(fname) < 1){
return 0;
}
if (stat(fname, &sbuf) < 0){
print_err("%s, %s", fname, strerror(errno));
return 0;
}
return sbuf.st_size; /* off_t shall be signed interge types, used for file size */
}
bool copyFile(CHAR *pszPathIn, CHAR *pszPathOut)
{
INT32 fdIn, fdOut;
UINT32 ulFileSize_in = 0;
UINT32 ulFileSize_out = 0;
CHAR *szDataBuf;
if (!pszPathIn || !pszPathOut)
{
print_err(" Invalid param!");
return false;
}
if ((1 > strlen(pszPathIn)) || (1 > strlen(pszPathOut)))
{
print_err(" Invalid param!");
return false;
}
if (0 != access(pszPathIn, F_OK))
{
print_err(" %s, %s!", pszPathIn, strerror(errno));
return false;
}
if (0 > (fdIn = open(pszPathIn, O_RDONLY)))
{
print_err("open(%s, ) failed, %s", pszPathIn, strerror(errno));
return false;
}
if (0 > (fdOut = open(pszPathOut, O_CREAT | O_WRONLY | O_TRUNC, 0777)))
{
print_err("open(%s, ) failed, %s", pszPathOut, strerror(errno));
close(fdIn);
return false;
}
szDataBuf = malloc(DATA_BUF_SIZE);
if (NULL == szDataBuf)
{
print_err("malloc() failed!");
return false;
}
while (1)
{
INT32 slSizeRead = read(fdIn, szDataBuf, sizeof(szDataBuf));
INT32 slSizeWrite;
if (slSizeRead <= 0)
{
break;
}
slSizeWrite = write(fdOut, szDataBuf, slSizeRead);
if (slSizeWrite < 0)
{
print_err("write(, , slSizeRead) failed, %s", slSizeRead, strerror(errno));
break;
}
if (slSizeWrite != slSizeRead) /* verify wheter write all byte data successfully */
{
print_err(" write(, , %d) failed!", slSizeRead);
break;
}
}
close(fdIn);
fsync(fdOut); /* causes all modified data and attributes to be moved to a permanent storage device */
close(fdOut);
ulFileSize_in = get_file_size(pszPathIn);
ulFileSize_out = get_file_size(pszPathOut);
if (ulFileSize_in == ulFileSize_out) /* verify again wheter write all byte data successfully */
{
free(szDataBuf);
return true;
}
free(szDataBuf);
return false;
}
【讨论】:
使用普通 POSIX 调用且没有任何循环的复制函数的另一个变体。代码灵感来自 caf 答案的缓冲区复制变体。
警告:在 32 位系统上使用 mmap 很容易失败,在 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;
}
EDIT:更正了文件创建错误。请参阅http://stackoverflow.com/questions/2180079/how-can-i-copy-a-file-on-unix-using-c/2180157#2180157 答案中的评论。
【讨论】:
API 中没有内置的等效 CopyFile 函数。但是sendfile 可用于在内核模式下复制文件,这比打开文件、循环读取文件并将输出写入另一个文件更快更好(出于多种原因)。
更新:
从 Linux 内核版本 2.6.33 开始,要求将 sendfile 的输出作为套接字的限制被取消,原始代码可以在 Linux 上运行,但是,从 OS X 10.9 Mavericks 开始,sendfile在 OS X 上现在要求输出是一个套接字,并且代码将不起作用!
以下代码 sn-p 应该适用于大多数 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。请参阅下面的评论。
【讨论】:
sendfile 的输出参数必须是套接字。你确定这行得通吗?
sendfile 的 out_fd 可能是 2.4 内核中的常规文件,但它现在必须支持 sendpage 内部内核 API(这实际上意味着管道或插座)。 sendpage 在不同的 UNIX 上以不同的方式实现 - 它没有标准语义。
sendfile() 优于 CopyFile(),因为它允许偏移。这对于从文件中剥离标题信息很有用。
有一种方法可以做到这一点,无需使用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 */
}
以上示例(省略了错误检查!)使用了open、close 和sendfile。
编辑:正如 caf 指出的那样,open 和 @ 之间可能会发生 竞态条件 987654328@ 所以我想我会让它更健壮一点...请记住,锁定机制因平台而异...在 Linux 下,lockf 的锁定机制就足够了。如果你想让它可移植,请使用#ifdef 宏来区分不同的平台/编译器...感谢 caf 发现这一点...有一个指向产生“通用锁定例程”here 的站点的链接。
【讨论】:
fdSource 打开的文件和你以stat()ed 打开的文件不一定相同。
open() 调用和 stat() 调用之间,其他人可以重命名文件并在该名称下放置一个不同的文件 - 因此您将从第一个文件,但使用第二个文件的长度。
sprintf( cmd, "/bin/cp -p \'%s\' \'%s\'", old, new);
system( cmd);
添加一些错误检查...
否则,同时打开并循环读取/写入,但可能不是您想要的。
...
更新以解决有效的安全问题:
与其使用“system()”,不如执行 fork/wait,并在子进程中调用 execv() 或 execl()。
execl( "/bin/cp", "-p", old, new);
【讨论】:
old 或new 值中的单引号字符,则存在shell 代码注入漏洞。多一点努力使用 fork 并自己执行 exec 可以避免所有这些引用问题。
直接使用 fork/execl 运行 cp 为您完成工作。这与系统相比具有优势,因为它不容易受到 Bobby Tables 攻击,并且您不需要对参数进行相同程度的清理。此外,由于 system() 要求您将命令参数拼凑在一起,因此您不太可能因为 sprintf() 检查草率而遇到缓冲区溢出问题。
直接调用 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 */
{
}
}
}
【讨论】:
不需要调用像sendfile 这样的不可移植的API,也不需要使用外部实用程序。 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;
}
【讨论】:
goto 的受控使用对于将错误处理路径整合到一个位置很有用。
write() 循环中处理EINTR,但不在read() 循环中。