commit f3873b3d88b61167b106e7b9227a20147f8f6197 Author: Wayne Davison Date: Mon Oct 10 11:49:50 2016 -0700 Support --sparse combined with --preallocate or --inplace. The new code tries to punch holes in the destination file using newer Linux fallocate features. It also supports a --whole-file + --sparse + --inplace copy on any filesystem by truncating the destination file. diff --git a/configure.ac b/configure.ac index b5e4049..e01e124 100644 --- a/configure.ac +++ b/configure.ac @@ -614,6 +614,36 @@ if test x"$rsync_cv_have_fallocate" = x"yes"; then AC_DEFINE(HAVE_FALLOCATE, 1, [Define to 1 if you have the fallocate function and it compiles and links without error]) fi +AC_MSG_CHECKING([for FALLOC_FL_PUNCH_HOLE]) +AC_PREPROC_IFELSE([AC_LANG_SOURCE([[ + #define _GNU_SOURCE 1 + #include + #ifndef FALLOC_FL_PUNCH_HOLE + #error FALLOC_FL_PUNCH_HOLE is missing + #endif + ]])], [ + AC_MSG_RESULT([yes]) + AC_DEFINE([HAVE_FALLOC_FL_PUNCH_HOLE], [1], [Define if FALLOC_FL_PUNCH_HOLE is available.]) + ], [ + AC_MSG_RESULT([no]) + ] +) + +AC_MSG_CHECKING([for FALLOC_FL_ZERO_RANGE]) +AC_PREPROC_IFELSE([AC_LANG_SOURCE([[ + #define _GNU_SOURCE 1 + #include + #ifndef FALLOC_FL_ZERO_RANGE + #error FALLOC_FL_ZERO_RANGE is missing + #endif + ]])], [ + AC_MSG_RESULT([yes]) + AC_DEFINE([HAVE_FALLOC_FL_ZERO_RANGE], [1], [Define if FALLOC_FL_ZERO_RANGE is available.]) + ], [ + AC_MSG_RESULT([no]) + ] +) + AC_CACHE_CHECK([for SYS_fallocate],rsync_cv_have_sys_fallocate,[ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[#include #include ]], [[syscall(SYS_fallocate, 0, 0, (loff_t)0, (loff_t)0);]])],[rsync_cv_have_sys_fallocate=yes],[rsync_cv_have_sys_fallocate=no])]) diff --git a/fileio.c b/fileio.c index 70e079d..1e8a562 100644 --- a/fileio.c +++ b/fileio.c @@ -35,7 +35,10 @@ extern int sparse_files; +OFF_T preallocated_len = 0; + static OFF_T sparse_seek = 0; +static OFF_T sparse_past_write = 0; int sparse_end(int f, OFF_T size) { @@ -63,8 +66,10 @@ int sparse_end(int f, OFF_T size) return ret; } - -static int write_sparse(int f, char *buf, int len) +/* Note that the offset is just the caller letting us know where + * the current file position is in the file. The use_seek arg tells + * us that we should seek over matching data instead of writing it. */ +static int write_sparse(int f, int use_seek, OFF_T offset, const char *buf, int len) { int l1 = 0, l2 = 0; int ret; @@ -77,9 +82,24 @@ static int write_sparse(int f, char *buf, int len) if (l1 == len) return len; - if (sparse_seek) - do_lseek(f, sparse_seek, SEEK_CUR); + if (sparse_seek) { + if (sparse_past_write >= preallocated_len) { + if (do_lseek(f, sparse_seek, SEEK_CUR) < 0) + return -1; + } else if (do_punch_hole(f, sparse_past_write, sparse_seek) < 0) { + sparse_seek = 0; + return -1; + } + } sparse_seek = l2; + sparse_past_write = offset + len - l2; + + if (use_seek) { + /* The in-place data already matches. */ + if (do_lseek(f, len - (l1+l2), SEEK_CUR) < 0) + return -1; + return len; + } while ((ret = write(f, buf + l1, len - (l1+l2))) <= 0) { if (ret < 0 && errno == EINTR) @@ -96,7 +116,6 @@ static int write_sparse(int f, char *buf, int len) return len; } - static char *wf_writeBuf; static size_t wf_writeBufSize; static size_t wf_writeBufCnt; @@ -118,12 +137,10 @@ int flush_write_file(int f) return ret; } - -/* - * write_file does not allow incomplete writes. It loops internally - * until len bytes are written or errno is set. - */ -int write_file(int f, char *buf, int len) +/* write_file does not allow incomplete writes. It loops internally + * until len bytes are written or errno is set. Note that use_seek and + * offset are only used in sparse processing (see write_sparse()). */ +int write_file(int f, int use_seek, OFF_T offset, const char *buf, int len) { int ret = 0; @@ -131,7 +148,8 @@ int write_file(int f, char *buf, int len) int r1; if (sparse_files > 0) { int len1 = MIN(len, SPARSE_WRITE_SIZE); - r1 = write_sparse(f, buf, len1); + r1 = write_sparse(f, use_seek, offset, buf, len1); + offset += r1; } else { if (!wf_writeBuf) { wf_writeBufSize = WRITE_SIZE * 8; @@ -164,6 +182,30 @@ int write_file(int f, char *buf, int len) return ret; } +/* An in-place update found identical data at an identical location. We either + * just seek past it, or (for an in-place sparse update), we give the data to + * the sparse processor with the use_seek flag set. */ +int skip_matched(int fd, OFF_T offset, const char *buf, int len) +{ + OFF_T pos; + + if (sparse_files > 0) { + if (write_file(fd, 1, offset, buf, len) != len) + return -1; + return 0; + } + + if (flush_write_file(fd) < 0) + return -1; + + if ((pos = do_lseek(fd, len, SEEK_CUR)) != offset + len) { + rsyserr(FERROR_XFER, errno, "lseek returned %s, not %s", + big_num(pos), big_num(offset)); + return -1; + } + + return 0; +} /* This provides functionality somewhat similar to mmap() but using read(). * It gives sliding window access to a file. mmap() is not used because of @@ -271,7 +313,6 @@ char *map_ptr(struct map_struct *map, OFF_T offset, int32 len) return map->p + align_fudge; } - int unmap_file(struct map_struct *map) { int ret; diff --git a/options.c b/options.c index 308443b..6ba13b7 100644 --- a/options.c +++ b/options.c @@ -714,7 +714,7 @@ void usage(enum logcode F) #ifdef SUPPORT_XATTRS rprintf(F," --fake-super store/recover privileged attrs using xattrs\n"); #endif - rprintf(F," -S, --sparse handle sparse files efficiently\n"); + rprintf(F," -S, --sparse turn sequences of nulls into sparse blocks\n"); #ifdef SUPPORT_PREALLOCATION rprintf(F," --preallocate allocate dest files before writing them\n"); #else @@ -2237,14 +2237,6 @@ int parse_arguments(int *argc_p, const char ***argv_p) bwlimit_writemax = 512; } - if (sparse_files && inplace) { - /* Note: we don't check for this below, because --append is - * OK with --sparse (as long as redos are handled right). */ - snprintf(err_buf, sizeof err_buf, - "--sparse cannot be used with --inplace\n"); - return 0; - } - if (append_mode) { if (whole_file > 0) { snprintf(err_buf, sizeof err_buf, diff --git a/receiver.c b/receiver.c index f9b97dd..bed5328 100644 --- a/receiver.c +++ b/receiver.c @@ -49,6 +49,7 @@ extern int sparse_files; extern int preallocate_files; extern int keep_partial; extern int checksum_seed; +extern int whole_file; extern int inplace; extern int allowed_lull; extern int delay_updates; @@ -61,6 +62,9 @@ extern char *basis_dir[MAX_BASIS_DIRS+1]; extern char sender_file_sum[MAX_DIGEST_LEN]; extern struct file_list *cur_flist, *first_flist, *dir_flist; extern filter_rule_list daemon_filter_list; +#ifdef SUPPORT_PREALLOCATION +extern OFF_T preallocated_len; +#endif static struct bitbag *delayed_bits = NULL; static int phase = 0, redoing = 0; @@ -241,22 +245,25 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r, char *data; int32 i; char *map = NULL; -#ifdef SUPPORT_PREALLOCATION -#ifdef PREALLOCATE_NEEDS_TRUNCATE - OFF_T preallocated_len = 0; -#endif +#ifdef SUPPORT_PREALLOCATION if (preallocate_files && fd != -1 && total_size > 0 && (!inplace || total_size > size_r)) { /* Try to preallocate enough space for file's eventual length. Can * reduce fragmentation on filesystems like ext4, xfs, and NTFS. */ - if (do_fallocate(fd, 0, total_size) == 0) { -#ifdef PREALLOCATE_NEEDS_TRUNCATE - preallocated_len = total_size; -#endif - } else + if ((preallocated_len = do_fallocate(fd, 0, total_size)) < 0) rsyserr(FWARNING, errno, "do_fallocate %s", full_fname(fname)); - } + } else +#endif + if (inplace) { +#ifdef HAVE_FTRUNCATE + /* The most compatible way to create a sparse file is to start with no length. */ + if (sparse_files > 0 && whole_file && fd >= 0 && do_ftruncate(fd, 0) == 0) + preallocated_len = 0; + else #endif + preallocated_len = size_r; + } else + preallocated_len = 0; read_sum_head(f_in, &sum); @@ -318,7 +325,7 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r, sum_update(data, i); - if (fd != -1 && write_file(fd,data,i) != i) + if (fd != -1 && write_file(fd, 0, offset, data, i) != i) goto report_write_error; offset += i; continue; @@ -348,37 +355,33 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r, if (updating_basis_or_equiv) { if (offset == offset2 && fd != -1) { - OFF_T pos; - if (flush_write_file(fd) < 0) + if (skip_matched(fd, offset, map, len) < 0) goto report_write_error; offset += len; - if ((pos = do_lseek(fd, len, SEEK_CUR)) != offset) { - rsyserr(FERROR_XFER, errno, - "lseek of %s returned %s, not %s", - full_fname(fname), - big_num(pos), big_num(offset)); - exit_cleanup(RERR_FILEIO); - } continue; } } - if (fd != -1 && map && write_file(fd, map, len) != (int)len) + if (fd != -1 && map && write_file(fd, 0, offset, map, len) != (int)len) goto report_write_error; offset += len; } - if (flush_write_file(fd) < 0) - goto report_write_error; + if (fd != -1 && offset > 0) { + if (sparse_files > 0) { + if (sparse_end(fd, offset) != 0) + goto report_write_error; + } else if (flush_write_file(fd) < 0) { + report_write_error: + rsyserr(FERROR_XFER, errno, "write failed on %s", full_fname(fname)); + exit_cleanup(RERR_FILEIO); + } + } #ifdef HAVE_FTRUNCATE /* inplace: New data could be shorter than old data. * preallocate_files: total_size could have been an overestimate. * Cut off any extra preallocated zeros from dest file. */ - if ((inplace -#ifdef PREALLOCATE_NEEDS_TRUNCATE - || preallocated_len > offset -#endif - ) && fd != -1 && do_ftruncate(fd, offset) < 0) { + if ((inplace || preallocated_len > offset) && fd != -1 && do_ftruncate(fd, offset) < 0) { rsyserr(FERROR_XFER, errno, "ftruncate failed on %s", full_fname(fname)); } @@ -387,13 +390,6 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r, if (INFO_GTE(PROGRESS, 1)) end_progress(total_size); - if (fd != -1 && offset > 0 && sparse_end(fd, offset) != 0) { - report_write_error: - rsyserr(FERROR_XFER, errno, "write failed on %s", - full_fname(fname)); - exit_cleanup(RERR_FILEIO); - } - checksum_len = sum_end(file_sum1); if (mapbuf) diff --git a/rsync.yo b/rsync.yo index bfe43b9..d1e6fdf 100644 --- a/rsync.yo +++ b/rsync.yo @@ -376,7 +376,7 @@ to the detailed description below for a complete description. verb( -J, --omit-link-times omit symlinks from --times --super receiver attempts super-user activities --fake-super store/recover privileged attrs using xattrs - -S, --sparse handle sparse files efficiently + -S, --sparse turn sequences of nulls into sparse blocks --preallocate allocate dest files before writing -n, --dry-run perform a trial run with no changes made -W, --whole-file copy files whole (w/o delta-xfer algorithm) @@ -873,9 +873,7 @@ the same or longer than the size on the sender, the file is skipped. This does not interfere with the updating of a file's non-content attributes (e.g. permissions, ownership, etc.) when the file does not need to be transferred, nor does it affect the updating of any non-regular files. -Implies bf(--inplace), -but does not conflict with bf(--sparse) (since it is always extending a -file's length). +Implies bf(--inplace). The use of bf(--append) can be dangerous if you aren't 100% sure that the files that are longer have only grown by the appending of data onto the end. You @@ -1252,20 +1250,30 @@ This option is overridden by both bf(--super) and bf(--no-super). See also the "fake super" setting in the daemon's rsyncd.conf file. dit(bf(-S, --sparse)) Try to handle sparse files efficiently so they take -up less space on the destination. Conflicts with bf(--inplace) because it's -not possible to overwrite data in a sparse fashion. +up less space on the destination. If combined with bf(--inplace) the +file created might not end up with sparse blocks with some combinations +of kernel version and/or filesystem type. If bf(--whole-file) is in +effect (e.g. for a local copy) then it will always work because rsync +truncates the file prior to writing out the updated version. + +Note that versions of rsync older than 3.1.3 will reject the combination of +bf(--sparse) and bf(--inplace). dit(bf(--preallocate)) This tells the receiver to allocate each destination -file to its eventual size before writing data to the file. Rsync will only use -the real filesystem-level preallocation support provided by Linux's +file to its eventual size before writing data to the file. Rsync will only +use the real filesystem-level preallocation support provided by Linux's bf(fallocate)(2) system call or Cygwin's bf(posix_fallocate)(3), not the slow -glibc implementation that writes a zero byte into each block. +glibc implementation that writes a null byte into each block. Without this option, larger files may not be entirely contiguous on the filesystem, but with this option rsync will probably copy more slowly. If the destination is not an extent-supporting filesystem (such as ext4, xfs, NTFS, etc.), this option may have no positive effect at all. +If combined with bf(--sparse), the file will only have sparse blocks (as +opposed to allocated sequences of null bytes) if the kernel version and +filesystem type support creating holes in the allocated data. + dit(bf(-n, --dry-run)) This makes rsync perform a trial run that doesn't make any changes (and produces mostly the same output as a real run). It is most commonly used in combination with the bf(-v, --verbose) and/or diff --git a/syscall.c b/syscall.c index ecca2f1..fa53b63 100644 --- a/syscall.c +++ b/syscall.c @@ -38,6 +38,8 @@ extern int am_root; extern int am_sender; extern int read_only; extern int list_only; +extern int inplace; +extern int preallocate_files; extern int preserve_perms; extern int preserve_executability; @@ -423,27 +425,80 @@ int do_utime(const char *fname, time_t modtime, UNUSED(uint32 mod_nsec)) #endif #ifdef SUPPORT_PREALLOCATION -int do_fallocate(int fd, OFF_T offset, OFF_T length) -{ #ifdef FALLOC_FL_KEEP_SIZE #define DO_FALLOC_OPTIONS FALLOC_FL_KEEP_SIZE #else #define DO_FALLOC_OPTIONS 0 #endif + +OFF_T do_fallocate(int fd, OFF_T offset, OFF_T length) +{ + int opts = inplace || preallocate_files ? 0 : DO_FALLOC_OPTIONS; + int ret; RETURN_ERROR_IF(dry_run, 0); RETURN_ERROR_IF_RO_OR_LO; + if (length & 1) /* make the length not match the desired length */ + length++; + else + length--; #if defined HAVE_FALLOCATE - return fallocate(fd, DO_FALLOC_OPTIONS, offset, length); + ret = fallocate(fd, opts, offset, length); #elif defined HAVE_SYS_FALLOCATE - return syscall(SYS_fallocate, fd, DO_FALLOC_OPTIONS, (loff_t)offset, (loff_t)length); + ret = syscall(SYS_fallocate, fd, opts, (loff_t)offset, (loff_t)length); #elif defined HAVE_EFFICIENT_POSIX_FALLOCATE - return posix_fallocate(fd, offset, length); + ret = posix_fallocate(fd, offset, length); #else #error Coding error in SUPPORT_PREALLOCATION logic. #endif + if (ret < 0) + return ret; + if (opts == 0) { + STRUCT_STAT st; + if (do_fstat(fd, &st) < 0) + return length; + return st.st_blocks * 512; + } + return 0; } #endif +/* Punch a hole at pos for len bytes. The current file position must be at pos and will be + * changed to be at pos + len. */ +int do_punch_hole(int fd, UNUSED(OFF_T pos), int len) +{ +#ifdef HAVE_FALLOCATE +# ifdef HAVE_FALLOC_FL_PUNCH_HOLE + if (fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, pos, len) == 0) { + if (do_lseek(fd, len, SEEK_CUR) != pos + len) + return -1; + return 0; + } +# endif +# ifdef HAVE_FALLOC_FL_ZERO_RANGE + if (fallocate(fd, FALLOC_FL_ZERO_RANGE, pos, len) == 0) { + if (do_lseek(fd, len, SEEK_CUR) != pos + len) + return -1; + return 0; + } +# endif +#endif + { + char zeros[4096]; + memset(zeros, 0, sizeof zeros); + while (len > 0) { + int chunk = len > (int)sizeof zeros ? (int)sizeof zeros : len; + int wrote = write(fd, zeros, chunk); + if (wrote <= 0) { + if (wrote < 0 && errno == EINTR) + continue; + return -1; + } + len -= wrote; + } + } + return 0; +} + int do_open_nofollow(const char *pathname, int flags) { #ifndef O_NOFOLLOW diff --git a/t_stub.c b/t_stub.c index 26951a6..fc1ee3b 100644 --- a/t_stub.c +++ b/t_stub.c @@ -21,6 +21,7 @@ #include "rsync.h" +int inplace = 0; int modify_window = 0; int preallocate_files = 0; int protect_args = 0; diff --git a/tls.c b/tls.c index 45d1e10..d5a2896 100644 --- a/tls.c +++ b/tls.c @@ -51,6 +51,8 @@ int link_owner = 0; int nsec_times = 0; int preserve_perms = 0; int preserve_executability = 0; +int preallocate_files = 0; +int inplace = 0; #ifdef SUPPORT_XATTRS diff --git a/trimslash.c b/trimslash.c index 207eaf2..5db6f3e 100644 --- a/trimslash.c +++ b/trimslash.c @@ -28,6 +28,8 @@ int read_only = 1; int list_only = 0; int preserve_perms = 0; int preserve_executability = 0; +int preallocate_files = 0; +int inplace = 0; int main(int argc, char **argv) diff --git a/util.c b/util.c index ca38f3e..49c5b71 100644 --- a/util.c +++ b/util.c @@ -323,9 +323,7 @@ int copy_file(const char *source, const char *dest, int ofd, mode_t mode) int ifd; char buf[1024 * 8]; int len; /* Number of bytes read into `buf'. */ -#ifdef PREALLOCATE_NEEDS_TRUNCATE - OFF_T preallocated_len = 0, offset = 0; -#endif + OFF_T prealloc_len = 0, offset = 0; if ((ifd = do_open(source, O_RDONLY, 0)) < 0) { int save_errno = errno; @@ -365,11 +363,8 @@ int copy_file(const char *source, const char *dest, int ofd, mode_t mode) if (do_fstat(ifd, &srcst) < 0) rsyserr(FWARNING, errno, "fstat %s", full_fname(source)); else if (srcst.st_size > 0) { - if (do_fallocate(ofd, 0, srcst.st_size) == 0) { -#ifdef PREALLOCATE_NEEDS_TRUNCATE - preallocated_len = srcst.st_size; -#endif - } else + prealloc_len = do_fallocate(ofd, 0, srcst.st_size); + if (prealloc_len < 0) rsyserr(FWARNING, errno, "do_fallocate %s", full_fname(dest)); } } @@ -384,9 +379,7 @@ int copy_file(const char *source, const char *dest, int ofd, mode_t mode) errno = save_errno; return -1; } -#ifdef PREALLOCATE_NEEDS_TRUNCATE offset += len; -#endif } if (len < 0) { @@ -403,15 +396,13 @@ int copy_file(const char *source, const char *dest, int ofd, mode_t mode) full_fname(source)); } -#ifdef PREALLOCATE_NEEDS_TRUNCATE /* Source file might have shrunk since we fstatted it. * Cut off any extra preallocated zeros from dest file. */ - if (offset < preallocated_len && do_ftruncate(ofd, offset) < 0) { + if (offset < prealloc_len && do_ftruncate(ofd, offset) < 0) { /* If we fail to truncate, the dest file may be wrong, so we * must trigger the "partial transfer" error. */ rsyserr(FERROR_XFER, errno, "ftruncate %s", full_fname(dest)); } -#endif if (close(ofd) < 0) { int save_errno = errno;