diff --git a/kvm-block-Add-new-bdrv_co_is_all_zeroes-function.patch b/kvm-block-Add-new-bdrv_co_is_all_zeroes-function.patch new file mode 100644 index 0000000..456b034 --- /dev/null +++ b/kvm-block-Add-new-bdrv_co_is_all_zeroes-function.patch @@ -0,0 +1,143 @@ +From 659dd2d1f7b1facbf9c548468c1b50237f7aa8e4 Mon Sep 17 00:00:00 2001 +From: Eric Blake +Date: Fri, 9 May 2025 15:40:21 -0500 +Subject: [PATCH 04/14] block: Add new bdrv_co_is_all_zeroes() function + +RH-Author: Eric Blake +RH-MergeRequest: 363: blockdev-mirror: More efficient handling of sparse mirrors +RH-Jira: RHEL-88435 RHEL-88437 +RH-Acked-by: Stefan Hajnoczi +RH-Acked-by: Miroslav Rezanina +RH-Commit: [4/14] 404590dbec0b1113872a7eb1bfe5af0450fe6a28 (ebblake/centos-qemu-kvm) + +There are some optimizations that require knowing if an image starts +out as reading all zeroes, such as making blockdev-mirror faster by +skipping the copying of source zeroes to the destination. The +existing bdrv_co_is_zero_fast() is a good building block for answering +this question, but it tends to give an answer of 0 for a file we just +created via QMP 'blockdev-create' or similar (such as 'qemu-img create +-f raw'). Why? Because file-posix.c insists on allocating a tiny +header to any file rather than leaving it 100% sparse, due to some +filesystems that are unable to answer alignment probes on a hole. But +teaching file-posix.c to read the tiny header doesn't scale - the +problem of a small header is also visible when libvirt sets up an NBD +client to a just-created file on a migration destination host. + +So, we need a wrapper function that handles a bit more complexity in a +common manner for all block devices - when the BDS is mostly a hole, +but has a small non-hole header, it is still worth the time to read +that header and check if it reads as all zeroes before giving up and +returning a pessimistic answer. + +Signed-off-by: Eric Blake +Reviewed-by: Stefan Hajnoczi +Message-ID: <20250509204341.3553601-19-eblake@redhat.com> +(cherry picked from commit 52726096707c5c8b90597c445de897fa64d56e73) +Jira: https://issues.redhat.com/browse/RHEL-88435 +Jira: https://issues.redhat.com/browse/RHEL-88437 +Signed-off-by: Eric Blake +--- + block/io.c | 62 ++++++++++++++++++++++++++++++++++++++++ + include/block/block-io.h | 2 ++ + 2 files changed, 64 insertions(+) + +diff --git a/block/io.c b/block/io.c +index 64f4b1d22a..b6fc07e1dc 100644 +--- a/block/io.c ++++ b/block/io.c +@@ -38,10 +38,14 @@ + #include "qemu/error-report.h" + #include "qemu/main-loop.h" + #include "system/replay.h" ++#include "qemu/units.h" + + /* Maximum bounce buffer for copy-on-read and write zeroes, in bytes */ + #define MAX_BOUNCE_BUFFER (32768 << BDRV_SECTOR_BITS) + ++/* Maximum read size for checking if data reads as zero, in bytes */ ++#define MAX_ZERO_CHECK_BUFFER (128 * KiB) ++ + static void coroutine_fn GRAPH_RDLOCK + bdrv_parent_cb_resize(BlockDriverState *bs); + +@@ -2778,6 +2782,64 @@ int coroutine_fn bdrv_co_is_zero_fast(BlockDriverState *bs, int64_t offset, + return 1; + } + ++/* ++ * Check @bs (and its backing chain) to see if the entire image is known ++ * to read as zeroes. ++ * Return 1 if that is the case, 0 otherwise and -errno on error. ++ * This test is meant to be fast rather than accurate so returning 0 ++ * does not guarantee non-zero data; however, a return of 1 is reliable, ++ * and this function can report 1 in more cases than bdrv_co_is_zero_fast. ++ */ ++int coroutine_fn bdrv_co_is_all_zeroes(BlockDriverState *bs) ++{ ++ int ret; ++ int64_t pnum, bytes; ++ char *buf; ++ QEMUIOVector local_qiov; ++ IO_CODE(); ++ ++ bytes = bdrv_co_getlength(bs); ++ if (bytes < 0) { ++ return bytes; ++ } ++ ++ /* First probe - see if the entire image reads as zero */ ++ ret = bdrv_co_common_block_status_above(bs, NULL, false, BDRV_WANT_ZERO, ++ 0, bytes, &pnum, NULL, NULL, ++ NULL); ++ if (ret < 0) { ++ return ret; ++ } ++ if (ret & BDRV_BLOCK_ZERO) { ++ return bdrv_co_is_zero_fast(bs, pnum, bytes - pnum); ++ } ++ ++ /* ++ * Because of the way 'blockdev-create' works, raw files tend to ++ * be created with a non-sparse region at the front to make ++ * alignment probing easier. If the block starts with only a ++ * small allocated region, it is still worth the effort to see if ++ * the rest of the image is still sparse, coupled with manually ++ * reading the first region to see if it reads zero after all. ++ */ ++ if (pnum > MAX_ZERO_CHECK_BUFFER) { ++ return 0; ++ } ++ ret = bdrv_co_is_zero_fast(bs, pnum, bytes - pnum); ++ if (ret <= 0) { ++ return ret; ++ } ++ /* Only the head of the image is unknown, and it's small. Read it. */ ++ buf = qemu_blockalign(bs, pnum); ++ qemu_iovec_init_buf(&local_qiov, buf, pnum); ++ ret = bdrv_driver_preadv(bs, 0, pnum, &local_qiov, 0, 0); ++ if (ret >= 0) { ++ ret = buffer_is_zero(buf, pnum); ++ } ++ qemu_vfree(buf); ++ return ret; ++} ++ + int coroutine_fn bdrv_co_is_allocated(BlockDriverState *bs, int64_t offset, + int64_t bytes, int64_t *pnum) + { +diff --git a/include/block/block-io.h b/include/block/block-io.h +index b49e0537dd..b99cc98d26 100644 +--- a/include/block/block-io.h ++++ b/include/block/block-io.h +@@ -161,6 +161,8 @@ bdrv_is_allocated_above(BlockDriverState *bs, BlockDriverState *base, + + int coroutine_fn GRAPH_RDLOCK + bdrv_co_is_zero_fast(BlockDriverState *bs, int64_t offset, int64_t bytes); ++int coroutine_fn GRAPH_RDLOCK ++bdrv_co_is_all_zeroes(BlockDriverState *bs); + + int GRAPH_RDLOCK + bdrv_apply_auto_read_only(BlockDriverState *bs, const char *errmsg, +-- +2.39.3 + diff --git a/kvm-block-Expand-block-status-mode-from-bool-to-flags.patch b/kvm-block-Expand-block-status-mode-from-bool-to-flags.patch new file mode 100644 index 0000000..b159e0f --- /dev/null +++ b/kvm-block-Expand-block-status-mode-from-bool-to-flags.patch @@ -0,0 +1,689 @@ +From cb945ccd11d37c959f590ae5661ffe5b73f372a7 Mon Sep 17 00:00:00 2001 +From: Eric Blake +Date: Fri, 9 May 2025 15:40:18 -0500 +Subject: [PATCH 01/14] block: Expand block status mode from bool to flags + +RH-Author: Eric Blake +RH-MergeRequest: 363: blockdev-mirror: More efficient handling of sparse mirrors +RH-Jira: RHEL-88435 RHEL-88437 +RH-Acked-by: Stefan Hajnoczi +RH-Acked-by: Miroslav Rezanina +RH-Commit: [1/14] 12507ca1dbe44640e4a8baded4d5afd5fb4ed615 (ebblake/centos-qemu-kvm) + +This patch is purely mechanical, changing bool want_zero into an +unsigned int for bitwise-or of flags. As of this patch, all +implementations are unchanged (the old want_zero==true is now +mode==BDRV_WANT_PRECISE which is a superset of BDRV_WANT_ZERO); but +the callers in io.c that used to pass want_zero==false are now +prepared for future driver changes that can now distinguish bewteen +BDRV_WANT_ZERO vs. BDRV_WANT_ALLOCATED. The next patch will actually +change the file-posix driver along those lines, now that we have +more-specific hints. + +As for the background why this patch is useful: right now, the +file-posix driver recognizes that if allocation is being queried, the +entire image can be reported as allocated (there is no backing file to +refer to) - but this throws away information on whether the entire +image reads as zero (trivially true if lseek(SEEK_HOLE) at offset 0 +returns -ENXIO, a bit more complicated to prove if the raw file was +created with 'qemu-img create' since we intentionally allocate a small +chunk of all-zero data to help with alignment probing). Later patches +will add a generic algorithm for seeing if an entire file reads as +zeroes. + +Signed-off-by: Eric Blake +Reviewed-by: Stefan Hajnoczi +Message-ID: <20250509204341.3553601-16-eblake@redhat.com> +(cherry picked from commit c33159dec79069514f78faecfe268439226b0f5b) +Jira: https://issues.redhat.com/browse/RHEL-88435 +Jira: https://issues.redhat.com/browse/RHEL-88437 +Signed-off-by: Eric Blake +--- + block/blkdebug.c | 6 ++-- + block/copy-before-write.c | 4 +-- + block/coroutines.h | 4 +-- + block/file-posix.c | 4 +-- + block/gluster.c | 4 +-- + block/io.c | 51 ++++++++++++++++---------------- + block/iscsi.c | 6 ++-- + block/nbd.c | 4 +-- + block/null.c | 6 ++-- + block/parallels.c | 6 ++-- + block/qcow.c | 2 +- + block/qcow2.c | 6 ++-- + block/qed.c | 6 ++-- + block/quorum.c | 4 +-- + block/raw-format.c | 4 +-- + block/rbd.c | 6 ++-- + block/snapshot-access.c | 4 +-- + block/vdi.c | 4 +-- + block/vmdk.c | 2 +- + block/vpc.c | 2 +- + block/vvfat.c | 6 ++-- + include/block/block-common.h | 11 +++++++ + include/block/block_int-common.h | 27 +++++++++-------- + include/block/block_int-io.h | 4 +-- + tests/unit/test-block-iothread.c | 2 +- + 25 files changed, 99 insertions(+), 86 deletions(-) + +diff --git a/block/blkdebug.c b/block/blkdebug.c +index 1c1967f8e0..c54aee0c84 100644 +--- a/block/blkdebug.c ++++ b/block/blkdebug.c +@@ -751,9 +751,9 @@ blkdebug_co_pdiscard(BlockDriverState *bs, int64_t offset, int64_t bytes) + } + + static int coroutine_fn GRAPH_RDLOCK +-blkdebug_co_block_status(BlockDriverState *bs, bool want_zero, int64_t offset, +- int64_t bytes, int64_t *pnum, int64_t *map, +- BlockDriverState **file) ++blkdebug_co_block_status(BlockDriverState *bs, unsigned int mode, ++ int64_t offset, int64_t bytes, int64_t *pnum, ++ int64_t *map, BlockDriverState **file) + { + int err; + +diff --git a/block/copy-before-write.c b/block/copy-before-write.c +index fd470f5f92..2badb3a885 100644 +--- a/block/copy-before-write.c ++++ b/block/copy-before-write.c +@@ -291,8 +291,8 @@ cbw_co_preadv_snapshot(BlockDriverState *bs, int64_t offset, int64_t bytes, + } + + static int coroutine_fn GRAPH_RDLOCK +-cbw_co_snapshot_block_status(BlockDriverState *bs, +- bool want_zero, int64_t offset, int64_t bytes, ++cbw_co_snapshot_block_status(BlockDriverState *bs, unsigned int mode, ++ int64_t offset, int64_t bytes, + int64_t *pnum, int64_t *map, + BlockDriverState **file) + { +diff --git a/block/coroutines.h b/block/coroutines.h +index 79e5efbf75..892646bb7a 100644 +--- a/block/coroutines.h ++++ b/block/coroutines.h +@@ -47,7 +47,7 @@ int coroutine_fn GRAPH_RDLOCK + bdrv_co_common_block_status_above(BlockDriverState *bs, + BlockDriverState *base, + bool include_base, +- bool want_zero, ++ unsigned int mode, + int64_t offset, + int64_t bytes, + int64_t *pnum, +@@ -78,7 +78,7 @@ int co_wrapper_mixed_bdrv_rdlock + bdrv_common_block_status_above(BlockDriverState *bs, + BlockDriverState *base, + bool include_base, +- bool want_zero, ++ unsigned int mode, + int64_t offset, + int64_t bytes, + int64_t *pnum, +diff --git a/block/file-posix.c b/block/file-posix.c +index 0d85123d0f..0c6569742f 100644 +--- a/block/file-posix.c ++++ b/block/file-posix.c +@@ -3266,7 +3266,7 @@ static int find_allocation(BlockDriverState *bs, off_t start, + * well exceed it. + */ + static int coroutine_fn raw_co_block_status(BlockDriverState *bs, +- bool want_zero, ++ unsigned int mode, + int64_t offset, + int64_t bytes, int64_t *pnum, + int64_t *map, +@@ -3282,7 +3282,7 @@ static int coroutine_fn raw_co_block_status(BlockDriverState *bs, + return ret; + } + +- if (!want_zero) { ++ if (mode != BDRV_WANT_PRECISE) { + *pnum = bytes; + *map = offset; + *file = bs; +diff --git a/block/gluster.c b/block/gluster.c +index c6d25ae733..8197b0ecef 100644 +--- a/block/gluster.c ++++ b/block/gluster.c +@@ -1465,7 +1465,7 @@ exit: + * (Based on raw_co_block_status() from file-posix.c.) + */ + static int coroutine_fn qemu_gluster_co_block_status(BlockDriverState *bs, +- bool want_zero, ++ unsigned int mode, + int64_t offset, + int64_t bytes, + int64_t *pnum, +@@ -1482,7 +1482,7 @@ static int coroutine_fn qemu_gluster_co_block_status(BlockDriverState *bs, + return ret; + } + +- if (!want_zero) { ++ if (mode != BDRV_WANT_PRECISE) { + *pnum = bytes; + *map = offset; + *file = bs; +diff --git a/block/io.c b/block/io.c +index ccec11386b..e328402adc 100644 +--- a/block/io.c ++++ b/block/io.c +@@ -2364,10 +2364,8 @@ int bdrv_flush_all(void) + * Drivers not implementing the functionality are assumed to not support + * backing files, hence all their sectors are reported as allocated. + * +- * If 'want_zero' is true, the caller is querying for mapping +- * purposes, with a focus on valid BDRV_BLOCK_OFFSET_VALID, _DATA, and +- * _ZERO where possible; otherwise, the result favors larger 'pnum', +- * with a focus on accurate BDRV_BLOCK_ALLOCATED. ++ * 'mode' serves as a hint as to which results are favored; see the ++ * BDRV_WANT_* macros for details. + * + * If 'offset' is beyond the end of the disk image the return value is + * BDRV_BLOCK_EOF and 'pnum' is set to 0. +@@ -2387,7 +2385,7 @@ int bdrv_flush_all(void) + * set to the host mapping and BDS corresponding to the guest offset. + */ + static int coroutine_fn GRAPH_RDLOCK +-bdrv_co_do_block_status(BlockDriverState *bs, bool want_zero, ++bdrv_co_do_block_status(BlockDriverState *bs, unsigned int mode, + int64_t offset, int64_t bytes, + int64_t *pnum, int64_t *map, BlockDriverState **file) + { +@@ -2476,7 +2474,7 @@ bdrv_co_do_block_status(BlockDriverState *bs, bool want_zero, + local_file = bs; + local_map = aligned_offset; + } else { +- ret = bs->drv->bdrv_co_block_status(bs, want_zero, aligned_offset, ++ ret = bs->drv->bdrv_co_block_status(bs, mode, aligned_offset, + aligned_bytes, pnum, &local_map, + &local_file); + +@@ -2488,10 +2486,10 @@ bdrv_co_do_block_status(BlockDriverState *bs, bool want_zero, + * the cache requires an RCU update, so double check here to avoid + * such an update if possible. + * +- * Check want_zero, because we only want to update the cache when we ++ * Check mode, because we only want to update the cache when we + * have accurate information about what is zero and what is data. + */ +- if (want_zero && ++ if (mode == BDRV_WANT_PRECISE && + ret == (BDRV_BLOCK_DATA | BDRV_BLOCK_OFFSET_VALID) && + QLIST_EMPTY(&bs->children)) + { +@@ -2548,7 +2546,7 @@ bdrv_co_do_block_status(BlockDriverState *bs, bool want_zero, + + if (ret & BDRV_BLOCK_RAW) { + assert(ret & BDRV_BLOCK_OFFSET_VALID && local_file); +- ret = bdrv_co_do_block_status(local_file, want_zero, local_map, ++ ret = bdrv_co_do_block_status(local_file, mode, local_map, + *pnum, pnum, &local_map, &local_file); + goto out; + } +@@ -2560,7 +2558,7 @@ bdrv_co_do_block_status(BlockDriverState *bs, bool want_zero, + + if (!cow_bs) { + ret |= BDRV_BLOCK_ZERO; +- } else if (want_zero) { ++ } else if (mode == BDRV_WANT_PRECISE) { + int64_t size2 = bdrv_co_getlength(cow_bs); + + if (size2 >= 0 && offset >= size2) { +@@ -2569,14 +2567,14 @@ bdrv_co_do_block_status(BlockDriverState *bs, bool want_zero, + } + } + +- if (want_zero && ret & BDRV_BLOCK_RECURSE && ++ if (mode == BDRV_WANT_PRECISE && ret & BDRV_BLOCK_RECURSE && + local_file && local_file != bs && + (ret & BDRV_BLOCK_DATA) && !(ret & BDRV_BLOCK_ZERO) && + (ret & BDRV_BLOCK_OFFSET_VALID)) { + int64_t file_pnum; + int ret2; + +- ret2 = bdrv_co_do_block_status(local_file, want_zero, local_map, ++ ret2 = bdrv_co_do_block_status(local_file, mode, local_map, + *pnum, &file_pnum, NULL, NULL); + if (ret2 >= 0) { + /* Ignore errors. This is just providing extra information, it +@@ -2627,7 +2625,7 @@ int coroutine_fn + bdrv_co_common_block_status_above(BlockDriverState *bs, + BlockDriverState *base, + bool include_base, +- bool want_zero, ++ unsigned int mode, + int64_t offset, + int64_t bytes, + int64_t *pnum, +@@ -2654,7 +2652,7 @@ bdrv_co_common_block_status_above(BlockDriverState *bs, + return 0; + } + +- ret = bdrv_co_do_block_status(bs, want_zero, offset, bytes, pnum, ++ ret = bdrv_co_do_block_status(bs, mode, offset, bytes, pnum, + map, file); + ++*depth; + if (ret < 0 || *pnum == 0 || ret & BDRV_BLOCK_ALLOCATED || bs == base) { +@@ -2671,7 +2669,7 @@ bdrv_co_common_block_status_above(BlockDriverState *bs, + for (p = bdrv_filter_or_cow_bs(bs); include_base || p != base; + p = bdrv_filter_or_cow_bs(p)) + { +- ret = bdrv_co_do_block_status(p, want_zero, offset, bytes, pnum, ++ ret = bdrv_co_do_block_status(p, mode, offset, bytes, pnum, + map, file); + ++*depth; + if (ret < 0) { +@@ -2734,7 +2732,8 @@ int coroutine_fn bdrv_co_block_status_above(BlockDriverState *bs, + BlockDriverState **file) + { + IO_CODE(); +- return bdrv_co_common_block_status_above(bs, base, false, true, offset, ++ return bdrv_co_common_block_status_above(bs, base, false, ++ BDRV_WANT_PRECISE, offset, + bytes, pnum, map, file, NULL); + } + +@@ -2765,8 +2764,9 @@ int coroutine_fn bdrv_co_is_zero_fast(BlockDriverState *bs, int64_t offset, + return 1; + } + +- ret = bdrv_co_common_block_status_above(bs, NULL, false, false, offset, +- bytes, &pnum, NULL, NULL, NULL); ++ ret = bdrv_co_common_block_status_above(bs, NULL, false, BDRV_WANT_ZERO, ++ offset, bytes, &pnum, NULL, NULL, ++ NULL); + + if (ret < 0) { + return ret; +@@ -2782,9 +2782,9 @@ int coroutine_fn bdrv_co_is_allocated(BlockDriverState *bs, int64_t offset, + int64_t dummy; + IO_CODE(); + +- ret = bdrv_co_common_block_status_above(bs, bs, true, false, offset, +- bytes, pnum ? pnum : &dummy, NULL, +- NULL, NULL); ++ ret = bdrv_co_common_block_status_above(bs, bs, true, BDRV_WANT_ALLOCATED, ++ offset, bytes, pnum ? pnum : &dummy, ++ NULL, NULL, NULL); + if (ret < 0) { + return ret; + } +@@ -2817,7 +2817,8 @@ int coroutine_fn bdrv_co_is_allocated_above(BlockDriverState *bs, + int ret; + IO_CODE(); + +- ret = bdrv_co_common_block_status_above(bs, base, include_base, false, ++ ret = bdrv_co_common_block_status_above(bs, base, include_base, ++ BDRV_WANT_ALLOCATED, + offset, bytes, pnum, NULL, NULL, + &depth); + if (ret < 0) { +@@ -3714,8 +3715,8 @@ bdrv_co_preadv_snapshot(BdrvChild *child, int64_t offset, int64_t bytes, + } + + int coroutine_fn +-bdrv_co_snapshot_block_status(BlockDriverState *bs, +- bool want_zero, int64_t offset, int64_t bytes, ++bdrv_co_snapshot_block_status(BlockDriverState *bs, unsigned int mode, ++ int64_t offset, int64_t bytes, + int64_t *pnum, int64_t *map, + BlockDriverState **file) + { +@@ -3733,7 +3734,7 @@ bdrv_co_snapshot_block_status(BlockDriverState *bs, + } + + bdrv_inc_in_flight(bs); +- ret = drv->bdrv_co_snapshot_block_status(bs, want_zero, offset, bytes, ++ ret = drv->bdrv_co_snapshot_block_status(bs, mode, offset, bytes, + pnum, map, file); + bdrv_dec_in_flight(bs); + +diff --git a/block/iscsi.c b/block/iscsi.c +index 2f0f4dac09..15b96ee880 100644 +--- a/block/iscsi.c ++++ b/block/iscsi.c +@@ -694,9 +694,9 @@ out_unlock: + + + static int coroutine_fn iscsi_co_block_status(BlockDriverState *bs, +- bool want_zero, int64_t offset, +- int64_t bytes, int64_t *pnum, +- int64_t *map, ++ unsigned int mode, ++ int64_t offset, int64_t bytes, ++ int64_t *pnum, int64_t *map, + BlockDriverState **file) + { + IscsiLun *iscsilun = bs->opaque; +diff --git a/block/nbd.c b/block/nbd.c +index 887841bc81..d5a2b21c6d 100644 +--- a/block/nbd.c ++++ b/block/nbd.c +@@ -1397,8 +1397,8 @@ nbd_client_co_pdiscard(BlockDriverState *bs, int64_t offset, int64_t bytes) + } + + static int coroutine_fn GRAPH_RDLOCK nbd_client_co_block_status( +- BlockDriverState *bs, bool want_zero, int64_t offset, int64_t bytes, +- int64_t *pnum, int64_t *map, BlockDriverState **file) ++ BlockDriverState *bs, unsigned int mode, int64_t offset, ++ int64_t bytes, int64_t *pnum, int64_t *map, BlockDriverState **file) + { + int ret, request_ret; + NBDExtent64 extent = { 0 }; +diff --git a/block/null.c b/block/null.c +index dc0b1fdbd9..4e448d593d 100644 +--- a/block/null.c ++++ b/block/null.c +@@ -227,9 +227,9 @@ static int null_reopen_prepare(BDRVReopenState *reopen_state, + } + + static int coroutine_fn null_co_block_status(BlockDriverState *bs, +- bool want_zero, int64_t offset, +- int64_t bytes, int64_t *pnum, +- int64_t *map, ++ unsigned int mode, ++ int64_t offset, int64_t bytes, ++ int64_t *pnum, int64_t *map, + BlockDriverState **file) + { + BDRVNullState *s = bs->opaque; +diff --git a/block/parallels.c b/block/parallels.c +index 347ca127f3..3a375e2a8a 100644 +--- a/block/parallels.c ++++ b/block/parallels.c +@@ -416,9 +416,9 @@ parallels_co_flush_to_os(BlockDriverState *bs) + } + + static int coroutine_fn GRAPH_RDLOCK +-parallels_co_block_status(BlockDriverState *bs, bool want_zero, int64_t offset, +- int64_t bytes, int64_t *pnum, int64_t *map, +- BlockDriverState **file) ++parallels_co_block_status(BlockDriverState *bs, unsigned int mode, ++ int64_t offset, int64_t bytes, int64_t *pnum, ++ int64_t *map, BlockDriverState **file) + { + BDRVParallelsState *s = bs->opaque; + int count; +diff --git a/block/qcow.c b/block/qcow.c +index da8ad4d243..8a3e7591a9 100644 +--- a/block/qcow.c ++++ b/block/qcow.c +@@ -530,7 +530,7 @@ get_cluster_offset(BlockDriverState *bs, uint64_t offset, int allocate, + } + + static int coroutine_fn GRAPH_RDLOCK +-qcow_co_block_status(BlockDriverState *bs, bool want_zero, ++qcow_co_block_status(BlockDriverState *bs, unsigned int mode, + int64_t offset, int64_t bytes, int64_t *pnum, + int64_t *map, BlockDriverState **file) + { +diff --git a/block/qcow2.c b/block/qcow2.c +index b6ade4755d..9fc96ba99a 100644 +--- a/block/qcow2.c ++++ b/block/qcow2.c +@@ -2147,9 +2147,9 @@ static void qcow2_join_options(QDict *options, QDict *old_options) + } + + static int coroutine_fn GRAPH_RDLOCK +-qcow2_co_block_status(BlockDriverState *bs, bool want_zero, int64_t offset, +- int64_t count, int64_t *pnum, int64_t *map, +- BlockDriverState **file) ++qcow2_co_block_status(BlockDriverState *bs, unsigned int mode, ++ int64_t offset, int64_t count, int64_t *pnum, ++ int64_t *map, BlockDriverState **file) + { + BDRVQcow2State *s = bs->opaque; + uint64_t host_offset; +diff --git a/block/qed.c b/block/qed.c +index ac24449ffb..4a36fb3929 100644 +--- a/block/qed.c ++++ b/block/qed.c +@@ -833,9 +833,9 @@ fail: + } + + static int coroutine_fn GRAPH_RDLOCK +-bdrv_qed_co_block_status(BlockDriverState *bs, bool want_zero, int64_t pos, +- int64_t bytes, int64_t *pnum, int64_t *map, +- BlockDriverState **file) ++bdrv_qed_co_block_status(BlockDriverState *bs, unsigned int mode, ++ int64_t pos, int64_t bytes, int64_t *pnum, ++ int64_t *map, BlockDriverState **file) + { + BDRVQEDState *s = bs->opaque; + size_t len = MIN(bytes, SIZE_MAX); +diff --git a/block/quorum.c b/block/quorum.c +index 30747a6df9..ed8ce801ee 100644 +--- a/block/quorum.c ++++ b/block/quorum.c +@@ -1226,7 +1226,7 @@ static void quorum_child_perm(BlockDriverState *bs, BdrvChild *c, + * region contains zeroes, and BDRV_BLOCK_DATA otherwise. + */ + static int coroutine_fn GRAPH_RDLOCK +-quorum_co_block_status(BlockDriverState *bs, bool want_zero, ++quorum_co_block_status(BlockDriverState *bs, unsigned int mode, + int64_t offset, int64_t count, + int64_t *pnum, int64_t *map, BlockDriverState **file) + { +@@ -1238,7 +1238,7 @@ quorum_co_block_status(BlockDriverState *bs, bool want_zero, + for (i = 0; i < s->num_children; i++) { + int64_t bytes; + ret = bdrv_co_common_block_status_above(s->children[i]->bs, NULL, false, +- want_zero, offset, count, ++ mode, offset, count, + &bytes, NULL, NULL, NULL); + if (ret < 0) { + quorum_report_bad(QUORUM_OP_TYPE_READ, offset, count, +diff --git a/block/raw-format.c b/block/raw-format.c +index e08526e2ec..df16ac1ea2 100644 +--- a/block/raw-format.c ++++ b/block/raw-format.c +@@ -283,8 +283,8 @@ fail: + } + + static int coroutine_fn GRAPH_RDLOCK +-raw_co_block_status(BlockDriverState *bs, bool want_zero, int64_t offset, +- int64_t bytes, int64_t *pnum, int64_t *map, ++raw_co_block_status(BlockDriverState *bs, unsigned int mode, ++ int64_t offset, int64_t bytes, int64_t *pnum, int64_t *map, + BlockDriverState **file) + { + BDRVRawState *s = bs->opaque; +diff --git a/block/rbd.c b/block/rbd.c +index af984fb7db..4f3d42a8e7 100644 +--- a/block/rbd.c ++++ b/block/rbd.c +@@ -1504,9 +1504,9 @@ static int qemu_rbd_diff_iterate_cb(uint64_t offs, size_t len, + } + + static int coroutine_fn qemu_rbd_co_block_status(BlockDriverState *bs, +- bool want_zero, int64_t offset, +- int64_t bytes, int64_t *pnum, +- int64_t *map, ++ unsigned int mode, ++ int64_t offset, int64_t bytes, ++ int64_t *pnum, int64_t *map, + BlockDriverState **file) + { + BDRVRBDState *s = bs->opaque; +diff --git a/block/snapshot-access.c b/block/snapshot-access.c +index 71ac83c01f..17ed2402db 100644 +--- a/block/snapshot-access.c ++++ b/block/snapshot-access.c +@@ -41,11 +41,11 @@ snapshot_access_co_preadv_part(BlockDriverState *bs, + + static int coroutine_fn GRAPH_RDLOCK + snapshot_access_co_block_status(BlockDriverState *bs, +- bool want_zero, int64_t offset, ++ unsigned int mode, int64_t offset, + int64_t bytes, int64_t *pnum, + int64_t *map, BlockDriverState **file) + { +- return bdrv_co_snapshot_block_status(bs->file->bs, want_zero, offset, ++ return bdrv_co_snapshot_block_status(bs->file->bs, mode, offset, + bytes, pnum, map, file); + } + +diff --git a/block/vdi.c b/block/vdi.c +index a2da6ecab0..3ddc62a569 100644 +--- a/block/vdi.c ++++ b/block/vdi.c +@@ -523,8 +523,8 @@ static int vdi_reopen_prepare(BDRVReopenState *state, + } + + static int coroutine_fn GRAPH_RDLOCK +-vdi_co_block_status(BlockDriverState *bs, bool want_zero, int64_t offset, +- int64_t bytes, int64_t *pnum, int64_t *map, ++vdi_co_block_status(BlockDriverState *bs, unsigned int mode, ++ int64_t offset, int64_t bytes, int64_t *pnum, int64_t *map, + BlockDriverState **file) + { + BDRVVdiState *s = (BDRVVdiState *)bs->opaque; +diff --git a/block/vmdk.c b/block/vmdk.c +index 2adec49912..9c7ab037e1 100644 +--- a/block/vmdk.c ++++ b/block/vmdk.c +@@ -1777,7 +1777,7 @@ static inline uint64_t vmdk_find_offset_in_cluster(VmdkExtent *extent, + } + + static int coroutine_fn GRAPH_RDLOCK +-vmdk_co_block_status(BlockDriverState *bs, bool want_zero, ++vmdk_co_block_status(BlockDriverState *bs, unsigned int mode, + int64_t offset, int64_t bytes, int64_t *pnum, + int64_t *map, BlockDriverState **file) + { +diff --git a/block/vpc.c b/block/vpc.c +index 0309e319f6..801ff5793f 100644 +--- a/block/vpc.c ++++ b/block/vpc.c +@@ -726,7 +726,7 @@ fail: + } + + static int coroutine_fn GRAPH_RDLOCK +-vpc_co_block_status(BlockDriverState *bs, bool want_zero, ++vpc_co_block_status(BlockDriverState *bs, unsigned int mode, + int64_t offset, int64_t bytes, + int64_t *pnum, int64_t *map, + BlockDriverState **file) +diff --git a/block/vvfat.c b/block/vvfat.c +index 91d69b3cc8..814796d918 100644 +--- a/block/vvfat.c ++++ b/block/vvfat.c +@@ -3134,9 +3134,9 @@ vvfat_co_pwritev(BlockDriverState *bs, int64_t offset, int64_t bytes, + } + + static int coroutine_fn vvfat_co_block_status(BlockDriverState *bs, +- bool want_zero, int64_t offset, +- int64_t bytes, int64_t *n, +- int64_t *map, ++ unsigned int mode, ++ int64_t offset, int64_t bytes, ++ int64_t *n, int64_t *map, + BlockDriverState **file) + { + *n = bytes; +diff --git a/include/block/block-common.h b/include/block/block-common.h +index 0b831ef87b..c8c626daea 100644 +--- a/include/block/block-common.h ++++ b/include/block/block-common.h +@@ -333,6 +333,17 @@ typedef enum { + #define BDRV_BLOCK_RECURSE 0x40 + #define BDRV_BLOCK_COMPRESSED 0x80 + ++/* ++ * Block status hints: the bitwise-or of these flags emphasize what ++ * the caller hopes to learn, and some drivers may be able to give ++ * faster answers by doing less work when the hint permits. ++ */ ++#define BDRV_WANT_ZERO BDRV_BLOCK_ZERO ++#define BDRV_WANT_OFFSET_VALID BDRV_BLOCK_OFFSET_VALID ++#define BDRV_WANT_ALLOCATED BDRV_BLOCK_ALLOCATED ++#define BDRV_WANT_PRECISE (BDRV_WANT_ZERO | BDRV_WANT_OFFSET_VALID | \ ++ BDRV_WANT_OFFSET_VALID) ++ + typedef QTAILQ_HEAD(BlockReopenQueue, BlockReopenQueueEntry) BlockReopenQueue; + + typedef struct BDRVReopenState { +diff --git a/include/block/block_int-common.h b/include/block/block_int-common.h +index ebb4e56a50..a9c0daa2a4 100644 +--- a/include/block/block_int-common.h ++++ b/include/block/block_int-common.h +@@ -608,15 +608,16 @@ struct BlockDriver { + * according to the current layer, and should only need to set + * BDRV_BLOCK_DATA, BDRV_BLOCK_ZERO, BDRV_BLOCK_OFFSET_VALID, + * and/or BDRV_BLOCK_RAW; if the current layer defers to a backing +- * layer, the result should be 0 (and not BDRV_BLOCK_ZERO). See +- * block.h for the overall meaning of the bits. As a hint, the +- * flag want_zero is true if the caller cares more about precise +- * mappings (favor accurate _OFFSET_VALID/_ZERO) or false for +- * overall allocation (favor larger *pnum, perhaps by reporting +- * _DATA instead of _ZERO). The block layer guarantees input +- * clamped to bdrv_getlength() and aligned to request_alignment, +- * as well as non-NULL pnum, map, and file; in turn, the driver +- * must return an error or set pnum to an aligned non-zero value. ++ * layer, the result should be 0 (and not BDRV_BLOCK_ZERO). The ++ * caller will synthesize BDRV_BLOCK_ALLOCATED based on the ++ * non-zero results. See block.h for the overall meaning of the ++ * bits. As a hint, the flags in @mode may include a bitwise-or ++ * of BDRV_WANT_ALLOCATED, BDRV_WANT_OFFSET_VALID, or ++ * BDRV_WANT_ZERO based on what the caller is looking for in the ++ * results. The block layer guarantees input clamped to ++ * bdrv_getlength() and aligned to request_alignment, as well as ++ * non-NULL pnum, map, and file; in turn, the driver must return ++ * an error or set pnum to an aligned non-zero value. + * + * Note that @bytes is just a hint on how big of a region the + * caller wants to inspect. It is not a limit on *pnum. +@@ -628,8 +629,8 @@ struct BlockDriver { + * to clamping *pnum for return to its caller. + */ + int coroutine_fn GRAPH_RDLOCK_PTR (*bdrv_co_block_status)( +- BlockDriverState *bs, +- bool want_zero, int64_t offset, int64_t bytes, int64_t *pnum, ++ BlockDriverState *bs, unsigned int mode, ++ int64_t offset, int64_t bytes, int64_t *pnum, + int64_t *map, BlockDriverState **file); + + /* +@@ -653,8 +654,8 @@ struct BlockDriver { + QEMUIOVector *qiov, size_t qiov_offset); + + int coroutine_fn GRAPH_RDLOCK_PTR (*bdrv_co_snapshot_block_status)( +- BlockDriverState *bs, bool want_zero, int64_t offset, int64_t bytes, +- int64_t *pnum, int64_t *map, BlockDriverState **file); ++ BlockDriverState *bs, unsigned int mode, int64_t offset, ++ int64_t bytes, int64_t *pnum, int64_t *map, BlockDriverState **file); + + int coroutine_fn GRAPH_RDLOCK_PTR (*bdrv_co_pdiscard_snapshot)( + BlockDriverState *bs, int64_t offset, int64_t bytes); +diff --git a/include/block/block_int-io.h b/include/block/block_int-io.h +index 4a7cf2b4fd..4f94eb3c5a 100644 +--- a/include/block/block_int-io.h ++++ b/include/block/block_int-io.h +@@ -38,8 +38,8 @@ + int coroutine_fn GRAPH_RDLOCK bdrv_co_preadv_snapshot(BdrvChild *child, + int64_t offset, int64_t bytes, QEMUIOVector *qiov, size_t qiov_offset); + int coroutine_fn GRAPH_RDLOCK bdrv_co_snapshot_block_status( +- BlockDriverState *bs, bool want_zero, int64_t offset, int64_t bytes, +- int64_t *pnum, int64_t *map, BlockDriverState **file); ++ BlockDriverState *bs, unsigned int mode, int64_t offset, ++ int64_t bytes, int64_t *pnum, int64_t *map, BlockDriverState **file); + int coroutine_fn GRAPH_RDLOCK bdrv_co_pdiscard_snapshot(BlockDriverState *bs, + int64_t offset, int64_t bytes); + +diff --git a/tests/unit/test-block-iothread.c b/tests/unit/test-block-iothread.c +index 2b358eaaa8..e26b3be593 100644 +--- a/tests/unit/test-block-iothread.c ++++ b/tests/unit/test-block-iothread.c +@@ -63,7 +63,7 @@ bdrv_test_co_truncate(BlockDriverState *bs, int64_t offset, bool exact, + } + + static int coroutine_fn bdrv_test_co_block_status(BlockDriverState *bs, +- bool want_zero, ++ unsigned int mode, + int64_t offset, int64_t count, + int64_t *pnum, int64_t *map, + BlockDriverState **file) +-- +2.39.3 + diff --git a/kvm-block-Let-bdrv_co_is_zero_fast-consolidate-adjacent-.patch b/kvm-block-Let-bdrv_co_is_zero_fast-consolidate-adjacent-.patch new file mode 100644 index 0000000..f0a2749 --- /dev/null +++ b/kvm-block-Let-bdrv_co_is_zero_fast-consolidate-adjacent-.patch @@ -0,0 +1,90 @@ +From e101b9872f9b3f6c5e128f29d7c3bb91faca362b Mon Sep 17 00:00:00 2001 +From: Eric Blake +Date: Fri, 9 May 2025 15:40:20 -0500 +Subject: [PATCH 03/14] block: Let bdrv_co_is_zero_fast consolidate adjacent + extents + +RH-Author: Eric Blake +RH-MergeRequest: 363: blockdev-mirror: More efficient handling of sparse mirrors +RH-Jira: RHEL-88435 RHEL-88437 +RH-Acked-by: Stefan Hajnoczi +RH-Acked-by: Miroslav Rezanina +RH-Commit: [3/14] 4520f7ef5bcc5803413541e6f48bf750af3d31e0 (ebblake/centos-qemu-kvm) + +Some BDS drivers have a cap on how much block status they can supply +in one query (for example, NBD talking to an older server cannot +inspect more than 4G per query; and qcow2 tends to cap its answers +rather than cross a cluster boundary of an L1 table). Although the +existing callers of bdrv_co_is_zero_fast are not passing in that large +of a 'bytes' parameter, an upcoming caller wants to query the entire +image at once, and will thus benefit from being able to treat adjacent +zero regions in a coalesced manner, rather than claiming the region is +non-zero merely because pnum was truncated and didn't match the +incoming bytes. + +While refactoring this into a loop, note that there is no need to +assign pnum prior to calling bdrv_co_common_block_status_above() (it +is guaranteed to be assigned deeper in the callstack). + +Signed-off-by: Eric Blake +Reviewed-by: Stefan Hajnoczi +Message-ID: <20250509204341.3553601-18-eblake@redhat.com> +(cherry picked from commit 31bf15d97dd1d205a3b264675f9a1b3bd1939068) +Jira: https://issues.redhat.com/browse/RHEL-88435 +Jira: https://issues.redhat.com/browse/RHEL-88437 +Signed-off-by: Eric Blake +--- + block/io.c | 27 +++++++++++++++------------ + 1 file changed, 15 insertions(+), 12 deletions(-) + +diff --git a/block/io.c b/block/io.c +index e328402adc..64f4b1d22a 100644 +--- a/block/io.c ++++ b/block/io.c +@@ -2751,28 +2751,31 @@ int coroutine_fn bdrv_co_block_status(BlockDriverState *bs, int64_t offset, + * by @offset and @bytes is known to read as zeroes. + * Return 1 if that is the case, 0 otherwise and -errno on error. + * This test is meant to be fast rather than accurate so returning 0 +- * does not guarantee non-zero data. ++ * does not guarantee non-zero data; but a return of 1 is reliable. + */ + int coroutine_fn bdrv_co_is_zero_fast(BlockDriverState *bs, int64_t offset, + int64_t bytes) + { + int ret; +- int64_t pnum = bytes; ++ int64_t pnum; + IO_CODE(); + +- if (!bytes) { +- return 1; +- } +- +- ret = bdrv_co_common_block_status_above(bs, NULL, false, BDRV_WANT_ZERO, +- offset, bytes, &pnum, NULL, NULL, +- NULL); ++ while (bytes) { ++ ret = bdrv_co_common_block_status_above(bs, NULL, false, ++ BDRV_WANT_ZERO, offset, bytes, ++ &pnum, NULL, NULL, NULL); + +- if (ret < 0) { +- return ret; ++ if (ret < 0) { ++ return ret; ++ } ++ if (!(ret & BDRV_BLOCK_ZERO)) { ++ return 0; ++ } ++ offset += pnum; ++ bytes -= pnum; + } + +- return (pnum == bytes) && (ret & BDRV_BLOCK_ZERO); ++ return 1; + } + + int coroutine_fn bdrv_co_is_allocated(BlockDriverState *bs, int64_t offset, +-- +2.39.3 + diff --git a/kvm-file-posix-gluster-Handle-zero-block-status-hint-bet.patch b/kvm-file-posix-gluster-Handle-zero-block-status-hint-bet.patch new file mode 100644 index 0000000..1405719 --- /dev/null +++ b/kvm-file-posix-gluster-Handle-zero-block-status-hint-bet.patch @@ -0,0 +1,64 @@ +From f8d89f67817fa362a3b8ed0721775e353dac8f18 Mon Sep 17 00:00:00 2001 +From: Eric Blake +Date: Fri, 9 May 2025 15:40:19 -0500 +Subject: [PATCH 02/14] file-posix, gluster: Handle zero block status hint + better + +RH-Author: Eric Blake +RH-MergeRequest: 363: blockdev-mirror: More efficient handling of sparse mirrors +RH-Jira: RHEL-88435 RHEL-88437 +RH-Acked-by: Stefan Hajnoczi +RH-Acked-by: Miroslav Rezanina +RH-Commit: [2/14] c40cd3f8cda2ea1646d90fd174b5f0dbd3e1a50b (ebblake/centos-qemu-kvm) + +Although the previous patch to change 'bool want_zero' into a bitmask +made no semantic change, it is now time to differentiate. When the +caller specifically wants to know what parts of the file read as zero, +we need to use lseek and actually reporting holes, rather than +short-circuiting and advertising full allocation. + +This change will be utilized in later patches to let mirroring +optimize for the case when the destination already reads as zeroes. + +Signed-off-by: Eric Blake +Reviewed-by: Stefan Hajnoczi +Message-ID: <20250509204341.3553601-17-eblake@redhat.com> +(cherry picked from commit a6a0a7fb0e327d17594c971b4a39de14e025b415) +Jira: https://issues.redhat.com/browse/RHEL-88435 +Jira: https://issues.redhat.com/browse/RHEL-88437 +Signed-off-by: Eric Blake +--- + block/file-posix.c | 3 ++- + block/gluster.c | 2 +- + 2 files changed, 3 insertions(+), 2 deletions(-) + +diff --git a/block/file-posix.c b/block/file-posix.c +index 0c6569742f..dea7b09b6c 100644 +--- a/block/file-posix.c ++++ b/block/file-posix.c +@@ -3282,7 +3282,8 @@ static int coroutine_fn raw_co_block_status(BlockDriverState *bs, + return ret; + } + +- if (mode != BDRV_WANT_PRECISE) { ++ if (!(mode & BDRV_WANT_ZERO)) { ++ /* There is no backing file - all bytes are allocated in this file. */ + *pnum = bytes; + *map = offset; + *file = bs; +diff --git a/block/gluster.c b/block/gluster.c +index 8197b0ecef..e702666cbc 100644 +--- a/block/gluster.c ++++ b/block/gluster.c +@@ -1482,7 +1482,7 @@ static int coroutine_fn qemu_gluster_co_block_status(BlockDriverState *bs, + return ret; + } + +- if (mode != BDRV_WANT_PRECISE) { ++ if (!(mode & BDRV_WANT_ZERO)) { + *pnum = bytes; + *map = offset; + *file = bs; +-- +2.39.3 + diff --git a/kvm-iotests-Improve-iotest-194-to-mirror-data.patch b/kvm-iotests-Improve-iotest-194-to-mirror-data.patch new file mode 100644 index 0000000..caeec40 --- /dev/null +++ b/kvm-iotests-Improve-iotest-194-to-mirror-data.patch @@ -0,0 +1,42 @@ +From 11b46a271d73631177f59ff581a408f967c30fb9 Mon Sep 17 00:00:00 2001 +From: Eric Blake +Date: Fri, 9 May 2025 15:40:22 -0500 +Subject: [PATCH 05/14] iotests: Improve iotest 194 to mirror data + +RH-Author: Eric Blake +RH-MergeRequest: 363: blockdev-mirror: More efficient handling of sparse mirrors +RH-Jira: RHEL-88435 RHEL-88437 +RH-Acked-by: Stefan Hajnoczi +RH-Acked-by: Miroslav Rezanina +RH-Commit: [5/14] 9f1fd3c7d4332ac310af4eb37e8f2122f6324294 (ebblake/centos-qemu-kvm) + +Mirroring a completely sparse image to a sparse destination should be +practically instantaneous. It isn't yet, but the test will be more +realistic if it has some non-zero to mirror as well as the holes. + +Signed-off-by: Eric Blake +Reviewed-by: Stefan Hajnoczi +Message-ID: <20250509204341.3553601-20-eblake@redhat.com> +(cherry picked from commit eb89627899bb84148d272394e885725eff456ae9) +Jira: https://issues.redhat.com/browse/RHEL-88435 +Jira: https://issues.redhat.com/browse/RHEL-88437 +Signed-off-by: Eric Blake +--- + tests/qemu-iotests/194 | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/tests/qemu-iotests/194 b/tests/qemu-iotests/194 +index c0ce82dd25..d0b9c084f5 100755 +--- a/tests/qemu-iotests/194 ++++ b/tests/qemu-iotests/194 +@@ -34,6 +34,7 @@ with iotests.FilePath('source.img') as source_img_path, \ + + img_size = '1G' + iotests.qemu_img_create('-f', iotests.imgfmt, source_img_path, img_size) ++ iotests.qemu_io('-f', iotests.imgfmt, '-c', 'write 512M 1M', source_img_path) + iotests.qemu_img_create('-f', iotests.imgfmt, dest_img_path, img_size) + + iotests.log('Launching VMs...') +-- +2.39.3 + diff --git a/kvm-iotests-common.rc-add-disk_usage-function.patch b/kvm-iotests-common.rc-add-disk_usage-function.patch new file mode 100644 index 0000000..02fc97a --- /dev/null +++ b/kvm-iotests-common.rc-add-disk_usage-function.patch @@ -0,0 +1,68 @@ +From d8ed5039981b1eb81d229d8ee672d5ee28862e92 Mon Sep 17 00:00:00 2001 +From: Andrey Drobyshev +Date: Fri, 9 May 2025 15:40:29 -0500 +Subject: [PATCH 12/14] iotests/common.rc: add disk_usage function + +RH-Author: Eric Blake +RH-MergeRequest: 363: blockdev-mirror: More efficient handling of sparse mirrors +RH-Jira: RHEL-88435 RHEL-88437 +RH-Acked-by: Stefan Hajnoczi +RH-Acked-by: Miroslav Rezanina +RH-Commit: [12/14] 0a007f9d09f01b50cf4edeb8ac8217356b2cb5d2 (ebblake/centos-qemu-kvm) + +Move the definition from iotests/250 to common.rc. This is used to +detect real disk usage of sparse files. In particular, we want to use +it for checking subclusters-based discards. + +Signed-off-by: Andrey Drobyshev +Reviewed-by: Alexander Ivanov +Reviewed-by: Alberto Garcia +Message-ID: <20240913163942.423050-6-andrey.drobyshev@virtuozzo.com> +Signed-off-by: Eric Blake +Reviewed-by: Stefan Hajnoczi +Message-ID: <20250509204341.3553601-27-eblake@redhat.com> +(cherry picked from commit be9bac072ede6e6aa27079f59efcf17b56bd7b26) +Jira: https://issues.redhat.com/browse/RHEL-88435 +Jira: https://issues.redhat.com/browse/RHEL-88437 +Signed-off-by: Eric Blake +--- + tests/qemu-iotests/250 | 5 ----- + tests/qemu-iotests/common.rc | 6 ++++++ + 2 files changed, 6 insertions(+), 5 deletions(-) + +diff --git a/tests/qemu-iotests/250 b/tests/qemu-iotests/250 +index af48f83aba..c0a0dbc0ff 100755 +--- a/tests/qemu-iotests/250 ++++ b/tests/qemu-iotests/250 +@@ -52,11 +52,6 @@ _unsupported_imgopts data_file + # bdrv_co_truncate(bs->file) call in qcow2_co_truncate(), which might succeed + # anyway. + +-disk_usage() +-{ +- du --block-size=1 $1 | awk '{print $1}' +-} +- + size=2100M + + _make_test_img -o "cluster_size=1M,preallocation=metadata" $size +diff --git a/tests/qemu-iotests/common.rc b/tests/qemu-iotests/common.rc +index 95c12577dd..237f746af8 100644 +--- a/tests/qemu-iotests/common.rc ++++ b/tests/qemu-iotests/common.rc +@@ -140,6 +140,12 @@ _optstr_add() + fi + } + ++# report real disk usage for sparse files ++disk_usage() ++{ ++ du --block-size=1 "$1" | awk '{print $1}' ++} ++ + # Set the variables to the empty string to turn Valgrind off + # for specific processes, e.g. + # $ VALGRIND_QEMU_IO= ./check -qcow2 -valgrind 015 +-- +2.39.3 + diff --git a/kvm-mirror-Allow-QMP-override-to-declare-target-already-.patch b/kvm-mirror-Allow-QMP-override-to-declare-target-already-.patch new file mode 100644 index 0000000..19b79e7 --- /dev/null +++ b/kvm-mirror-Allow-QMP-override-to-declare-target-already-.patch @@ -0,0 +1,295 @@ +From bc4571743fc3bbb829101fbf294615e3b8fb3577 Mon Sep 17 00:00:00 2001 +From: Eric Blake +Date: Fri, 9 May 2025 15:40:25 -0500 +Subject: [PATCH 08/14] mirror: Allow QMP override to declare target already + zero + +RH-Author: Eric Blake +RH-MergeRequest: 363: blockdev-mirror: More efficient handling of sparse mirrors +RH-Jira: RHEL-88435 RHEL-88437 +RH-Acked-by: Stefan Hajnoczi +RH-Acked-by: Miroslav Rezanina +RH-Commit: [8/14] 4ad06d67db8c43df4e6e0b8f929b1d8c19e4b338 (ebblake/centos-qemu-kvm) + +QEMU has an optimization for a just-created drive-mirror destination +that is not possible for blockdev-mirror (which can't create the +destination) - any time we know the destination starts life as all +zeroes, we can skip a pre-zeroing pass on the destination. Recent +patches have added an improved heuristic for detecting if a file +contains all zeroes, and we plan to use that heuristic in upcoming +patches. But since a heuristic cannot quickly detect all scenarios, +and there may be cases where the caller is aware of information that +QEMU cannot learn quickly, it makes sense to have a way to tell QEMU +to assume facts about the destination that can make the mirror +operation faster. Given our existing example of "qemu-img convert +--target-is-zero", it is time to expose this override in QMP for +blockdev-mirror as well. + +This patch results in some slight redundancy between the older +s->zero_target (set any time mode==FULL and the destination image was +not just created - ie. clear if drive-mirror is asking to skip the +pre-zero pass) and the newly-introduced s->target_is_zero (in addition +to the QMP override, it is set when drive-mirror creates the +destination image); this will be cleaned up in the next patch. + +There is also a subtlety that we must consider. When drive-mirror is +passing target_is_zero on behalf of a just-created image, we know the +image is sparse (skipping the pre-zeroing keeps it that way), so it +doesn't matter whether the destination also has "discard":"unmap" and +"detect-zeroes":"unmap". But now that we are letting the user set the +knob for target-is-zero, if the user passes a pre-existing file that +is fully allocated, it is fine to leave the file fully allocated under +"detect-zeroes":"on", but if the file is open with +"detect-zeroes":"unmap", we should really be trying harder to punch +holes in the destination for every region of zeroes copied from the +source. The easiest way to do this is to still run the pre-zeroing +pass (turning the entire destination file sparse before populating +just the allocated portions of the source), even though that currently +results in double I/O to the portions of the file that are allocated. +A later patch will add further optimizations to reduce redundant +zeroing I/O during the mirror operation. + +Since "target-is-zero":true is designed for optimizations, it is okay +to silently ignore the parameter rather than erroring if the user ever +sets the parameter in a scenario where the mirror job can't exploit it +(for example, when doing "sync":"top" instead of "sync":"full", we +can't pre-zero, so setting the parameter won't make a speed +difference). + +Signed-off-by: Eric Blake +Acked-by: Markus Armbruster +Message-ID: <20250509204341.3553601-23-eblake@redhat.com> +Reviewed-by: Sunny Zhu +Reviewed-by: Stefan Hajnoczi +(cherry picked from commit d17a34bfb94bda3a89d7320ae67255ded1d8c939) +Jira: https://issues.redhat.com/browse/RHEL-88435 +Jira: https://issues.redhat.com/browse/RHEL-88437 +Signed-off-by: Eric Blake +--- + block/mirror.c | 27 ++++++++++++++++++++++---- + blockdev.c | 18 ++++++++++------- + include/block/block_int-global-state.h | 3 ++- + qapi/block-core.json | 8 +++++++- + tests/unit/test-block-iothread.c | 2 +- + 5 files changed, 44 insertions(+), 14 deletions(-) + +diff --git a/block/mirror.c b/block/mirror.c +index 2599b75d09..4dcb50c81a 100644 +--- a/block/mirror.c ++++ b/block/mirror.c +@@ -55,6 +55,8 @@ typedef struct MirrorBlockJob { + BlockMirrorBackingMode backing_mode; + /* Whether the target image requires explicit zero-initialization */ + bool zero_target; ++ /* Whether the target should be assumed to be already zero initialized */ ++ bool target_is_zero; + /* + * To be accesssed with atomics. Written only under the BQL (required by the + * current implementation of mirror_change()). +@@ -844,12 +846,26 @@ static int coroutine_fn GRAPH_UNLOCKED mirror_dirty_init(MirrorBlockJob *s) + BlockDriverState *target_bs = blk_bs(s->target); + int ret = -EIO; + int64_t count; ++ bool punch_holes = ++ target_bs->detect_zeroes == BLOCKDEV_DETECT_ZEROES_OPTIONS_UNMAP && ++ bdrv_can_write_zeroes_with_unmap(target_bs); + + bdrv_graph_co_rdlock(); + bs = s->mirror_top_bs->backing->bs; + bdrv_graph_co_rdunlock(); + +- if (s->zero_target) { ++ if (s->zero_target && (!s->target_is_zero || punch_holes)) { ++ /* ++ * Here, we are in FULL mode; our goal is to avoid writing ++ * zeroes if the destination already reads as zero, except ++ * when we are trying to punch holes. This is possible if ++ * zeroing happened externally (s->target_is_zero) or if we ++ * have a fast way to pre-zero the image (the dirty bitmap ++ * will be populated later by the non-zero portions, the same ++ * as for TOP mode). If pre-zeroing is not fast, or we need ++ * to punch holes, then our only recourse is to write the ++ * entire image. ++ */ + if (!bdrv_can_write_zeroes_with_unmap(target_bs)) { + bdrv_set_dirty_bitmap(s->dirty_bitmap, 0, s->bdev_length); + return 0; +@@ -1714,7 +1730,7 @@ static BlockJob *mirror_start_job( + uint32_t granularity, int64_t buf_size, + MirrorSyncMode sync_mode, + BlockMirrorBackingMode backing_mode, +- bool zero_target, ++ bool zero_target, bool target_is_zero, + BlockdevOnError on_source_error, + BlockdevOnError on_target_error, + bool unmap, +@@ -1883,6 +1899,7 @@ static BlockJob *mirror_start_job( + s->sync_mode = sync_mode; + s->backing_mode = backing_mode; + s->zero_target = zero_target; ++ s->target_is_zero = target_is_zero; + qatomic_set(&s->copy_mode, copy_mode); + s->base = base; + s->base_overlay = bdrv_find_overlay(bs, base); +@@ -2011,7 +2028,7 @@ void mirror_start(const char *job_id, BlockDriverState *bs, + int creation_flags, int64_t speed, + uint32_t granularity, int64_t buf_size, + MirrorSyncMode mode, BlockMirrorBackingMode backing_mode, +- bool zero_target, ++ bool zero_target, bool target_is_zero, + BlockdevOnError on_source_error, + BlockdevOnError on_target_error, + bool unmap, const char *filter_node_name, +@@ -2034,7 +2051,8 @@ void mirror_start(const char *job_id, BlockDriverState *bs, + + mirror_start_job(job_id, bs, creation_flags, target, replaces, + speed, granularity, buf_size, mode, backing_mode, +- zero_target, on_source_error, on_target_error, unmap, ++ zero_target, ++ target_is_zero, on_source_error, on_target_error, unmap, + NULL, NULL, &mirror_job_driver, base, false, + filter_node_name, true, copy_mode, false, errp); + } +@@ -2062,6 +2080,7 @@ BlockJob *commit_active_start(const char *job_id, BlockDriverState *bs, + job = mirror_start_job( + job_id, bs, creation_flags, base, NULL, speed, 0, 0, + MIRROR_SYNC_MODE_TOP, MIRROR_LEAVE_BACKING_CHAIN, false, ++ false, + on_error, on_error, true, cb, opaque, + &commit_active_job_driver, base, auto_complete, + filter_node_name, false, MIRROR_COPY_MODE_BACKGROUND, +diff --git a/blockdev.c b/blockdev.c +index 1d1f27cfff..2e2fed539e 100644 +--- a/blockdev.c ++++ b/blockdev.c +@@ -2798,7 +2798,7 @@ static void blockdev_mirror_common(const char *job_id, BlockDriverState *bs, + const char *replaces, + enum MirrorSyncMode sync, + BlockMirrorBackingMode backing_mode, +- bool zero_target, ++ bool zero_target, bool target_is_zero, + bool has_speed, int64_t speed, + bool has_granularity, uint32_t granularity, + bool has_buf_size, int64_t buf_size, +@@ -2909,11 +2909,10 @@ static void blockdev_mirror_common(const char *job_id, BlockDriverState *bs, + /* pass the node name to replace to mirror start since it's loose coupling + * and will allow to check whether the node still exist at mirror completion + */ +- mirror_start(job_id, bs, target, +- replaces, job_flags, ++ mirror_start(job_id, bs, target, replaces, job_flags, + speed, granularity, buf_size, sync, backing_mode, zero_target, +- on_source_error, on_target_error, unmap, filter_node_name, +- copy_mode, errp); ++ target_is_zero, on_source_error, on_target_error, unmap, ++ filter_node_name, copy_mode, errp); + } + + void qmp_drive_mirror(DriveMirror *arg, Error **errp) +@@ -2928,6 +2927,7 @@ void qmp_drive_mirror(DriveMirror *arg, Error **errp) + int64_t size; + const char *format = arg->format; + bool zero_target; ++ bool target_is_zero; + int ret; + + bs = qmp_get_root_bs(arg->device, errp); +@@ -3044,6 +3044,8 @@ void qmp_drive_mirror(DriveMirror *arg, Error **errp) + zero_target = (arg->sync == MIRROR_SYNC_MODE_FULL && + (arg->mode == NEW_IMAGE_MODE_EXISTING || + !bdrv_has_zero_init(target_bs))); ++ target_is_zero = (arg->mode != NEW_IMAGE_MODE_EXISTING && ++ bdrv_has_zero_init(target_bs)); + bdrv_graph_rdunlock_main_loop(); + + +@@ -3055,7 +3057,7 @@ void qmp_drive_mirror(DriveMirror *arg, Error **errp) + + blockdev_mirror_common(arg->job_id, bs, target_bs, + arg->replaces, arg->sync, +- backing_mode, zero_target, ++ backing_mode, zero_target, target_is_zero, + arg->has_speed, arg->speed, + arg->has_granularity, arg->granularity, + arg->has_buf_size, arg->buf_size, +@@ -3085,6 +3087,7 @@ void qmp_blockdev_mirror(const char *job_id, + bool has_copy_mode, MirrorCopyMode copy_mode, + bool has_auto_finalize, bool auto_finalize, + bool has_auto_dismiss, bool auto_dismiss, ++ bool has_target_is_zero, bool target_is_zero, + Error **errp) + { + BlockDriverState *bs; +@@ -3115,7 +3118,8 @@ void qmp_blockdev_mirror(const char *job_id, + + blockdev_mirror_common(job_id, bs, target_bs, + replaces, sync, backing_mode, +- zero_target, has_speed, speed, ++ zero_target, has_target_is_zero && target_is_zero, ++ has_speed, speed, + has_granularity, granularity, + has_buf_size, buf_size, + has_on_source_error, on_source_error, +diff --git a/include/block/block_int-global-state.h b/include/block/block_int-global-state.h +index eb2d92a226..8cf0003ce7 100644 +--- a/include/block/block_int-global-state.h ++++ b/include/block/block_int-global-state.h +@@ -140,6 +140,7 @@ BlockJob *commit_active_start(const char *job_id, BlockDriverState *bs, + * @mode: Whether to collapse all images in the chain to the target. + * @backing_mode: How to establish the target's backing chain after completion. + * @zero_target: Whether the target should be explicitly zero-initialized ++ * @target_is_zero: Whether the target already is zero-initialized. + * @on_source_error: The action to take upon error reading from the source. + * @on_target_error: The action to take upon error writing to the target. + * @unmap: Whether to unmap target where source sectors only contain zeroes. +@@ -159,7 +160,7 @@ void mirror_start(const char *job_id, BlockDriverState *bs, + int creation_flags, int64_t speed, + uint32_t granularity, int64_t buf_size, + MirrorSyncMode mode, BlockMirrorBackingMode backing_mode, +- bool zero_target, ++ bool zero_target, bool target_is_zero, + BlockdevOnError on_source_error, + BlockdevOnError on_target_error, + bool unmap, const char *filter_node_name, +diff --git a/qapi/block-core.json b/qapi/block-core.json +index b1937780e1..7f70ec6d3c 100644 +--- a/qapi/block-core.json ++++ b/qapi/block-core.json +@@ -2538,6 +2538,11 @@ + # disappear from the query list without user intervention. + # Defaults to true. (Since 3.1) + # ++# @target-is-zero: Assume the destination reads as all zeroes before ++# the mirror started. Setting this to true can speed up the ++# mirror. Setting this to true when the destination is not ++# actually all zero can corrupt the destination. (Since 10.1) ++# + # Since: 2.6 + # + # .. qmp-example:: +@@ -2557,7 +2562,8 @@ + '*on-target-error': 'BlockdevOnError', + '*filter-node-name': 'str', + '*copy-mode': 'MirrorCopyMode', +- '*auto-finalize': 'bool', '*auto-dismiss': 'bool' }, ++ '*auto-finalize': 'bool', '*auto-dismiss': 'bool', ++ '*target-is-zero': 'bool'}, + 'allow-preconfig': true } + + ## +diff --git a/tests/unit/test-block-iothread.c b/tests/unit/test-block-iothread.c +index e26b3be593..54aed8252c 100644 +--- a/tests/unit/test-block-iothread.c ++++ b/tests/unit/test-block-iothread.c +@@ -755,7 +755,7 @@ static void test_propagate_mirror(void) + + /* Start a mirror job */ + mirror_start("job0", src, target, NULL, JOB_DEFAULT, 0, 0, 0, +- MIRROR_SYNC_MODE_NONE, MIRROR_OPEN_BACKING_CHAIN, false, ++ MIRROR_SYNC_MODE_NONE, MIRROR_OPEN_BACKING_CHAIN, false, false, + BLOCKDEV_ON_ERROR_REPORT, BLOCKDEV_ON_ERROR_REPORT, + false, "filter_node", MIRROR_COPY_MODE_BACKGROUND, + &error_abort); +-- +2.39.3 + diff --git a/kvm-mirror-Drop-redundant-zero_target-parameter.patch b/kvm-mirror-Drop-redundant-zero_target-parameter.patch new file mode 100644 index 0000000..bd35c8c --- /dev/null +++ b/kvm-mirror-Drop-redundant-zero_target-parameter.patch @@ -0,0 +1,241 @@ +From db1a158312c2b94af1c1a50e0f13ace6ae58f0b6 Mon Sep 17 00:00:00 2001 +From: Eric Blake +Date: Fri, 9 May 2025 15:40:26 -0500 +Subject: [PATCH 09/14] mirror: Drop redundant zero_target parameter + +RH-Author: Eric Blake +RH-MergeRequest: 363: blockdev-mirror: More efficient handling of sparse mirrors +RH-Jira: RHEL-88435 RHEL-88437 +RH-Acked-by: Stefan Hajnoczi +RH-Acked-by: Miroslav Rezanina +RH-Commit: [9/14] b4cbd267c81b4758f59e0d51b947fd450caf6ef5 (ebblake/centos-qemu-kvm) + +The two callers to a mirror job (drive-mirror and blockdev-mirror) set +zero_target precisely when sync mode == FULL, with the one exception +that drive-mirror skips zeroing the target if it was newly created and +reads as zero. But given the previous patch, that exception is +equally captured by target_is_zero. + +Meanwhile, there is another slight wrinkle, fortunately caught by +iotest 185: if the caller uses "sync":"top" but the source has no +backing file, the code in blockdev.c was changing sync to be FULL, but +only after it had set zero_target=false. In mirror.c, prior to recent +patches, this didn't matter: the only places that inspected sync were +setting is_none_mode (both TOP and FULL had set that to false), and +mirror_start() setting base = mode == MIRROR_SYNC_MODE_TOP ? +bdrv_backing_chain_next(bs) : NULL. But now that we are passing sync +around, the slammed sync mode would result in a new pre-zeroing pass +even when the user had passed "sync":"top" in an effort to skip +pre-zeroing. Fortunately, the assignment of base when bs has no +backing chain still works out to NULL if we don't slam things. So +with the forced change of sync ripped out of blockdev.c, the sync mode +is passed through the full callstack unmolested, and we can now +reliably reconstruct the same settings as what used to be passed in by +zero_target=false, without the redundant parameter. + +Signed-off-by: Eric Blake +Message-ID: <20250509204341.3553601-24-eblake@redhat.com> +Reviewed-by: Sunny Zhu +Reviewed-by: Stefan Hajnoczi +[eblake: Fix regression in iotest 185] +Signed-off-by: Eric Blake +(cherry picked from commit 253b43a29077de9266351e120c600a73b82e9c49) +Jira: https://issues.redhat.com/browse/RHEL-88435 +Jira: https://issues.redhat.com/browse/RHEL-88437 +Signed-off-by: Eric Blake +--- + block/mirror.c | 13 +++++-------- + blockdev.c | 19 ++++--------------- + include/block/block_int-global-state.h | 3 +-- + tests/unit/test-block-iothread.c | 2 +- + 4 files changed, 11 insertions(+), 26 deletions(-) + +diff --git a/block/mirror.c b/block/mirror.c +index 4dcb50c81a..d04db85883 100644 +--- a/block/mirror.c ++++ b/block/mirror.c +@@ -53,8 +53,6 @@ typedef struct MirrorBlockJob { + Error *replace_blocker; + MirrorSyncMode sync_mode; + BlockMirrorBackingMode backing_mode; +- /* Whether the target image requires explicit zero-initialization */ +- bool zero_target; + /* Whether the target should be assumed to be already zero initialized */ + bool target_is_zero; + /* +@@ -854,7 +852,9 @@ static int coroutine_fn GRAPH_UNLOCKED mirror_dirty_init(MirrorBlockJob *s) + bs = s->mirror_top_bs->backing->bs; + bdrv_graph_co_rdunlock(); + +- if (s->zero_target && (!s->target_is_zero || punch_holes)) { ++ if (s->sync_mode == MIRROR_SYNC_MODE_TOP) { ++ /* In TOP mode, there is no benefit to a pre-zeroing pass. */ ++ } else if (!s->target_is_zero || punch_holes) { + /* + * Here, we are in FULL mode; our goal is to avoid writing + * zeroes if the destination already reads as zero, except +@@ -1730,7 +1730,7 @@ static BlockJob *mirror_start_job( + uint32_t granularity, int64_t buf_size, + MirrorSyncMode sync_mode, + BlockMirrorBackingMode backing_mode, +- bool zero_target, bool target_is_zero, ++ bool target_is_zero, + BlockdevOnError on_source_error, + BlockdevOnError on_target_error, + bool unmap, +@@ -1898,7 +1898,6 @@ static BlockJob *mirror_start_job( + s->on_target_error = on_target_error; + s->sync_mode = sync_mode; + s->backing_mode = backing_mode; +- s->zero_target = zero_target; + s->target_is_zero = target_is_zero; + qatomic_set(&s->copy_mode, copy_mode); + s->base = base; +@@ -2028,7 +2027,7 @@ void mirror_start(const char *job_id, BlockDriverState *bs, + int creation_flags, int64_t speed, + uint32_t granularity, int64_t buf_size, + MirrorSyncMode mode, BlockMirrorBackingMode backing_mode, +- bool zero_target, bool target_is_zero, ++ bool target_is_zero, + BlockdevOnError on_source_error, + BlockdevOnError on_target_error, + bool unmap, const char *filter_node_name, +@@ -2051,7 +2050,6 @@ void mirror_start(const char *job_id, BlockDriverState *bs, + + mirror_start_job(job_id, bs, creation_flags, target, replaces, + speed, granularity, buf_size, mode, backing_mode, +- zero_target, + target_is_zero, on_source_error, on_target_error, unmap, + NULL, NULL, &mirror_job_driver, base, false, + filter_node_name, true, copy_mode, false, errp); +@@ -2080,7 +2078,6 @@ BlockJob *commit_active_start(const char *job_id, BlockDriverState *bs, + job = mirror_start_job( + job_id, bs, creation_flags, base, NULL, speed, 0, 0, + MIRROR_SYNC_MODE_TOP, MIRROR_LEAVE_BACKING_CHAIN, false, +- false, + on_error, on_error, true, cb, opaque, + &commit_active_job_driver, base, auto_complete, + filter_node_name, false, MIRROR_COPY_MODE_BACKGROUND, +diff --git a/blockdev.c b/blockdev.c +index 2e2fed539e..0fa8813efe 100644 +--- a/blockdev.c ++++ b/blockdev.c +@@ -2798,7 +2798,7 @@ static void blockdev_mirror_common(const char *job_id, BlockDriverState *bs, + const char *replaces, + enum MirrorSyncMode sync, + BlockMirrorBackingMode backing_mode, +- bool zero_target, bool target_is_zero, ++ bool target_is_zero, + bool has_speed, int64_t speed, + bool has_granularity, uint32_t granularity, + bool has_buf_size, int64_t buf_size, +@@ -2865,10 +2865,6 @@ static void blockdev_mirror_common(const char *job_id, BlockDriverState *bs, + return; + } + +- if (!bdrv_backing_chain_next(bs) && sync == MIRROR_SYNC_MODE_TOP) { +- sync = MIRROR_SYNC_MODE_FULL; +- } +- + if (!replaces) { + /* We want to mirror from @bs, but keep implicit filters on top */ + unfiltered_bs = bdrv_skip_implicit_filters(bs); +@@ -2910,7 +2906,7 @@ static void blockdev_mirror_common(const char *job_id, BlockDriverState *bs, + * and will allow to check whether the node still exist at mirror completion + */ + mirror_start(job_id, bs, target, replaces, job_flags, +- speed, granularity, buf_size, sync, backing_mode, zero_target, ++ speed, granularity, buf_size, sync, backing_mode, + target_is_zero, on_source_error, on_target_error, unmap, + filter_node_name, copy_mode, errp); + } +@@ -2926,7 +2922,6 @@ void qmp_drive_mirror(DriveMirror *arg, Error **errp) + int flags; + int64_t size; + const char *format = arg->format; +- bool zero_target; + bool target_is_zero; + int ret; + +@@ -3041,9 +3036,6 @@ void qmp_drive_mirror(DriveMirror *arg, Error **errp) + } + + bdrv_graph_rdlock_main_loop(); +- zero_target = (arg->sync == MIRROR_SYNC_MODE_FULL && +- (arg->mode == NEW_IMAGE_MODE_EXISTING || +- !bdrv_has_zero_init(target_bs))); + target_is_zero = (arg->mode != NEW_IMAGE_MODE_EXISTING && + bdrv_has_zero_init(target_bs)); + bdrv_graph_rdunlock_main_loop(); +@@ -3057,7 +3049,7 @@ void qmp_drive_mirror(DriveMirror *arg, Error **errp) + + blockdev_mirror_common(arg->job_id, bs, target_bs, + arg->replaces, arg->sync, +- backing_mode, zero_target, target_is_zero, ++ backing_mode, target_is_zero, + arg->has_speed, arg->speed, + arg->has_granularity, arg->granularity, + arg->has_buf_size, arg->buf_size, +@@ -3094,7 +3086,6 @@ void qmp_blockdev_mirror(const char *job_id, + BlockDriverState *target_bs; + AioContext *aio_context; + BlockMirrorBackingMode backing_mode = MIRROR_LEAVE_BACKING_CHAIN; +- bool zero_target; + int ret; + + bs = qmp_get_root_bs(device, errp); +@@ -3107,8 +3098,6 @@ void qmp_blockdev_mirror(const char *job_id, + return; + } + +- zero_target = (sync == MIRROR_SYNC_MODE_FULL); +- + aio_context = bdrv_get_aio_context(bs); + + ret = bdrv_try_change_aio_context(target_bs, aio_context, NULL, errp); +@@ -3118,7 +3107,7 @@ void qmp_blockdev_mirror(const char *job_id, + + blockdev_mirror_common(job_id, bs, target_bs, + replaces, sync, backing_mode, +- zero_target, has_target_is_zero && target_is_zero, ++ has_target_is_zero && target_is_zero, + has_speed, speed, + has_granularity, granularity, + has_buf_size, buf_size, +diff --git a/include/block/block_int-global-state.h b/include/block/block_int-global-state.h +index 8cf0003ce7..d21bd7fd2f 100644 +--- a/include/block/block_int-global-state.h ++++ b/include/block/block_int-global-state.h +@@ -139,7 +139,6 @@ BlockJob *commit_active_start(const char *job_id, BlockDriverState *bs, + * @buf_size: The amount of data that can be in flight at one time. + * @mode: Whether to collapse all images in the chain to the target. + * @backing_mode: How to establish the target's backing chain after completion. +- * @zero_target: Whether the target should be explicitly zero-initialized + * @target_is_zero: Whether the target already is zero-initialized. + * @on_source_error: The action to take upon error reading from the source. + * @on_target_error: The action to take upon error writing to the target. +@@ -160,7 +159,7 @@ void mirror_start(const char *job_id, BlockDriverState *bs, + int creation_flags, int64_t speed, + uint32_t granularity, int64_t buf_size, + MirrorSyncMode mode, BlockMirrorBackingMode backing_mode, +- bool zero_target, bool target_is_zero, ++ bool target_is_zero, + BlockdevOnError on_source_error, + BlockdevOnError on_target_error, + bool unmap, const char *filter_node_name, +diff --git a/tests/unit/test-block-iothread.c b/tests/unit/test-block-iothread.c +index 54aed8252c..e26b3be593 100644 +--- a/tests/unit/test-block-iothread.c ++++ b/tests/unit/test-block-iothread.c +@@ -755,7 +755,7 @@ static void test_propagate_mirror(void) + + /* Start a mirror job */ + mirror_start("job0", src, target, NULL, JOB_DEFAULT, 0, 0, 0, +- MIRROR_SYNC_MODE_NONE, MIRROR_OPEN_BACKING_CHAIN, false, false, ++ MIRROR_SYNC_MODE_NONE, MIRROR_OPEN_BACKING_CHAIN, false, + BLOCKDEV_ON_ERROR_REPORT, BLOCKDEV_ON_ERROR_REPORT, + false, "filter_node", MIRROR_COPY_MODE_BACKGROUND, + &error_abort); +-- +2.39.3 + diff --git a/kvm-mirror-Minor-refactoring.patch b/kvm-mirror-Minor-refactoring.patch new file mode 100644 index 0000000..d658673 --- /dev/null +++ b/kvm-mirror-Minor-refactoring.patch @@ -0,0 +1,90 @@ +From e95294aecc606deacf716861d716f9178b132ed8 Mon Sep 17 00:00:00 2001 +From: Eric Blake +Date: Fri, 9 May 2025 15:40:23 -0500 +Subject: [PATCH 06/14] mirror: Minor refactoring + +RH-Author: Eric Blake +RH-MergeRequest: 363: blockdev-mirror: More efficient handling of sparse mirrors +RH-Jira: RHEL-88435 RHEL-88437 +RH-Acked-by: Stefan Hajnoczi +RH-Acked-by: Miroslav Rezanina +RH-Commit: [6/14] 22a87aca8033f3f5d10fd224dc0786633a4d040f (ebblake/centos-qemu-kvm) + +Commit 5791ba52 (v9.2) pre-initialized ret in mirror_dirty_init to +silence a false positive compiler warning, even though in all code +paths where ret is used, it was guaranteed to be reassigned +beforehand. But since the function returns -errno, and -1 is not +always the right errno, it's better to initialize to -EIO. + +An upcoming patch wants to track two bitmaps in +do_sync_target_write(); this will be easier if the current variables +related to the dirty bitmap are renamed. + +Signed-off-by: Eric Blake +Reviewed-by: Stefan Hajnoczi +Message-ID: <20250509204341.3553601-21-eblake@redhat.com> +(cherry picked from commit 870f8963cf1a84f8ec929b05a6d68906974a76c5) +Jira: https://issues.redhat.com/browse/RHEL-88435 +Jira: https://issues.redhat.com/browse/RHEL-88437 +Signed-off-by: Eric Blake +--- + block/mirror.c | 22 +++++++++++----------- + 1 file changed, 11 insertions(+), 11 deletions(-) + +diff --git a/block/mirror.c b/block/mirror.c +index a53582f17b..34c6c5252e 100644 +--- a/block/mirror.c ++++ b/block/mirror.c +@@ -841,7 +841,7 @@ static int coroutine_fn GRAPH_UNLOCKED mirror_dirty_init(MirrorBlockJob *s) + int64_t offset; + BlockDriverState *bs; + BlockDriverState *target_bs = blk_bs(s->target); +- int ret = -1; ++ int ret = -EIO; + int64_t count; + + bdrv_graph_co_rdlock(); +@@ -1341,7 +1341,7 @@ do_sync_target_write(MirrorBlockJob *job, MirrorMethod method, + { + int ret; + size_t qiov_offset = 0; +- int64_t bitmap_offset, bitmap_end; ++ int64_t dirty_bitmap_offset, dirty_bitmap_end; + + if (!QEMU_IS_ALIGNED(offset, job->granularity) && + bdrv_dirty_bitmap_get(job->dirty_bitmap, offset)) +@@ -1388,11 +1388,11 @@ do_sync_target_write(MirrorBlockJob *job, MirrorMethod method, + * Tails are either clean or shrunk, so for bitmap resetting + * we safely align the range down. + */ +- bitmap_offset = QEMU_ALIGN_UP(offset, job->granularity); +- bitmap_end = QEMU_ALIGN_DOWN(offset + bytes, job->granularity); +- if (bitmap_offset < bitmap_end) { +- bdrv_reset_dirty_bitmap(job->dirty_bitmap, bitmap_offset, +- bitmap_end - bitmap_offset); ++ dirty_bitmap_offset = QEMU_ALIGN_UP(offset, job->granularity); ++ dirty_bitmap_end = QEMU_ALIGN_DOWN(offset + bytes, job->granularity); ++ if (dirty_bitmap_offset < dirty_bitmap_end) { ++ bdrv_reset_dirty_bitmap(job->dirty_bitmap, dirty_bitmap_offset, ++ dirty_bitmap_end - dirty_bitmap_offset); + } + + job_progress_increase_remaining(&job->common.job, bytes); +@@ -1430,10 +1430,10 @@ do_sync_target_write(MirrorBlockJob *job, MirrorMethod method, + * at function start, and they must be still dirty, as we've locked + * the region for in-flight op. + */ +- bitmap_offset = QEMU_ALIGN_DOWN(offset, job->granularity); +- bitmap_end = QEMU_ALIGN_UP(offset + bytes, job->granularity); +- bdrv_set_dirty_bitmap(job->dirty_bitmap, bitmap_offset, +- bitmap_end - bitmap_offset); ++ dirty_bitmap_offset = QEMU_ALIGN_DOWN(offset, job->granularity); ++ dirty_bitmap_end = QEMU_ALIGN_UP(offset + bytes, job->granularity); ++ bdrv_set_dirty_bitmap(job->dirty_bitmap, dirty_bitmap_offset, ++ dirty_bitmap_end - dirty_bitmap_offset); + qatomic_set(&job->actively_synced, false); + + action = mirror_error_action(job, false, -ret); +-- +2.39.3 + diff --git a/kvm-mirror-Pass-full-sync-mode-rather-than-bool-to-inter.patch b/kvm-mirror-Pass-full-sync-mode-rather-than-bool-to-inter.patch new file mode 100644 index 0000000..3029150 --- /dev/null +++ b/kvm-mirror-Pass-full-sync-mode-rather-than-bool-to-inter.patch @@ -0,0 +1,139 @@ +From db0b92495a4e774caafaaa148e778b575112bad2 Mon Sep 17 00:00:00 2001 +From: Eric Blake +Date: Fri, 9 May 2025 15:40:24 -0500 +Subject: [PATCH 07/14] mirror: Pass full sync mode rather than bool to + internals + +RH-Author: Eric Blake +RH-MergeRequest: 363: blockdev-mirror: More efficient handling of sparse mirrors +RH-Jira: RHEL-88435 RHEL-88437 +RH-Acked-by: Stefan Hajnoczi +RH-Acked-by: Miroslav Rezanina +RH-Commit: [7/14] e8872e3edad069ee6c76f6104c1bc277c025b5ac (ebblake/centos-qemu-kvm) + +Out of the five possible values for MirrorSyncMode, INCREMENTAL and +BITMAP are already rejected up front in mirror_start, leaving NONE, +TOP, and FULL as the remaining values that the code was collapsing +into a single bool is_none_mode. Furthermore, mirror_dirty_init() is +only reachable for modes TOP and FULL, as further guided by +s->zero_target. However, upcoming patches want to further optimize +the pre-zeroing pass of a sync=full mirror in mirror_dirty_init(), +while avoiding that pass on a sync=top action. Instead of throwing +away context by collapsing these two values into +s->is_none_mode=false, it is better to pass s->sync_mode throughout +the entire operation. For active commit, the desired semantics match +sync mode TOP. + +Signed-off-by: Eric Blake +Message-ID: <20250509204341.3553601-22-eblake@redhat.com> +Reviewed-by: Sunny Zhu +Reviewed-by: Stefan Hajnoczi +(cherry picked from commit 9474d97bd7421b4fe7c806ab0949697514d11e88) +Jira: https://issues.redhat.com/browse/RHEL-88435 +Jira: https://issues.redhat.com/browse/RHEL-88437 +Signed-off-by: Eric Blake +--- + block/mirror.c | 24 ++++++++++++------------ + 1 file changed, 12 insertions(+), 12 deletions(-) + +diff --git a/block/mirror.c b/block/mirror.c +index 34c6c5252e..2599b75d09 100644 +--- a/block/mirror.c ++++ b/block/mirror.c +@@ -51,7 +51,7 @@ typedef struct MirrorBlockJob { + BlockDriverState *to_replace; + /* Used to block operations on the drive-mirror-replace target */ + Error *replace_blocker; +- bool is_none_mode; ++ MirrorSyncMode sync_mode; + BlockMirrorBackingMode backing_mode; + /* Whether the target image requires explicit zero-initialization */ + bool zero_target; +@@ -723,9 +723,10 @@ static int mirror_exit_common(Job *job) + &error_abort); + + if (!abort && s->backing_mode == MIRROR_SOURCE_BACKING_CHAIN) { +- BlockDriverState *backing = s->is_none_mode ? src : s->base; ++ BlockDriverState *backing; + BlockDriverState *unfiltered_target = bdrv_skip_filters(target_bs); + ++ backing = s->sync_mode == MIRROR_SYNC_MODE_NONE ? src : s->base; + if (bdrv_cow_bs(unfiltered_target) != backing) { + bdrv_set_backing_hd(unfiltered_target, backing, &local_err); + if (local_err) { +@@ -1020,7 +1021,7 @@ static int coroutine_fn mirror_run(Job *job, Error **errp) + mirror_free_init(s); + + s->last_pause_ns = qemu_clock_get_ns(QEMU_CLOCK_REALTIME); +- if (!s->is_none_mode) { ++ if (s->sync_mode != MIRROR_SYNC_MODE_NONE) { + ret = mirror_dirty_init(s); + if (ret < 0 || job_is_cancelled(&s->common.job)) { + goto immediate_exit; +@@ -1711,6 +1712,7 @@ static BlockJob *mirror_start_job( + int creation_flags, BlockDriverState *target, + const char *replaces, int64_t speed, + uint32_t granularity, int64_t buf_size, ++ MirrorSyncMode sync_mode, + BlockMirrorBackingMode backing_mode, + bool zero_target, + BlockdevOnError on_source_error, +@@ -1719,7 +1721,7 @@ static BlockJob *mirror_start_job( + BlockCompletionFunc *cb, + void *opaque, + const BlockJobDriver *driver, +- bool is_none_mode, BlockDriverState *base, ++ BlockDriverState *base, + bool auto_complete, const char *filter_node_name, + bool is_mirror, MirrorCopyMode copy_mode, + bool base_ro, +@@ -1878,7 +1880,7 @@ static BlockJob *mirror_start_job( + s->replaces = g_strdup(replaces); + s->on_source_error = on_source_error; + s->on_target_error = on_target_error; +- s->is_none_mode = is_none_mode; ++ s->sync_mode = sync_mode; + s->backing_mode = backing_mode; + s->zero_target = zero_target; + qatomic_set(&s->copy_mode, copy_mode); +@@ -2015,7 +2017,6 @@ void mirror_start(const char *job_id, BlockDriverState *bs, + bool unmap, const char *filter_node_name, + MirrorCopyMode copy_mode, Error **errp) + { +- bool is_none_mode; + BlockDriverState *base; + + GLOBAL_STATE_CODE(); +@@ -2028,14 +2029,13 @@ void mirror_start(const char *job_id, BlockDriverState *bs, + } + + bdrv_graph_rdlock_main_loop(); +- is_none_mode = mode == MIRROR_SYNC_MODE_NONE; + base = mode == MIRROR_SYNC_MODE_TOP ? bdrv_backing_chain_next(bs) : NULL; + bdrv_graph_rdunlock_main_loop(); + + mirror_start_job(job_id, bs, creation_flags, target, replaces, +- speed, granularity, buf_size, backing_mode, zero_target, +- on_source_error, on_target_error, unmap, NULL, NULL, +- &mirror_job_driver, is_none_mode, base, false, ++ speed, granularity, buf_size, mode, backing_mode, ++ zero_target, on_source_error, on_target_error, unmap, ++ NULL, NULL, &mirror_job_driver, base, false, + filter_node_name, true, copy_mode, false, errp); + } + +@@ -2061,9 +2061,9 @@ BlockJob *commit_active_start(const char *job_id, BlockDriverState *bs, + + job = mirror_start_job( + job_id, bs, creation_flags, base, NULL, speed, 0, 0, +- MIRROR_LEAVE_BACKING_CHAIN, false, ++ MIRROR_SYNC_MODE_TOP, MIRROR_LEAVE_BACKING_CHAIN, false, + on_error, on_error, true, cb, opaque, +- &commit_active_job_driver, false, base, auto_complete, ++ &commit_active_job_driver, base, auto_complete, + filter_node_name, false, MIRROR_COPY_MODE_BACKGROUND, + base_read_only, errp); + if (!job) { +-- +2.39.3 + diff --git a/kvm-mirror-Reduce-I-O-when-destination-is-detect-zeroes-.patch b/kvm-mirror-Reduce-I-O-when-destination-is-detect-zeroes-.patch new file mode 100644 index 0000000..27a0ccc --- /dev/null +++ b/kvm-mirror-Reduce-I-O-when-destination-is-detect-zeroes-.patch @@ -0,0 +1,58 @@ +From 9fedd14da6f1dc7aa3f0711d86f722397d080993 Mon Sep 17 00:00:00 2001 +From: Eric Blake +Date: Tue, 13 May 2025 17:00:45 -0500 +Subject: [PATCH 14/14] mirror: Reduce I/O when destination is + detect-zeroes:unmap + +RH-Author: Eric Blake +RH-MergeRequest: 363: blockdev-mirror: More efficient handling of sparse mirrors +RH-Jira: RHEL-88435 RHEL-88437 +RH-Acked-by: Stefan Hajnoczi +RH-Acked-by: Miroslav Rezanina +RH-Commit: [14/14] d4ba9d88a8da00f82c2ba7ebf050152fbe1e2465 (ebblake/centos-qemu-kvm) + +If we are going to punch holes in the mirror destination even for the +portions where the source image is unallocated, it is nicer to treat +the entire image as dirty and punch as we go, rather than pre-zeroing +the entire image just to re-do I/O to the allocated portions of the +image. + +Signed-off-by: Eric Blake +Message-ID: <20250513220142.535200-2-eblake@redhat.com> +Reviewed-by: Stefan Hajnoczi +(cherry picked from commit 9abfc81246c9cc1845080eec5920779961187c07) +Jira: https://issues.redhat.com/browse/RHEL-88435 +Jira: https://issues.redhat.com/browse/RHEL-88437 +Signed-off-by: Eric Blake +--- + block/mirror.c | 13 +++++++++---- + 1 file changed, 9 insertions(+), 4 deletions(-) + +diff --git a/block/mirror.c b/block/mirror.c +index 724318f037..c2c5099c95 100644 +--- a/block/mirror.c ++++ b/block/mirror.c +@@ -920,11 +920,16 @@ static int coroutine_fn GRAPH_UNLOCKED mirror_dirty_init(MirrorBlockJob *s) + * zeroing happened externally (ret > 0) or if we have a fast + * way to pre-zero the image (the dirty bitmap will be + * populated later by the non-zero portions, the same as for +- * TOP mode). If pre-zeroing is not fast, then our only +- * recourse is to mark the entire image dirty. The act of +- * pre-zeroing will populate the zero bitmap. ++ * TOP mode). If pre-zeroing is not fast, or we need to visit ++ * the entire image in order to punch holes even in the ++ * non-allocated regions of the source, then just mark the ++ * entire image dirty and leave the zero bitmap clear at this ++ * point in time. Otherwise, it can be faster to pre-zero the ++ * image now, even if we re-write the allocated portions of ++ * the disk later, and the pre-zero pass will populate the ++ * zero bitmap. + */ +- if (!bdrv_can_write_zeroes_with_unmap(target_bs)) { ++ if (!bdrv_can_write_zeroes_with_unmap(target_bs) || punch_holes) { + bdrv_set_dirty_bitmap(s->dirty_bitmap, 0, s->bdev_length); + return 0; + } +-- +2.39.3 + diff --git a/kvm-mirror-Skip-pre-zeroing-destination-if-it-is-already.patch b/kvm-mirror-Skip-pre-zeroing-destination-if-it-is-already.patch new file mode 100644 index 0000000..cb2b25f --- /dev/null +++ b/kvm-mirror-Skip-pre-zeroing-destination-if-it-is-already.patch @@ -0,0 +1,180 @@ +From 92a033b6c8394c8efb5b881cbbe463eeff5711cd Mon Sep 17 00:00:00 2001 +From: Eric Blake +Date: Fri, 9 May 2025 15:40:27 -0500 +Subject: [PATCH 10/14] mirror: Skip pre-zeroing destination if it is already + zero + +RH-Author: Eric Blake +RH-MergeRequest: 363: blockdev-mirror: More efficient handling of sparse mirrors +RH-Jira: RHEL-88435 RHEL-88437 +RH-Acked-by: Stefan Hajnoczi +RH-Acked-by: Miroslav Rezanina +RH-Commit: [10/14] 5d86d9c763a1bb49fab591a45e211d3be819ccfe (ebblake/centos-qemu-kvm) + +When doing a sync=full mirroring, we can skip pre-zeroing the +destination if it already reads as zeroes and we are not also trying +to punch holes due to detect-zeroes. With this patch, there are fewer +scenarios that have to pass in an explicit target-is-zero, while still +resulting in a sparse destination remaining sparse. + +A later patch will then further improve things to skip writing to the +destination for parts of the image where the source is zero; but even +with just this patch, it is possible to see a difference for any +source that does not report itself as fully allocated, coupled with a +destination BDS that can quickly report that it already reads as zero. +(For a source that reports as fully allocated, such as a file, the +rest of mirror_dirty_init() still sets the entire dirty bitmap to +true, so even though we avoided the pre-zeroing, we are not yet +avoiding all redundant I/O). + +Iotest 194 detects the difference made by this patch: for a file +source (where block status reports the entire image as allocated, and +therefore we end up writing zeroes everywhere in the destination +anyways), the job length remains the same. But for a qcow2 source and +a destination that reads as all zeroes, the dirty bitmap changes to +just tracking the allocated portions of the source, which results in +faster completion and smaller job statistics. For the test to pass +with both ./check -file and -qcow2, a new python filter is needed to +mask out the now-varying job amounts (this matches the shell filters +_filter_block_job_{offset,len} in common.filter). A later test will +also be added which further validates expected sparseness, so it does +not matter that 194 is no longer explicitly looking at how many bytes +were copied. + +Signed-off-by: Eric Blake +Message-ID: <20250509204341.3553601-25-eblake@redhat.com> +Reviewed-by: Sunny Zhu +Reviewed-by: Stefan Hajnoczi +(cherry picked from commit 181a63667adf16c35b57e446def3e41c70f1fea6) +Jira: https://issues.redhat.com/browse/RHEL-88435 +Jira: https://issues.redhat.com/browse/RHEL-88437 +Signed-off-by: Eric Blake +--- + block/mirror.c | 24 ++++++++++++++++-------- + tests/qemu-iotests/194 | 6 ++++-- + tests/qemu-iotests/194.out | 4 ++-- + tests/qemu-iotests/iotests.py | 12 +++++++++++- + 4 files changed, 33 insertions(+), 13 deletions(-) + +diff --git a/block/mirror.c b/block/mirror.c +index d04db85883..bca99ec206 100644 +--- a/block/mirror.c ++++ b/block/mirror.c +@@ -848,23 +848,31 @@ static int coroutine_fn GRAPH_UNLOCKED mirror_dirty_init(MirrorBlockJob *s) + target_bs->detect_zeroes == BLOCKDEV_DETECT_ZEROES_OPTIONS_UNMAP && + bdrv_can_write_zeroes_with_unmap(target_bs); + ++ /* Determine if the image is already zero, regardless of sync mode. */ + bdrv_graph_co_rdlock(); + bs = s->mirror_top_bs->backing->bs; ++ if (s->target_is_zero) { ++ ret = 1; ++ } else { ++ ret = bdrv_co_is_all_zeroes(target_bs); ++ } + bdrv_graph_co_rdunlock(); + +- if (s->sync_mode == MIRROR_SYNC_MODE_TOP) { ++ /* Determine if a pre-zeroing pass is necessary. */ ++ if (ret < 0) { ++ return ret; ++ } else if (s->sync_mode == MIRROR_SYNC_MODE_TOP) { + /* In TOP mode, there is no benefit to a pre-zeroing pass. */ +- } else if (!s->target_is_zero || punch_holes) { ++ } else if (ret == 0 || punch_holes) { + /* + * Here, we are in FULL mode; our goal is to avoid writing + * zeroes if the destination already reads as zero, except + * when we are trying to punch holes. This is possible if +- * zeroing happened externally (s->target_is_zero) or if we +- * have a fast way to pre-zero the image (the dirty bitmap +- * will be populated later by the non-zero portions, the same +- * as for TOP mode). If pre-zeroing is not fast, or we need +- * to punch holes, then our only recourse is to write the +- * entire image. ++ * zeroing happened externally (ret > 0) or if we have a fast ++ * way to pre-zero the image (the dirty bitmap will be ++ * populated later by the non-zero portions, the same as for ++ * TOP mode). If pre-zeroing is not fast, or we need to punch ++ * holes, then our only recourse is to write the entire image. + */ + if (!bdrv_can_write_zeroes_with_unmap(target_bs)) { + bdrv_set_dirty_bitmap(s->dirty_bitmap, 0, s->bdev_length); +diff --git a/tests/qemu-iotests/194 b/tests/qemu-iotests/194 +index d0b9c084f5..e114c0b269 100755 +--- a/tests/qemu-iotests/194 ++++ b/tests/qemu-iotests/194 +@@ -62,7 +62,8 @@ with iotests.FilePath('source.img') as source_img_path, \ + + iotests.log('Waiting for `drive-mirror` to complete...') + iotests.log(source_vm.event_wait('BLOCK_JOB_READY'), +- filters=[iotests.filter_qmp_event]) ++ filters=[iotests.filter_qmp_event, ++ iotests.filter_block_job]) + + iotests.log('Starting migration...') + capabilities = [{'capability': 'events', 'state': True}, +@@ -88,7 +89,8 @@ with iotests.FilePath('source.img') as source_img_path, \ + + while True: + event2 = source_vm.event_wait('BLOCK_JOB_COMPLETED') +- iotests.log(event2, filters=[iotests.filter_qmp_event]) ++ iotests.log(event2, filters=[iotests.filter_qmp_event, ++ iotests.filter_block_job]) + if event2['event'] == 'BLOCK_JOB_COMPLETED': + iotests.log('Stopping the NBD server on destination...') + iotests.log(dest_vm.qmp('nbd-server-stop')) +diff --git a/tests/qemu-iotests/194.out b/tests/qemu-iotests/194.out +index 6940e809cd..d02655a514 100644 +--- a/tests/qemu-iotests/194.out ++++ b/tests/qemu-iotests/194.out +@@ -7,7 +7,7 @@ Launching NBD server on destination... + Starting `drive-mirror` on source... + {"return": {}} + Waiting for `drive-mirror` to complete... +-{"data": {"device": "mirror-job0", "len": 1073741824, "offset": 1073741824, "speed": 0, "type": "mirror"}, "event": "BLOCK_JOB_READY", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}} ++{"data": {"device": "mirror-job0", "len": "LEN", "offset": "OFFSET", "speed": 0, "type": "mirror"}, "event": "BLOCK_JOB_READY", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}} + Starting migration... + {"return": {}} + {"execute": "migrate-start-postcopy", "arguments": {}} +@@ -18,7 +18,7 @@ Starting migration... + {"data": {"status": "completed"}, "event": "MIGRATION", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}} + Gracefully ending the `drive-mirror` job on source... + {"return": {}} +-{"data": {"device": "mirror-job0", "len": 1073741824, "offset": 1073741824, "speed": 0, "type": "mirror"}, "event": "BLOCK_JOB_COMPLETED", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}} ++{"data": {"device": "mirror-job0", "len": "LEN", "offset": "OFFSET", "speed": 0, "type": "mirror"}, "event": "BLOCK_JOB_COMPLETED", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}} + Stopping the NBD server on destination... + {"return": {}} + Wait for migration completion on target... +diff --git a/tests/qemu-iotests/iotests.py b/tests/qemu-iotests/iotests.py +index 7292c8b342..05274772ce 100644 +--- a/tests/qemu-iotests/iotests.py ++++ b/tests/qemu-iotests/iotests.py +@@ -601,13 +601,23 @@ def filter_chown(msg): + return chown_re.sub("chown UID:GID", msg) + + def filter_qmp_event(event): +- '''Filter a QMP event dict''' ++ '''Filter the timestamp of a QMP event dict''' + event = dict(event) + if 'timestamp' in event: + event['timestamp']['seconds'] = 'SECS' + event['timestamp']['microseconds'] = 'USECS' + return event + ++def filter_block_job(event): ++ '''Filter the offset and length of a QMP block job event dict''' ++ event = dict(event) ++ if 'data' in event: ++ if 'offset' in event['data']: ++ event['data']['offset'] = 'OFFSET' ++ if 'len' in event['data']: ++ event['data']['len'] = 'LEN' ++ return event ++ + def filter_qmp(qmsg, filter_fn): + '''Given a string filter, filter a QMP object's values. + filter_fn takes a (key, value) pair.''' +-- +2.39.3 + diff --git a/kvm-mirror-Skip-writing-zeroes-when-target-is-already-ze.patch b/kvm-mirror-Skip-writing-zeroes-when-target-is-already-ze.patch new file mode 100644 index 0000000..af29809 --- /dev/null +++ b/kvm-mirror-Skip-writing-zeroes-when-target-is-already-ze.patch @@ -0,0 +1,355 @@ +From cc72e6ec30fb113b82fcdb61f79a0fae18e31e79 Mon Sep 17 00:00:00 2001 +From: Eric Blake +Date: Fri, 9 May 2025 15:40:28 -0500 +Subject: [PATCH 11/14] mirror: Skip writing zeroes when target is already zero + +RH-Author: Eric Blake +RH-MergeRequest: 363: blockdev-mirror: More efficient handling of sparse mirrors +RH-Jira: RHEL-88435 RHEL-88437 +RH-Acked-by: Stefan Hajnoczi +RH-Acked-by: Miroslav Rezanina +RH-Commit: [11/14] 82df7fcf94606e6b6570469d3c05666da5039408 (ebblake/centos-qemu-kvm) + +When mirroring, the goal is to ensure that the destination reads the +same as the source; this goal is met whether the destination is sparse +or fully-allocated (except when explicitly punching holes, then merely +reading zero is not enough to know if it is sparse, so we still want +to punch the hole). Avoiding a redundant write to zero (whether in +the background because the zero cluster was marked in the dirty +bitmap, or in the foreground because the guest is writing zeroes) when +the destination already reads as zero makes mirroring faster, and +avoids allocating the destination merely because the source reports as +allocated. + +The effect is especially pronounced when the source is a raw file. +That's because when the source is a qcow2 file, the dirty bitmap only +visits the portions of the source that are allocated, which tend to be +non-zero. But when the source is a raw file, +bdrv_co_is_allocated_above() reports the entire file as allocated so +mirror_dirty_init sets the entire dirty bitmap, and it is only later +during mirror_iteration that we change to consulting the more precise +bdrv_co_block_status_above() to learn where the source reads as zero. + +Remember that since a mirror operation can write a cluster more than +once (every time the guest changes the source, the destination is also +changed to keep up), and the guest can change whether a given cluster +reads as zero, is discarded, or has non-zero data over the course of +the mirror operation, we can't take the shortcut of relying on +s->target_is_zero (which is static for the life of the job) in +mirror_co_zero() to see if the destination is already zero, because +that information may be stale. Any solution we use must be dynamic in +the face of the guest writing or discarding a cluster while the mirror +has been ongoing. + +We could just teach mirror_co_zero() to do a block_status() probe of +the destination, and skip the zeroes if the destination already reads +as zero, but we know from past experience that extra block_status() +calls are not always cheap (tmpfs, anyone?), especially when they are +random access rather than linear. Use of block_status() of the source +by the background task in a linear fashion is not our bottleneck (it's +a background task, after all); but since mirroring can be done while +the source is actively being changed, we don't want a slow +block_status() of the destination to occur on the hot path of the +guest trying to do random-access writes to the source. + +So this patch takes a slightly different approach: any time we have to +track dirty clusters, we can also track which clusters are known to +read as zero. For sync=TOP or when we are punching holes from +"detect-zeroes":"unmap", the zero bitmap starts out empty, but +prevents a second write zero to a cluster that was already zero by an +earlier pass; for sync=FULL when we are not punching holes, the zero +bitmap starts out full if the destination reads as zero during +initialization. Either way, I/O to the destination can now avoid +redundant write zero to a cluster that already reads as zero, all +without having to do a block_status() per write on the destination. + +With this patch, if I create a raw sparse destination file, connect it +with QMP 'blockdev-add' while leaving it at the default "discard": +"ignore", then run QMP 'blockdev-mirror' with "sync": "full", the +destination remains sparse rather than fully allocated. Meanwhile, a +destination image that is already fully allocated remains so unless it +was opened with "detect-zeroes": "unmap". And any time writing zeroes +is skipped, the job counters are not incremented. + +Signed-off-by: Eric Blake +Message-ID: <20250509204341.3553601-26-eblake@redhat.com> +Reviewed-by: Stefan Hajnoczi +(cherry picked from commit 7e277545b90874171128804e256a538fb0e8dd7e) +Jira: https://issues.redhat.com/browse/RHEL-88435 +Jira: https://issues.redhat.com/browse/RHEL-88437 +Signed-off-by: Eric Blake +--- + block/mirror.c | 107 ++++++++++++++++++++++++++++++++++++++++++------- + 1 file changed, 93 insertions(+), 14 deletions(-) + +diff --git a/block/mirror.c b/block/mirror.c +index bca99ec206..724318f037 100644 +--- a/block/mirror.c ++++ b/block/mirror.c +@@ -73,6 +73,7 @@ typedef struct MirrorBlockJob { + size_t buf_size; + int64_t bdev_length; + unsigned long *cow_bitmap; ++ unsigned long *zero_bitmap; + BdrvDirtyBitmap *dirty_bitmap; + BdrvDirtyBitmapIter *dbi; + uint8_t *buf; +@@ -108,9 +109,12 @@ struct MirrorOp { + int64_t offset; + uint64_t bytes; + +- /* The pointee is set by mirror_co_read(), mirror_co_zero(), and +- * mirror_co_discard() before yielding for the first time */ ++ /* ++ * These pointers are set by mirror_co_read(), mirror_co_zero(), and ++ * mirror_co_discard() before yielding for the first time ++ */ + int64_t *bytes_handled; ++ bool *io_skipped; + + bool is_pseudo_op; + bool is_active_write; +@@ -408,15 +412,34 @@ static void coroutine_fn mirror_co_read(void *opaque) + static void coroutine_fn mirror_co_zero(void *opaque) + { + MirrorOp *op = opaque; +- int ret; ++ bool write_needed = true; ++ int ret = 0; + + op->s->in_flight++; + op->s->bytes_in_flight += op->bytes; + *op->bytes_handled = op->bytes; + op->is_in_flight = true; + +- ret = blk_co_pwrite_zeroes(op->s->target, op->offset, op->bytes, +- op->s->unmap ? BDRV_REQ_MAY_UNMAP : 0); ++ if (op->s->zero_bitmap) { ++ unsigned long end = DIV_ROUND_UP(op->offset + op->bytes, ++ op->s->granularity); ++ assert(QEMU_IS_ALIGNED(op->offset, op->s->granularity)); ++ assert(QEMU_IS_ALIGNED(op->bytes, op->s->granularity) || ++ op->offset + op->bytes == op->s->bdev_length); ++ if (find_next_zero_bit(op->s->zero_bitmap, end, ++ op->offset / op->s->granularity) == end) { ++ write_needed = false; ++ *op->io_skipped = true; ++ } ++ } ++ if (write_needed) { ++ ret = blk_co_pwrite_zeroes(op->s->target, op->offset, op->bytes, ++ op->s->unmap ? BDRV_REQ_MAY_UNMAP : 0); ++ } ++ if (ret >= 0 && op->s->zero_bitmap) { ++ bitmap_set(op->s->zero_bitmap, op->offset / op->s->granularity, ++ DIV_ROUND_UP(op->bytes, op->s->granularity)); ++ } + mirror_write_complete(op, ret); + } + +@@ -435,29 +458,43 @@ static void coroutine_fn mirror_co_discard(void *opaque) + } + + static unsigned mirror_perform(MirrorBlockJob *s, int64_t offset, +- unsigned bytes, MirrorMethod mirror_method) ++ unsigned bytes, MirrorMethod mirror_method, ++ bool *io_skipped) + { + MirrorOp *op; + Coroutine *co; + int64_t bytes_handled = -1; + ++ assert(QEMU_IS_ALIGNED(offset, s->granularity)); ++ assert(QEMU_IS_ALIGNED(bytes, s->granularity) || ++ offset + bytes == s->bdev_length); + op = g_new(MirrorOp, 1); + *op = (MirrorOp){ + .s = s, + .offset = offset, + .bytes = bytes, + .bytes_handled = &bytes_handled, ++ .io_skipped = io_skipped, + }; + qemu_co_queue_init(&op->waiting_requests); + + switch (mirror_method) { + case MIRROR_METHOD_COPY: ++ if (s->zero_bitmap) { ++ bitmap_clear(s->zero_bitmap, offset / s->granularity, ++ DIV_ROUND_UP(bytes, s->granularity)); ++ } + co = qemu_coroutine_create(mirror_co_read, op); + break; + case MIRROR_METHOD_ZERO: ++ /* s->zero_bitmap handled in mirror_co_zero */ + co = qemu_coroutine_create(mirror_co_zero, op); + break; + case MIRROR_METHOD_DISCARD: ++ if (s->zero_bitmap) { ++ bitmap_clear(s->zero_bitmap, offset / s->granularity, ++ DIV_ROUND_UP(bytes, s->granularity)); ++ } + co = qemu_coroutine_create(mirror_co_discard, op); + break; + default: +@@ -568,6 +605,7 @@ static void coroutine_fn GRAPH_UNLOCKED mirror_iteration(MirrorBlockJob *s) + int ret = -1; + int64_t io_bytes; + int64_t io_bytes_acct; ++ bool io_skipped = false; + MirrorMethod mirror_method = MIRROR_METHOD_COPY; + + assert(!(offset % s->granularity)); +@@ -611,8 +649,10 @@ static void coroutine_fn GRAPH_UNLOCKED mirror_iteration(MirrorBlockJob *s) + } + + io_bytes = mirror_clip_bytes(s, offset, io_bytes); +- io_bytes = mirror_perform(s, offset, io_bytes, mirror_method); +- if (mirror_method != MIRROR_METHOD_COPY && write_zeroes_ok) { ++ io_bytes = mirror_perform(s, offset, io_bytes, mirror_method, ++ &io_skipped); ++ if (io_skipped || ++ (mirror_method != MIRROR_METHOD_COPY && write_zeroes_ok)) { + io_bytes_acct = 0; + } else { + io_bytes_acct = io_bytes; +@@ -847,8 +887,10 @@ static int coroutine_fn GRAPH_UNLOCKED mirror_dirty_init(MirrorBlockJob *s) + bool punch_holes = + target_bs->detect_zeroes == BLOCKDEV_DETECT_ZEROES_OPTIONS_UNMAP && + bdrv_can_write_zeroes_with_unmap(target_bs); ++ int64_t bitmap_length = DIV_ROUND_UP(s->bdev_length, s->granularity); + + /* Determine if the image is already zero, regardless of sync mode. */ ++ s->zero_bitmap = bitmap_new(bitmap_length); + bdrv_graph_co_rdlock(); + bs = s->mirror_top_bs->backing->bs; + if (s->target_is_zero) { +@@ -862,7 +904,14 @@ static int coroutine_fn GRAPH_UNLOCKED mirror_dirty_init(MirrorBlockJob *s) + if (ret < 0) { + return ret; + } else if (s->sync_mode == MIRROR_SYNC_MODE_TOP) { +- /* In TOP mode, there is no benefit to a pre-zeroing pass. */ ++ /* ++ * In TOP mode, there is no benefit to a pre-zeroing pass, but ++ * the zero bitmap can be set if the destination already reads ++ * as zero and we are not punching holes. ++ */ ++ if (ret > 0 && !punch_holes) { ++ bitmap_set(s->zero_bitmap, 0, bitmap_length); ++ } + } else if (ret == 0 || punch_holes) { + /* + * Here, we are in FULL mode; our goal is to avoid writing +@@ -871,8 +920,9 @@ static int coroutine_fn GRAPH_UNLOCKED mirror_dirty_init(MirrorBlockJob *s) + * zeroing happened externally (ret > 0) or if we have a fast + * way to pre-zero the image (the dirty bitmap will be + * populated later by the non-zero portions, the same as for +- * TOP mode). If pre-zeroing is not fast, or we need to punch +- * holes, then our only recourse is to write the entire image. ++ * TOP mode). If pre-zeroing is not fast, then our only ++ * recourse is to mark the entire image dirty. The act of ++ * pre-zeroing will populate the zero bitmap. + */ + if (!bdrv_can_write_zeroes_with_unmap(target_bs)) { + bdrv_set_dirty_bitmap(s->dirty_bitmap, 0, s->bdev_length); +@@ -883,6 +933,7 @@ static int coroutine_fn GRAPH_UNLOCKED mirror_dirty_init(MirrorBlockJob *s) + for (offset = 0; offset < s->bdev_length; ) { + int bytes = MIN(s->bdev_length - offset, + QEMU_ALIGN_DOWN(INT_MAX, s->granularity)); ++ bool ignored; + + mirror_throttle(s); + +@@ -898,12 +949,15 @@ static int coroutine_fn GRAPH_UNLOCKED mirror_dirty_init(MirrorBlockJob *s) + continue; + } + +- mirror_perform(s, offset, bytes, MIRROR_METHOD_ZERO); ++ mirror_perform(s, offset, bytes, MIRROR_METHOD_ZERO, &ignored); + offset += bytes; + } + + mirror_wait_for_all_io(s); + s->initial_zeroing_ongoing = false; ++ } else { ++ /* In FULL mode, and image already reads as zero. */ ++ bitmap_set(s->zero_bitmap, 0, bitmap_length); + } + + /* First part, loop on the sectors and initialize the dirty bitmap. */ +@@ -1188,6 +1242,7 @@ immediate_exit: + assert(s->in_flight == 0); + qemu_vfree(s->buf); + g_free(s->cow_bitmap); ++ g_free(s->zero_bitmap); + g_free(s->in_flight_bitmap); + bdrv_dirty_iter_free(s->dbi); + +@@ -1367,6 +1422,7 @@ do_sync_target_write(MirrorBlockJob *job, MirrorMethod method, + int ret; + size_t qiov_offset = 0; + int64_t dirty_bitmap_offset, dirty_bitmap_end; ++ int64_t zero_bitmap_offset, zero_bitmap_end; + + if (!QEMU_IS_ALIGNED(offset, job->granularity) && + bdrv_dirty_bitmap_get(job->dirty_bitmap, offset)) +@@ -1410,8 +1466,9 @@ do_sync_target_write(MirrorBlockJob *job, MirrorMethod method, + } + + /* +- * Tails are either clean or shrunk, so for bitmap resetting +- * we safely align the range down. ++ * Tails are either clean or shrunk, so for dirty bitmap resetting ++ * we safely align the range narrower. But for zero bitmap, round ++ * range wider for checking or clearing, and narrower for setting. + */ + dirty_bitmap_offset = QEMU_ALIGN_UP(offset, job->granularity); + dirty_bitmap_end = QEMU_ALIGN_DOWN(offset + bytes, job->granularity); +@@ -1419,22 +1476,44 @@ do_sync_target_write(MirrorBlockJob *job, MirrorMethod method, + bdrv_reset_dirty_bitmap(job->dirty_bitmap, dirty_bitmap_offset, + dirty_bitmap_end - dirty_bitmap_offset); + } ++ zero_bitmap_offset = offset / job->granularity; ++ zero_bitmap_end = DIV_ROUND_UP(offset + bytes, job->granularity); + + job_progress_increase_remaining(&job->common.job, bytes); + job->active_write_bytes_in_flight += bytes; + + switch (method) { + case MIRROR_METHOD_COPY: ++ if (job->zero_bitmap) { ++ bitmap_clear(job->zero_bitmap, zero_bitmap_offset, ++ zero_bitmap_end - zero_bitmap_offset); ++ } + ret = blk_co_pwritev_part(job->target, offset, bytes, + qiov, qiov_offset, flags); + break; + + case MIRROR_METHOD_ZERO: ++ if (job->zero_bitmap) { ++ if (find_next_zero_bit(job->zero_bitmap, zero_bitmap_end, ++ zero_bitmap_offset) == zero_bitmap_end) { ++ ret = 0; ++ break; ++ } ++ } + assert(!qiov); + ret = blk_co_pwrite_zeroes(job->target, offset, bytes, flags); ++ if (job->zero_bitmap && ret >= 0) { ++ bitmap_set(job->zero_bitmap, dirty_bitmap_offset / job->granularity, ++ (dirty_bitmap_end - dirty_bitmap_offset) / ++ job->granularity); ++ } + break; + + case MIRROR_METHOD_DISCARD: ++ if (job->zero_bitmap) { ++ bitmap_clear(job->zero_bitmap, zero_bitmap_offset, ++ zero_bitmap_end - zero_bitmap_offset); ++ } + assert(!qiov); + ret = blk_co_pdiscard(job->target, offset, bytes); + break; +-- +2.39.3 + diff --git a/kvm-tests-Add-iotest-mirror-sparse-for-recent-patches.patch b/kvm-tests-Add-iotest-mirror-sparse-for-recent-patches.patch new file mode 100644 index 0000000..70ab894 --- /dev/null +++ b/kvm-tests-Add-iotest-mirror-sparse-for-recent-patches.patch @@ -0,0 +1,545 @@ +From 2bb881df5b93f5534e5f0b91cf1ed3e0b524c2d3 Mon Sep 17 00:00:00 2001 +From: Eric Blake +Date: Fri, 9 May 2025 15:40:30 -0500 +Subject: [PATCH 13/14] tests: Add iotest mirror-sparse for recent patches + +RH-Author: Eric Blake +RH-MergeRequest: 363: blockdev-mirror: More efficient handling of sparse mirrors +RH-Jira: RHEL-88435 RHEL-88437 +RH-Acked-by: Stefan Hajnoczi +RH-Acked-by: Miroslav Rezanina +RH-Commit: [13/14] 474f12dfe9161c7e9f59cafde203e5183e2fc3f5 (ebblake/centos-qemu-kvm) + +Prove that blockdev-mirror can now result in sparse raw destination +files, regardless of whether the source is raw or qcow2. By making +this a separate test, it was possible to test effects of individual +patches for the various pieces that all have to work together for a +sparse mirror to be successful. + +Note that ./check -file produces different job lengths than ./check +-qcow2 (the test uses a filter to normalize); that's because when +deciding how much of the image to be mirrored, the code looks at how +much of the source image was allocated (for qcow2, this is only the +written clusters; for raw, it is the entire file). But the important +part is that the destination file ends up smaller than 3M, rather than +the 20M it used to be before this patch series. + +Signed-off-by: Eric Blake +Message-ID: <20250509204341.3553601-28-eblake@redhat.com> +Reviewed-by: Stefan Hajnoczi +(cherry picked from commit c0ddcb2cbc146e64f666eaae4edc7b5db7e5814d) +Jira: https://issues.redhat.com/browse/RHEL-88435 +Jira: https://issues.redhat.com/browse/RHEL-88437 +Signed-off-by: Eric Blake +--- + tests/qemu-iotests/tests/mirror-sparse | 125 +++++++ + tests/qemu-iotests/tests/mirror-sparse.out | 365 +++++++++++++++++++++ + 2 files changed, 490 insertions(+) + create mode 100755 tests/qemu-iotests/tests/mirror-sparse + create mode 100644 tests/qemu-iotests/tests/mirror-sparse.out + +diff --git a/tests/qemu-iotests/tests/mirror-sparse b/tests/qemu-iotests/tests/mirror-sparse +new file mode 100755 +index 0000000000..8c52a4e244 +--- /dev/null ++++ b/tests/qemu-iotests/tests/mirror-sparse +@@ -0,0 +1,125 @@ ++#!/usr/bin/env bash ++# group: rw auto quick ++# ++# Test blockdev-mirror with raw sparse destination ++# ++# Copyright (C) 2025 Red Hat, Inc. ++# ++# This program is free software; you can redistribute it and/or modify ++# it under the terms of the GNU General Public License as published by ++# the Free Software Foundation; either version 2 of the License, or ++# (at your option) any later version. ++# ++# This program is distributed in the hope that it will be useful, ++# but WITHOUT ANY WARRANTY; without even the implied warranty of ++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++# GNU General Public License for more details. ++# ++# You should have received a copy of the GNU General Public License ++# along with this program. If not, see . ++# ++ ++seq="$(basename $0)" ++echo "QA output created by $seq" ++ ++status=1 # failure is the default! ++ ++_cleanup() ++{ ++ _cleanup_test_img ++ _cleanup_qemu ++} ++trap "_cleanup; exit \$status" 0 1 2 3 15 ++ ++# get standard environment, filters and checks ++cd .. ++. ./common.rc ++. ./common.filter ++. ./common.qemu ++ ++_supported_fmt qcow2 raw # Format of the source. dst is always raw file ++_supported_proto file ++_supported_os Linux ++ ++echo ++echo "=== Initial image setup ===" ++echo ++ ++TEST_IMG="$TEST_IMG.base" _make_test_img 20M ++$QEMU_IO -c 'w 8M 2M' -f $IMGFMT "$TEST_IMG.base" | _filter_qemu_io ++ ++_launch_qemu \ ++ -blockdev '{"driver":"file", "cache":{"direct":true, "no-flush":false}, ++ "filename":"'"$TEST_IMG.base"'", "node-name":"src-file"}' \ ++ -blockdev '{"driver":"'$IMGFMT'", "node-name":"src", "file":"src-file"}' ++h1=$QEMU_HANDLE ++_send_qemu_cmd $h1 '{"execute": "qmp_capabilities"}' 'return' ++ ++# Check several combinations; most should result in a sparse destination; ++# the destination should only be fully allocated if pre-allocated ++# and not punching holes due to detect-zeroes ++# do_test creation discard zeroes result ++do_test() { ++ creation=$1 ++ discard=$2 ++ zeroes=$3 ++ expected=$4 ++ ++echo ++echo "=== Testing creation=$creation discard=$discard zeroes=$zeroes ===" ++echo ++ ++rm -f $TEST_IMG ++if test $creation = external; then ++ truncate --size=20M $TEST_IMG ++else ++ _send_qemu_cmd $h1 '{"execute": "blockdev-create", "arguments": ++ {"options": {"driver":"file", "filename":"'$TEST_IMG'", ++ "size":'$((20*1024*1024))', "preallocation":"'$creation'"}, ++ "job-id":"job1"}}' 'concluded' ++ _send_qemu_cmd $h1 '{"execute": "job-dismiss", "arguments": ++ {"id": "job1"}}' 'return' ++fi ++_send_qemu_cmd $h1 '{"execute": "blockdev-add", "arguments": ++ {"node-name": "dst", "driver":"file", ++ "filename":"'$TEST_IMG'", "aio":"threads", ++ "auto-read-only":true, "discard":"'$discard'", ++ "detect-zeroes":"'$zeroes'"}}' 'return' ++_send_qemu_cmd $h1 '{"execute":"blockdev-mirror", "arguments": ++ {"sync":"full", "device":"src", "target":"dst", ++ "job-id":"job2"}}' 'return' ++_timed_wait_for $h1 '"ready"' ++_send_qemu_cmd $h1 '{"execute": "job-complete", "arguments": ++ {"id":"job2"}}' 'return' \ ++ | _filter_block_job_offset | _filter_block_job_len ++_send_qemu_cmd $h1 '{"execute": "blockdev-del", "arguments": ++ {"node-name": "dst"}}' 'return' \ ++ | _filter_block_job_offset | _filter_block_job_len ++$QEMU_IMG compare -U -f $IMGFMT -F raw $TEST_IMG.base $TEST_IMG ++result=$(disk_usage $TEST_IMG) ++if test $result -lt $((3*1024*1024)); then ++ actual=sparse ++elif test $result = $((20*1024*1024)); then ++ actual=full ++else ++ actual=unknown ++fi ++echo "Destination is $actual; expected $expected" ++} ++ ++do_test external ignore off sparse ++do_test external unmap off sparse ++do_test external unmap unmap sparse ++do_test off ignore off sparse ++do_test off unmap off sparse ++do_test off unmap unmap sparse ++do_test full ignore off full ++do_test full unmap off sparse ++do_test full unmap unmap sparse ++ ++_send_qemu_cmd $h1 '{"execute":"quit"}' '' ++ ++# success, all done ++echo '*** done' ++rm -f $seq.full ++status=0 +diff --git a/tests/qemu-iotests/tests/mirror-sparse.out b/tests/qemu-iotests/tests/mirror-sparse.out +new file mode 100644 +index 0000000000..2103b891c3 +--- /dev/null ++++ b/tests/qemu-iotests/tests/mirror-sparse.out +@@ -0,0 +1,365 @@ ++QA output created by mirror-sparse ++ ++=== Initial image setup === ++ ++Formatting 'TEST_DIR/t.IMGFMT.base', fmt=IMGFMT size=20971520 ++wrote 2097152/2097152 bytes at offset 8388608 ++2 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec) ++{"execute": "qmp_capabilities"} ++{"return": {}} ++ ++=== Testing creation=external discard=ignore zeroes=off === ++ ++{"execute": "blockdev-add", "arguments": ++ {"node-name": "dst", "driver":"file", ++ "filename":"TEST_DIR/t.IMGFMT", "aio":"threads", ++ "auto-read-only":true, "discard":"ignore", ++ "detect-zeroes":"off"}} ++{"return": {}} ++{"execute":"blockdev-mirror", "arguments": ++ {"sync":"full", "device":"src", "target":"dst", ++ "job-id":"job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "created", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "running", "id": "job2"}} ++{"return": {}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "ready", "id": "job2"}} ++{"execute": "job-complete", "arguments": ++ {"id":"job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "BLOCK_JOB_READY", "data": {"device": "job2", "len": LEN, "offset": OFFSET, "speed": 0, "type": "mirror"}} ++{"return": {}} ++{"execute": "blockdev-del", "arguments": ++ {"node-name": "dst"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "waiting", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "pending", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "BLOCK_JOB_COMPLETED", "data": {"device": "job2", "len": LEN, "offset": OFFSET, "speed": 0, "type": "mirror"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "concluded", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "null", "id": "job2"}} ++{"return": {}} ++Images are identical. ++Destination is sparse; expected sparse ++ ++=== Testing creation=external discard=unmap zeroes=off === ++ ++{"execute": "blockdev-add", "arguments": ++ {"node-name": "dst", "driver":"file", ++ "filename":"TEST_DIR/t.IMGFMT", "aio":"threads", ++ "auto-read-only":true, "discard":"unmap", ++ "detect-zeroes":"off"}} ++{"return": {}} ++{"execute":"blockdev-mirror", "arguments": ++ {"sync":"full", "device":"src", "target":"dst", ++ "job-id":"job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "created", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "running", "id": "job2"}} ++{"return": {}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "ready", "id": "job2"}} ++{"execute": "job-complete", "arguments": ++ {"id":"job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "BLOCK_JOB_READY", "data": {"device": "job2", "len": LEN, "offset": OFFSET, "speed": 0, "type": "mirror"}} ++{"return": {}} ++{"execute": "blockdev-del", "arguments": ++ {"node-name": "dst"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "waiting", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "pending", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "BLOCK_JOB_COMPLETED", "data": {"device": "job2", "len": LEN, "offset": OFFSET, "speed": 0, "type": "mirror"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "concluded", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "null", "id": "job2"}} ++{"return": {}} ++Images are identical. ++Destination is sparse; expected sparse ++ ++=== Testing creation=external discard=unmap zeroes=unmap === ++ ++{"execute": "blockdev-add", "arguments": ++ {"node-name": "dst", "driver":"file", ++ "filename":"TEST_DIR/t.IMGFMT", "aio":"threads", ++ "auto-read-only":true, "discard":"unmap", ++ "detect-zeroes":"unmap"}} ++{"return": {}} ++{"execute":"blockdev-mirror", "arguments": ++ {"sync":"full", "device":"src", "target":"dst", ++ "job-id":"job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "created", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "running", "id": "job2"}} ++{"return": {}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "ready", "id": "job2"}} ++{"execute": "job-complete", "arguments": ++ {"id":"job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "BLOCK_JOB_READY", "data": {"device": "job2", "len": LEN, "offset": OFFSET, "speed": 0, "type": "mirror"}} ++{"return": {}} ++{"execute": "blockdev-del", "arguments": ++ {"node-name": "dst"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "waiting", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "pending", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "BLOCK_JOB_COMPLETED", "data": {"device": "job2", "len": LEN, "offset": OFFSET, "speed": 0, "type": "mirror"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "concluded", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "null", "id": "job2"}} ++{"return": {}} ++Images are identical. ++Destination is sparse; expected sparse ++ ++=== Testing creation=off discard=ignore zeroes=off === ++ ++{"execute": "blockdev-create", "arguments": ++ {"options": {"driver":"file", "filename":"TEST_DIR/t.IMGFMT", ++ "size":20971520, "preallocation":"off"}, ++ "job-id":"job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "created", "id": "job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "running", "id": "job1"}} ++{"return": {}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "waiting", "id": "job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "pending", "id": "job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "concluded", "id": "job1"}} ++{"execute": "job-dismiss", "arguments": ++ {"id": "job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "null", "id": "job1"}} ++{"return": {}} ++{"execute": "blockdev-add", "arguments": ++ {"node-name": "dst", "driver":"file", ++ "filename":"TEST_DIR/t.IMGFMT", "aio":"threads", ++ "auto-read-only":true, "discard":"ignore", ++ "detect-zeroes":"off"}} ++{"return": {}} ++{"execute":"blockdev-mirror", "arguments": ++ {"sync":"full", "device":"src", "target":"dst", ++ "job-id":"job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "created", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "running", "id": "job2"}} ++{"return": {}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "ready", "id": "job2"}} ++{"execute": "job-complete", "arguments": ++ {"id":"job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "BLOCK_JOB_READY", "data": {"device": "job2", "len": LEN, "offset": OFFSET, "speed": 0, "type": "mirror"}} ++{"return": {}} ++{"execute": "blockdev-del", "arguments": ++ {"node-name": "dst"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "waiting", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "pending", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "BLOCK_JOB_COMPLETED", "data": {"device": "job2", "len": LEN, "offset": OFFSET, "speed": 0, "type": "mirror"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "concluded", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "null", "id": "job2"}} ++{"return": {}} ++Images are identical. ++Destination is sparse; expected sparse ++ ++=== Testing creation=off discard=unmap zeroes=off === ++ ++{"execute": "blockdev-create", "arguments": ++ {"options": {"driver":"file", "filename":"TEST_DIR/t.IMGFMT", ++ "size":20971520, "preallocation":"off"}, ++ "job-id":"job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "created", "id": "job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "running", "id": "job1"}} ++{"return": {}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "waiting", "id": "job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "pending", "id": "job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "concluded", "id": "job1"}} ++{"execute": "job-dismiss", "arguments": ++ {"id": "job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "null", "id": "job1"}} ++{"return": {}} ++{"execute": "blockdev-add", "arguments": ++ {"node-name": "dst", "driver":"file", ++ "filename":"TEST_DIR/t.IMGFMT", "aio":"threads", ++ "auto-read-only":true, "discard":"unmap", ++ "detect-zeroes":"off"}} ++{"return": {}} ++{"execute":"blockdev-mirror", "arguments": ++ {"sync":"full", "device":"src", "target":"dst", ++ "job-id":"job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "created", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "running", "id": "job2"}} ++{"return": {}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "ready", "id": "job2"}} ++{"execute": "job-complete", "arguments": ++ {"id":"job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "BLOCK_JOB_READY", "data": {"device": "job2", "len": LEN, "offset": OFFSET, "speed": 0, "type": "mirror"}} ++{"return": {}} ++{"execute": "blockdev-del", "arguments": ++ {"node-name": "dst"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "waiting", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "pending", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "BLOCK_JOB_COMPLETED", "data": {"device": "job2", "len": LEN, "offset": OFFSET, "speed": 0, "type": "mirror"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "concluded", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "null", "id": "job2"}} ++{"return": {}} ++Images are identical. ++Destination is sparse; expected sparse ++ ++=== Testing creation=off discard=unmap zeroes=unmap === ++ ++{"execute": "blockdev-create", "arguments": ++ {"options": {"driver":"file", "filename":"TEST_DIR/t.IMGFMT", ++ "size":20971520, "preallocation":"off"}, ++ "job-id":"job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "created", "id": "job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "running", "id": "job1"}} ++{"return": {}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "waiting", "id": "job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "pending", "id": "job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "concluded", "id": "job1"}} ++{"execute": "job-dismiss", "arguments": ++ {"id": "job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "null", "id": "job1"}} ++{"return": {}} ++{"execute": "blockdev-add", "arguments": ++ {"node-name": "dst", "driver":"file", ++ "filename":"TEST_DIR/t.IMGFMT", "aio":"threads", ++ "auto-read-only":true, "discard":"unmap", ++ "detect-zeroes":"unmap"}} ++{"return": {}} ++{"execute":"blockdev-mirror", "arguments": ++ {"sync":"full", "device":"src", "target":"dst", ++ "job-id":"job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "created", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "running", "id": "job2"}} ++{"return": {}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "ready", "id": "job2"}} ++{"execute": "job-complete", "arguments": ++ {"id":"job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "BLOCK_JOB_READY", "data": {"device": "job2", "len": LEN, "offset": OFFSET, "speed": 0, "type": "mirror"}} ++{"return": {}} ++{"execute": "blockdev-del", "arguments": ++ {"node-name": "dst"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "waiting", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "pending", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "BLOCK_JOB_COMPLETED", "data": {"device": "job2", "len": LEN, "offset": OFFSET, "speed": 0, "type": "mirror"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "concluded", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "null", "id": "job2"}} ++{"return": {}} ++Images are identical. ++Destination is sparse; expected sparse ++ ++=== Testing creation=full discard=ignore zeroes=off === ++ ++{"execute": "blockdev-create", "arguments": ++ {"options": {"driver":"file", "filename":"TEST_DIR/t.IMGFMT", ++ "size":20971520, "preallocation":"full"}, ++ "job-id":"job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "created", "id": "job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "running", "id": "job1"}} ++{"return": {}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "waiting", "id": "job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "pending", "id": "job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "concluded", "id": "job1"}} ++{"execute": "job-dismiss", "arguments": ++ {"id": "job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "null", "id": "job1"}} ++{"return": {}} ++{"execute": "blockdev-add", "arguments": ++ {"node-name": "dst", "driver":"file", ++ "filename":"TEST_DIR/t.IMGFMT", "aio":"threads", ++ "auto-read-only":true, "discard":"ignore", ++ "detect-zeroes":"off"}} ++{"return": {}} ++{"execute":"blockdev-mirror", "arguments": ++ {"sync":"full", "device":"src", "target":"dst", ++ "job-id":"job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "created", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "running", "id": "job2"}} ++{"return": {}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "ready", "id": "job2"}} ++{"execute": "job-complete", "arguments": ++ {"id":"job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "BLOCK_JOB_READY", "data": {"device": "job2", "len": LEN, "offset": OFFSET, "speed": 0, "type": "mirror"}} ++{"return": {}} ++{"execute": "blockdev-del", "arguments": ++ {"node-name": "dst"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "waiting", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "pending", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "BLOCK_JOB_COMPLETED", "data": {"device": "job2", "len": LEN, "offset": OFFSET, "speed": 0, "type": "mirror"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "concluded", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "null", "id": "job2"}} ++{"return": {}} ++Images are identical. ++Destination is full; expected full ++ ++=== Testing creation=full discard=unmap zeroes=off === ++ ++{"execute": "blockdev-create", "arguments": ++ {"options": {"driver":"file", "filename":"TEST_DIR/t.IMGFMT", ++ "size":20971520, "preallocation":"full"}, ++ "job-id":"job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "created", "id": "job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "running", "id": "job1"}} ++{"return": {}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "waiting", "id": "job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "pending", "id": "job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "concluded", "id": "job1"}} ++{"execute": "job-dismiss", "arguments": ++ {"id": "job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "null", "id": "job1"}} ++{"return": {}} ++{"execute": "blockdev-add", "arguments": ++ {"node-name": "dst", "driver":"file", ++ "filename":"TEST_DIR/t.IMGFMT", "aio":"threads", ++ "auto-read-only":true, "discard":"unmap", ++ "detect-zeroes":"off"}} ++{"return": {}} ++{"execute":"blockdev-mirror", "arguments": ++ {"sync":"full", "device":"src", "target":"dst", ++ "job-id":"job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "created", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "running", "id": "job2"}} ++{"return": {}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "ready", "id": "job2"}} ++{"execute": "job-complete", "arguments": ++ {"id":"job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "BLOCK_JOB_READY", "data": {"device": "job2", "len": LEN, "offset": OFFSET, "speed": 0, "type": "mirror"}} ++{"return": {}} ++{"execute": "blockdev-del", "arguments": ++ {"node-name": "dst"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "waiting", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "pending", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "BLOCK_JOB_COMPLETED", "data": {"device": "job2", "len": LEN, "offset": OFFSET, "speed": 0, "type": "mirror"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "concluded", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "null", "id": "job2"}} ++{"return": {}} ++Images are identical. ++Destination is sparse; expected sparse ++ ++=== Testing creation=full discard=unmap zeroes=unmap === ++ ++{"execute": "blockdev-create", "arguments": ++ {"options": {"driver":"file", "filename":"TEST_DIR/t.IMGFMT", ++ "size":20971520, "preallocation":"full"}, ++ "job-id":"job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "created", "id": "job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "running", "id": "job1"}} ++{"return": {}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "waiting", "id": "job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "pending", "id": "job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "concluded", "id": "job1"}} ++{"execute": "job-dismiss", "arguments": ++ {"id": "job1"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "null", "id": "job1"}} ++{"return": {}} ++{"execute": "blockdev-add", "arguments": ++ {"node-name": "dst", "driver":"file", ++ "filename":"TEST_DIR/t.IMGFMT", "aio":"threads", ++ "auto-read-only":true, "discard":"unmap", ++ "detect-zeroes":"unmap"}} ++{"return": {}} ++{"execute":"blockdev-mirror", "arguments": ++ {"sync":"full", "device":"src", "target":"dst", ++ "job-id":"job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "created", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "running", "id": "job2"}} ++{"return": {}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "ready", "id": "job2"}} ++{"execute": "job-complete", "arguments": ++ {"id":"job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "BLOCK_JOB_READY", "data": {"device": "job2", "len": LEN, "offset": OFFSET, "speed": 0, "type": "mirror"}} ++{"return": {}} ++{"execute": "blockdev-del", "arguments": ++ {"node-name": "dst"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "waiting", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "pending", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "BLOCK_JOB_COMPLETED", "data": {"device": "job2", "len": LEN, "offset": OFFSET, "speed": 0, "type": "mirror"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "concluded", "id": "job2"}} ++{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "JOB_STATUS_CHANGE", "data": {"status": "null", "id": "job2"}} ++{"return": {}} ++Images are identical. ++Destination is sparse; expected sparse ++{"execute":"quit"} ++*** done +-- +2.39.3 + diff --git a/qemu-kvm.spec b/qemu-kvm.spec index f5e5b4b..a29cba7 100644 --- a/qemu-kvm.spec +++ b/qemu-kvm.spec @@ -147,7 +147,7 @@ Obsoletes: %{name}-block-ssh <= %{epoch}:%{version} \ Summary: QEMU is a machine emulator and virtualizer Name: qemu-kvm Version: 10.0.0 -Release: 3%{?rcrel}%{?dist}%{?cc_suffix} +Release: 4%{?rcrel}%{?dist}%{?cc_suffix} # Epoch because we pushed a qemu-1.0 package. AIUI this can't ever be dropped # Epoch 15 used for RHEL 8 # Epoch 17 used for RHEL 9 (due to release versioning offset in RHEL 8.5) @@ -202,6 +202,48 @@ Patch24: kvm-migration-postcopy-Spatial-locality-page-hint-for-pr.patch Patch25: kvm-meson-configure-add-valgrind-option-en-dis-able-valg.patch # Fixing s390x build issues Patch26: kvm-docs-Don-t-define-duplicate-label-in-qemu-block-driv.patch +# For RHEL-88435 - --migrate-disks-detect-zeroes doesn't take effect for disk migration [rhel-10.1] +# For RHEL-88437 - Disk size of target raw image is full allocated when doing mirror with default discard value [rhel-10.1] +Patch27: kvm-block-Expand-block-status-mode-from-bool-to-flags.patch +# For RHEL-88435 - --migrate-disks-detect-zeroes doesn't take effect for disk migration [rhel-10.1] +# For RHEL-88437 - Disk size of target raw image is full allocated when doing mirror with default discard value [rhel-10.1] +Patch28: kvm-file-posix-gluster-Handle-zero-block-status-hint-bet.patch +# For RHEL-88435 - --migrate-disks-detect-zeroes doesn't take effect for disk migration [rhel-10.1] +# For RHEL-88437 - Disk size of target raw image is full allocated when doing mirror with default discard value [rhel-10.1] +Patch29: kvm-block-Let-bdrv_co_is_zero_fast-consolidate-adjacent-.patch +# For RHEL-88435 - --migrate-disks-detect-zeroes doesn't take effect for disk migration [rhel-10.1] +# For RHEL-88437 - Disk size of target raw image is full allocated when doing mirror with default discard value [rhel-10.1] +Patch30: kvm-block-Add-new-bdrv_co_is_all_zeroes-function.patch +# For RHEL-88435 - --migrate-disks-detect-zeroes doesn't take effect for disk migration [rhel-10.1] +# For RHEL-88437 - Disk size of target raw image is full allocated when doing mirror with default discard value [rhel-10.1] +Patch31: kvm-iotests-Improve-iotest-194-to-mirror-data.patch +# For RHEL-88435 - --migrate-disks-detect-zeroes doesn't take effect for disk migration [rhel-10.1] +# For RHEL-88437 - Disk size of target raw image is full allocated when doing mirror with default discard value [rhel-10.1] +Patch32: kvm-mirror-Minor-refactoring.patch +# For RHEL-88435 - --migrate-disks-detect-zeroes doesn't take effect for disk migration [rhel-10.1] +# For RHEL-88437 - Disk size of target raw image is full allocated when doing mirror with default discard value [rhel-10.1] +Patch33: kvm-mirror-Pass-full-sync-mode-rather-than-bool-to-inter.patch +# For RHEL-88435 - --migrate-disks-detect-zeroes doesn't take effect for disk migration [rhel-10.1] +# For RHEL-88437 - Disk size of target raw image is full allocated when doing mirror with default discard value [rhel-10.1] +Patch34: kvm-mirror-Allow-QMP-override-to-declare-target-already-.patch +# For RHEL-88435 - --migrate-disks-detect-zeroes doesn't take effect for disk migration [rhel-10.1] +# For RHEL-88437 - Disk size of target raw image is full allocated when doing mirror with default discard value [rhel-10.1] +Patch35: kvm-mirror-Drop-redundant-zero_target-parameter.patch +# For RHEL-88435 - --migrate-disks-detect-zeroes doesn't take effect for disk migration [rhel-10.1] +# For RHEL-88437 - Disk size of target raw image is full allocated when doing mirror with default discard value [rhel-10.1] +Patch36: kvm-mirror-Skip-pre-zeroing-destination-if-it-is-already.patch +# For RHEL-88435 - --migrate-disks-detect-zeroes doesn't take effect for disk migration [rhel-10.1] +# For RHEL-88437 - Disk size of target raw image is full allocated when doing mirror with default discard value [rhel-10.1] +Patch37: kvm-mirror-Skip-writing-zeroes-when-target-is-already-ze.patch +# For RHEL-88435 - --migrate-disks-detect-zeroes doesn't take effect for disk migration [rhel-10.1] +# For RHEL-88437 - Disk size of target raw image is full allocated when doing mirror with default discard value [rhel-10.1] +Patch38: kvm-iotests-common.rc-add-disk_usage-function.patch +# For RHEL-88435 - --migrate-disks-detect-zeroes doesn't take effect for disk migration [rhel-10.1] +# For RHEL-88437 - Disk size of target raw image is full allocated when doing mirror with default discard value [rhel-10.1] +Patch39: kvm-tests-Add-iotest-mirror-sparse-for-recent-patches.patch +# For RHEL-88435 - --migrate-disks-detect-zeroes doesn't take effect for disk migration [rhel-10.1] +# For RHEL-88437 - Disk size of target raw image is full allocated when doing mirror with default discard value [rhel-10.1] +Patch40: kvm-mirror-Reduce-I-O-when-destination-is-detect-zeroes-.patch %if %{have_clang} BuildRequires: clang @@ -1284,6 +1326,26 @@ useradd -r -u 107 -g qemu -G kvm -d / -s /sbin/nologin \ %endif %changelog +* Mon May 26 2025 Miroslav Rezanina - 10.0.0-4 +- kvm-block-Expand-block-status-mode-from-bool-to-flags.patch [RHEL-88435 RHEL-88437] +- kvm-file-posix-gluster-Handle-zero-block-status-hint-bet.patch [RHEL-88435 RHEL-88437] +- kvm-block-Let-bdrv_co_is_zero_fast-consolidate-adjacent-.patch [RHEL-88435 RHEL-88437] +- kvm-block-Add-new-bdrv_co_is_all_zeroes-function.patch [RHEL-88435 RHEL-88437] +- kvm-iotests-Improve-iotest-194-to-mirror-data.patch [RHEL-88435 RHEL-88437] +- kvm-mirror-Minor-refactoring.patch [RHEL-88435 RHEL-88437] +- kvm-mirror-Pass-full-sync-mode-rather-than-bool-to-inter.patch [RHEL-88435 RHEL-88437] +- kvm-mirror-Allow-QMP-override-to-declare-target-already-.patch [RHEL-88435 RHEL-88437] +- kvm-mirror-Drop-redundant-zero_target-parameter.patch [RHEL-88435 RHEL-88437] +- kvm-mirror-Skip-pre-zeroing-destination-if-it-is-already.patch [RHEL-88435 RHEL-88437] +- kvm-mirror-Skip-writing-zeroes-when-target-is-already-ze.patch [RHEL-88435 RHEL-88437] +- kvm-iotests-common.rc-add-disk_usage-function.patch [RHEL-88435 RHEL-88437] +- kvm-tests-Add-iotest-mirror-sparse-for-recent-patches.patch [RHEL-88435 RHEL-88437] +- kvm-mirror-Reduce-I-O-when-destination-is-detect-zeroes-.patch [RHEL-88435 RHEL-88437] +- Resolves: RHEL-88435 + (--migrate-disks-detect-zeroes doesn't take effect for disk migration [rhel-10.1]) +- Resolves: RHEL-88437 + (Disk size of target raw image is full allocated when doing mirror with default discard value [rhel-10.1]) + * Mon May 19 2025 Miroslav Rezanina - 10.0.0-3 - kvm-migration-postcopy-Spatial-locality-page-hint-for-pr.patch [RHEL-85635] - kvm-meson-configure-add-valgrind-option-en-dis-able-valg.patch [RHEL-88457]