Subject: bug#23904: Btrfs clone support in copy operations

From: Paul Eggert <[email-masked]>
Date: Wed, 6 Jul 2016 13:48:26 +0200

On 07/06/2016 04:45 AM, Dmitry Antipov wrote:
> IMHO this should use FICLONERANGE in a loop where you can
> handle errors and use process_pending_signals as usual. 

Thanks, that suggestion should resolve the FIXME about how FICLONE 
behaves when the output file is already larger than the input. However, 
what should the clone chunk size be,each time through the loop? I don't 
use btrfs so I don't know what a good size would be.

A downside of the suggestion is that the file copy won't be atomic. 
However, the existing code isn't atomic, so this is nothing new.

Revised patch attached. Basically untested, since I don't use any file 
systems that support FIOCLONERANGE. The patch still has FIXMEs for what 
happens if the FIOCLONERANGE ioctl is interrupted by a signal, and for a 
good clone chunk size (the patch guesses 1 GiB).
>From 9c60f6f603563c335737102fc54609b837f5d02a Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email-masked]>
Date: Wed, 6 Jul 2016 12:44:46 +0200
Subject: [PATCH] copy-file now uses GNU/Linux file cloning

>From a suggestion by Kieran Colford (see Bug#23904).
* configure.ac: Check for linux/fs.h.
* src/fileio.c [HAVE_LINUX_FS_H]: Include sys/ioctl.h and linux/fs.h.
(clone_file_range): New function.
(Fcopy_file): Use it.
---
 configure.ac |  1 +
 src/fileio.c | 62 +++++++++++++++++++++++++++++++++++++++++++++++-------------
 2 files changed, 50 insertions(+), 13 deletions(-)

diff --git a/configure.ac b/configure.ac
index aaddfcd..1798fa3 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1636,6 +1636,7 @@ AC_DEFUN
 
 dnl checks for header files
 AC_CHECK_HEADERS_ONCE(
+  linux/fs.h
   malloc.h
   sys/systeminfo.h
   sys/sysinfo.h
diff --git a/src/fileio.c b/src/fileio.c
index b1f9d3c..f980247 100644
--- a/src/fileio.c
+++ b/src/fileio.c
@@ -52,6 +52,11 @@ along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.  */
 #include "region-cache.h"
 #include "frame.h"
 
+#ifdef HAVE_LINUX_FS_H
+# include <sys/ioctl.h>
+# include <linux/fs.h>
+#endif
+
 #ifdef WINDOWSNT
 #define NOMINMAX 1
 #include <windows.h>
@@ -1828,6 +1833,23 @@ barf_or_query_if_file_exists (Lisp_Object absname, bool known_to_exist,
     }
 }
 
+/* Copy data to DEST from SOURCE if possible, starting at byte offset
+   OFF and continuing for LENGTH bytes.  Return true if OK.  */
+static bool
+clone_file_range (int dest, int source, off_t off, off_t length)
+{
+#ifdef FICLONERANGE
+  /* FIXME: Is the ioctl interruptible?  If so, what happens if the
+     ioctl is interrupted by a signal?  Might it partially copy the
+     range?  */
+  struct file_clone_range range = { src_fd: source, src_offset: off,
+				    src_length: length, dest_offset: off };
+  if (ioctl (dest, FICLONERANGE, &range) == 0)
+    return true;
+#endif
+  return false;
+}
+
 DEFUN ("copy-file", Fcopy_file, Scopy_file, 2, 6,
        "fCopy file: \nGCopy %s to file: \np\nP",
        doc: /* Copy FILE to NEWNAME.  Both args must be strings.
@@ -1974,7 +1996,7 @@ permissions.  */)
 
   record_unwind_protect_int (close_file_unwind, ofd);
 
-  off_t oldsize = 0, newsize = 0;
+  off_t oldsize = 0, newsize;
 
   if (already_exists)
     {
@@ -1990,18 +2012,32 @@ permissions.  */)
 
   immediate_quit = 1;
   QUIT;
-  while (true)
-    {
-      char buf[MAX_ALLOCA];
-      ptrdiff_t n = emacs_read (ifd, buf, sizeof buf);
-      if (n < 0)
-	report_file_error ("Read error", file);
-      if (n == 0)
-	break;
-      if (emacs_write_sig (ofd, buf, n) != n)
-	report_file_error ("Write error", newname);
-      newsize += n;
-    }
+
+  off_t insize = st.st_size;
+  /* FIXME: is 1 GiB a good number?  */
+  off_t max_clone_chunk_size = 1 << 30;
+  off_t chunk_size = min (insize, max_clone_chunk_size);
+
+  if (clone_file_range (ofd, ifd, 0, chunk_size))
+    for (newsize = chunk_size; newsize < insize; newsize += chunk_size)
+      {
+	chunk_size = min (insize - newsize, max_clone_chunk_size);
+	if (! clone_file_range (ofd, ifd, newsize, chunk_size))
+	  report_file_error ("Clone error", file);
+      }
+  else
+    for (newsize = 0; ; newsize += chunk_size)
+      {
+	char buf[MAX_ALLOCA];
+	ptrdiff_t n = emacs_read (ifd, buf, sizeof buf);
+	if (n < 0)
+	  report_file_error ("Read error", file);
+	if (n == 0)
+	  break;
+	if (emacs_write_sig (ofd, buf, n) != n)
+	  report_file_error ("Write error", newname);
+	chunk_size = n;
+      }
 
   /* Truncate any existing output file after writing the data.  This
      is more likely to work than truncation before writing, if the
-- 
2.5.5