From: Paul Eggert Date: Thu, 25 Jun 2020 23:31:44 +0000 (-0700) Subject: cp: use SEEK_DATA/SEEK_HOLE if available X-Git-Tag: v9.0~228 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=a6eaee501f6ec0c152abe88640203a64c390993e;p=thirdparty%2Fcoreutils.git cp: use SEEK_DATA/SEEK_HOLE if available If it works, prefer lseek with SEEK_DATA and SEEK_HOLE to FIEMAP, as lseek is simpler and more portable (will be in next POSIX). Problem reported in 2011 by Jeff Liu (Bug#8061). * NEWS: Mention this. * src/copy.c (lseek_copy) [SEEK_HOLE]: New function. (enum scantype): New constants ERROR_SCANTYPE, LSEEK_SCANTYPE. (union scan_inference): New type. (infer_scantype): Last arg is now union scan_inference *, not struct extent_scan *. All callers changed. Prefer SEEK_HOLE to FIEMAP if both work, since SEEK_HOLE is simpler and more portable. (copy_reg): Do the fdadvise after initial scan, in case the scan fails. Report an error if the initial scan fails. (copy_reg) [SEEK_HOLE]: Use lseek_copy if scantype says so. --- diff --git a/NEWS b/NEWS index d713fa7245..63cb47d105 100644 --- a/NEWS +++ b/NEWS @@ -17,6 +17,9 @@ GNU coreutils NEWS -*- outline -*- cp and install now default to copy-on-write (COW) if available. + cp, install and mv now prefer lseek+SEEK_HOLE to ioctl+FS_IOC_FIEMAP + on sparse files, as lseek is simpler and more portable. + On GNU/Linux systems, ls no longer issues an error message on a directory merely because it was removed. This reverts a change that was made in release 8.32. diff --git a/src/copy.c b/src/copy.c index b382cfa4d1..d88f8cf930 100644 --- a/src/copy.c +++ b/src/copy.c @@ -416,7 +416,12 @@ write_zeros (int fd, off_t n_bytes) Upon a successful copy, return true. If the initial extent scan fails, set *NORMAL_COPY_REQUIRED to true and return false. Upon any other failure, set *NORMAL_COPY_REQUIRED to false and - return false. */ + return false. + + FIXME: Once we no longer need to support Linux kernel versions + before 3.1 (2011), this function can be retired as it is superseded + by lseek_copy. That is, we no longer need extent-scan.h and can + remove any of the code that uses it. */ static bool extent_copy (int src_fd, int dest_fd, char *buf, size_t buf_size, size_t hole_size, off_t src_total_size, @@ -595,6 +600,150 @@ extent_copy (int src_fd, int dest_fd, char *buf, size_t buf_size, return true; } +#ifdef SEEK_HOLE +/* Perform an efficient extent copy, if possible. This avoids + the overhead of detecting holes in hole-introducing/preserving + copy, and thus makes copying sparse files much more efficient. + Copy from SRC_FD to DEST_FD, using BUF (of size BUF_SIZE) for a buffer. + Look for holes of size HOLE_SIZE in the input. + The input file is of size SRC_TOTAL_SIZE. + Use SPARSE_MODE to determine whether to create holes in the output. + SRC_NAME and DST_NAME are the input and output file names. + Return true if successful, false (with a diagnostic) otherwise. */ + +static bool +lseek_copy (int src_fd, int dest_fd, char *buf, size_t buf_size, + size_t hole_size, off_t ext_start, off_t src_total_size, + enum Sparse_type sparse_mode, + char const *src_name, char const *dst_name) +{ + off_t last_ext_start = 0; + off_t last_ext_len = 0; + off_t dest_pos = 0; + bool wrote_hole_at_eof = true; + + while (0 <= ext_start) + { + off_t ext_end = lseek (src_fd, ext_start, SEEK_HOLE); + if (ext_end < 0) + { + if (errno != ENXIO) + goto cannot_lseek; + ext_end = src_total_size; + if (ext_end <= ext_start) + { + /* The input file grew; get its current size. */ + src_total_size = lseek (src_fd, 0, SEEK_END); + if (src_total_size < 0) + goto cannot_lseek; + + /* If the input file shrank after growing, stop copying. */ + if (src_total_size <= ext_start) + break; + + ext_end = src_total_size; + } + } + /* If the input file must have grown, increase its measured size. */ + if (src_total_size < ext_end) + src_total_size = ext_end; + + if (lseek (src_fd, ext_start, SEEK_SET) < 0) + goto cannot_lseek; + + wrote_hole_at_eof = false; + off_t ext_hole_size = ext_start - last_ext_start - last_ext_len; + + if (ext_hole_size) + { + if (sparse_mode != SPARSE_NEVER) + { + if (! create_hole (dest_fd, dst_name, + sparse_mode == SPARSE_ALWAYS, + ext_hole_size)) + return false; + wrote_hole_at_eof = true; + } + else + { + /* When not inducing holes and when there is a hole between + the end of the previous extent and the beginning of the + current one, write zeros to the destination file. */ + if (! write_zeros (dest_fd, ext_hole_size)) + { + error (0, errno, _("%s: write failed"), + quotef (dst_name)); + return false; + } + } + } + + off_t ext_len = ext_end - ext_start; + last_ext_start = ext_start; + last_ext_len = ext_len; + + /* Copy this extent, looking for further opportunities to not + bother to write zeros unless --sparse=never, since SEEK_HOLE + is conservative and may miss some holes. */ + off_t n_read; + bool read_hole; + if ( ! sparse_copy (src_fd, dest_fd, buf, buf_size, + sparse_mode == SPARSE_NEVER ? 0 : hole_size, + true, src_name, dst_name, ext_len, &n_read, + &read_hole)) + return false; + + dest_pos = ext_start + n_read; + if (n_read) + wrote_hole_at_eof = read_hole; + if (n_read < ext_len) + { + /* The input file shrank. */ + src_total_size = dest_pos; + break; + } + + ext_start = lseek (src_fd, dest_pos, SEEK_DATA); + if (ext_start < 0) + { + if (errno != ENXIO) + goto cannot_lseek; + break; + } + } + + /* When the source file ends with a hole, we have to do a little more work, + since the above copied only up to and including the final extent. + In order to complete the copy, we may have to insert a hole or write + zeros in the destination corresponding to the source file's hole-at-EOF. + + In addition, if the final extent was a block of zeros at EOF and we've + just converted them to a hole in the destination, we must call ftruncate + here in order to record the proper length in the destination. */ + if ((dest_pos < src_total_size || wrote_hole_at_eof) + && ! (sparse_mode == SPARSE_NEVER + ? write_zeros (dest_fd, src_total_size - dest_pos) + : ftruncate (dest_fd, src_total_size) == 0)) + { + error (0, errno, _("failed to extend %s"), quoteaf (dst_name)); + return false; + } + + if (sparse_mode == SPARSE_ALWAYS && dest_pos < src_total_size + && punch_hole (dest_fd, dest_pos, src_total_size - dest_pos) < 0) + { + error (0, errno, _("error deallocating %s"), quoteaf (dst_name)); + return false; + } + + return true; + + cannot_lseek: + error (0, errno, _("cannot lseek %s"), quoteaf (src_name)); + return false; +} +#endif + /* FIXME: describe */ /* FIXME: rewrite this to use a hash table so we avoid the quadratic performance hit that's probably noticeable only on trees deeper @@ -1010,6 +1159,9 @@ fchmod_or_lchmod (int desc, char const *name, mode_t mode) /* Type of scan being done on the input when looking for sparseness. */ enum scantype { + /* An error was found when determining scantype. */ + ERROR_SCANTYPE, + /* No fancy scanning; just read and write. */ PLAIN_SCANTYPE, @@ -1017,22 +1169,44 @@ enum scantype attempting to create sparse output. */ ZERO_SCANTYPE, + /* lseek information is available. */ + LSEEK_SCANTYPE, + /* Extent information is available. */ EXTENT_SCANTYPE }; -/* Use a heuristic to determine whether stat buffer SB comes from a file - with sparse blocks. If the file has fewer blocks than would normally - be needed for a file of its size, then at least one of the blocks in - the file is a hole. In that case, return true. */ +/* Result of infer_scantype. */ +union scan_inference +{ + /* Used if infer_scantype returns LSEEK_SCANTYPE. This is the + offset of the first data block, or -1 if the file has no data. */ + off_t ext_start; + + /* Used if infer_scantype returns EXTENT_SCANTYPE. */ + struct extent_scan extent_scan; +}; + +/* Return how to scan a file with descriptor FD and stat buffer SB. + Store any information gathered into *SCAN. */ static enum scantype -infer_scantype (int fd, struct stat const *sb, struct extent_scan *scan) +infer_scantype (int fd, struct stat const *sb, + union scan_inference *scan_inference) { if (! (HAVE_STRUCT_STAT_ST_BLOCKS && S_ISREG (sb->st_mode) && ST_NBLOCKS (*sb) < sb->st_size / ST_NBLOCKSIZE)) return PLAIN_SCANTYPE; +#ifdef SEEK_HOLE + scan_inference->ext_start = lseek (fd, 0, SEEK_DATA); + if (0 <= scan_inference->ext_start) + return LSEEK_SCANTYPE; + else if (errno != EINVAL && errno != ENOTSUP) + return errno == ENXIO ? LSEEK_SCANTYPE : ERROR_SCANTYPE; +#endif + + struct extent_scan *scan = &scan_inference->extent_scan; extent_scan_init (fd, scan); extent_scan_read (scan); return scan->initial_scan_failed ? ZERO_SCANTYPE : EXTENT_SCANTYPE; @@ -1066,7 +1240,7 @@ copy_reg (char const *src_name, char const *dst_name, mode_t src_mode = src_sb->st_mode; struct stat sb; struct stat src_open_sb; - struct extent_scan scan; + union scan_inference scan_inference; bool return_val = true; bool data_copy_required = x->data_copy_required; @@ -1263,17 +1437,23 @@ copy_reg (char const *src_name, char const *dst_name, size_t buf_size = io_blksize (sb); size_t hole_size = ST_BLKSIZE (sb); - fdadvise (source_desc, 0, 0, FADVISE_SEQUENTIAL); - /* Deal with sparse files. */ enum scantype scantype = infer_scantype (source_desc, &src_open_sb, - &scan); + &scan_inference); + if (scantype == ERROR_SCANTYPE) + { + error (0, errno, _("cannot lseek %s"), quoteaf (src_name)); + return_val = false; + goto close_src_and_dst_desc; + } bool make_holes = (S_ISREG (sb.st_mode) && (x->sparse_mode == SPARSE_ALWAYS || (x->sparse_mode == SPARSE_AUTO && scantype != PLAIN_SCANTYPE))); + fdadvise (source_desc, 0, 0, FADVISE_SEQUENTIAL); + /* If not making a sparse file, try to use a more-efficient buffer size. */ if (! make_holes) @@ -1307,7 +1487,14 @@ copy_reg (char const *src_name, char const *dst_name, ? extent_copy (source_desc, dest_desc, buf, buf_size, hole_size, src_open_sb.st_size, make_holes ? x->sparse_mode : SPARSE_NEVER, - src_name, dst_name, &scan) + src_name, dst_name, &scan_inference.extent_scan) +#ifdef SEEK_HOLE + : scantype == LSEEK_SCANTYPE + ? lseek_copy (source_desc, dest_desc, buf, buf_size, hole_size, + scan_inference.ext_start, src_open_sb.st_size, + make_holes ? x->sparse_mode : SPARSE_NEVER, + src_name, dst_name) +#endif : sparse_copy (source_desc, dest_desc, buf, buf_size, make_holes ? hole_size : 0, x->sparse_mode == SPARSE_ALWAYS,