[Devel] [PATCH hci-8.1] drivers/md/dm-qcow2: truncate unused tail of image on suspend

Denis V. Lunev den at virtuozzo.com
Tue Oct 6 17:45:57 MSK 2026


On 10/6/26 16:44, Alexander Ivanov wrote:
> On postsuspend we truncate the image file only down to
> file_preallocated_area_start, i.e. we drop preallocations only.
> Clusters at the end of the file freed by discard (refcount dropped
> to zero) are never given back, so the file size never shrinks.
>
> Add qcow2_calc_used_end(), which scans refcount blocks backward from
> the end of the file and returns the end of the last cluster with
> non-zero refcount. Refcount ranges with no allocated refblock are
> skipped at once. Metadata pages are taken from md cache or read
> synchronously. This is called on suspended device after md
> writeback, so R1 and R2 can't change under us.
>
> Truncate the file to min(file_preallocated_area_start, used end).
> Skip the scan if md writeback failed, as refcounts on disk are stale
> then, and fall back to the old behaviour on scan error.
>
> https://virtuozzo.atlassian.net/browse/VSTOR-148171
> Feature: dm-qcow2: block device over QCOW2 files driver
>
> Signed-off-by: Alexander Ivanov <alexander.ivanov at virtuozzo.com>
> ---
>  drivers/md/dm-qcow2-map.c    | 77 ++++++++++++++++++++++++++++++++++++
>  drivers/md/dm-qcow2-target.c | 12 +++++-
>  drivers/md/dm-qcow2.h        |  1 +
>  3 files changed, 89 insertions(+), 1 deletion(-)
>
> diff --git a/drivers/md/dm-qcow2-map.c b/drivers/md/dm-qcow2-map.c
> index 78ee7cf5e881..39d465343d8b 100644
> --- a/drivers/md/dm-qcow2-map.c
> +++ b/drivers/md/dm-qcow2-map.c
> @@ -2208,6 +2208,83 @@ int qcow2_truncate_safe(struct file *file, loff_t new_len)
>  	return do_ftruncate(file, new_len, 0);
>  }
>  
> +static int get_md_page_sync(struct qcow2 *qcow2, u64 id, struct md_page **md)
> +{
> +	int ret;
> +
> +	spin_lock_irq(&qcow2->md_pages_lock);
> +	*md = qcow2_md_page_find_or_postpone(qcow2, id, NULL);
> +	spin_unlock_irq(&qcow2->md_pages_lock);
> +	if (*md)
> +		return 0;
> +
> +	ret = qcow2_alloc_and_insert_md_page(qcow2, id, md);
> +	if (ret)
> +		return ret;
> +
> +	ret = qcow2_rw_page_sync(READ, qcow2, id, (*md)->page);
> +	spin_lock_irq(&qcow2->md_pages_lock);
> +	if (ret)
> +		qcow2_md_page_erase(qcow2, *md);
> +	else
> +		(*md)->status |= MD_UPTODATE;
> +	spin_unlock_irq(&qcow2->md_pages_lock);
> +	if (ret)
> +		qcow2_free_md_page(*md);
> +	return ret;
> +}
> +
> +/*
> + * Returns the end of the last cluster, which is used according
> + * to refcount table. Clusters freed by discard may leave the tail
> + * of file unused. This must be called on suspended device after
> + * md writeback, so nobody changes R1 and R2.
> + */
> +loff_t qcow2_calc_used_end(struct qcow2 *qcow2)
> +{
> +	u32 clu_size = qcow2->clu_size;
> +	struct qcow2_map_item r1, r2;
> +	struct md_page *r1_md = NULL;
> +	loff_t pos;
> +	s32 i;
> +	int ret;
> +
> +	pos = round_down(qcow2->file_size - 1, clu_size);
> +	while (pos >= 0) {
> +		if (calc_refcounters_map(qcow2, pos, &r1, &r2) < 0)
> +			return -EIO;
> +
> +		if (!r1_md || r1_md->id != r1.page_id) {
> +			ret = get_md_page_sync(qcow2, r1.page_id, &r1_md);
> +			if (ret)
> +				return ret;
> +		}
> +		r1.md = r1_md;
> +
> +		ret = calc_r2_page_id(qcow2, &r1, &r2);
> +		if (ret == -ENOENT) {
> +			/* No R2 cluster, so all clusters it covers are unused */
> +			pos = (loff_t)r1.index * qcow2->refblock_entries * clu_size;
> +			pos -= clu_size;
> +			continue;
> +		}
> +
> +		if (ret < 0)
> +			return ret;
> +
> +		ret = get_md_page_sync(qcow2, r2.page_id, &r2.md);
> +		if (ret)
> +			return ret;
> +
> +		for (i = r2.index_in_page; i >= 0; i--, pos -= clu_size) {
> +			if (get_r2_entry(qcow2, r2.md, i))
> +				return pos + clu_size;
> +		}
> +	}
> +
> +	return -EIO;
> +}
> +
>  static int truncate_prealloc_safe(struct qcow2 *qcow2, loff_t len, const char *func)
>  {
>  	loff_t prealloc_len, max_prealloc_len = qcow2->reftable_max_file_size;
> diff --git a/drivers/md/dm-qcow2-target.c b/drivers/md/dm-qcow2-target.c
> index be09615eb8e2..95c8b114d92f 100644
> --- a/drivers/md/dm-qcow2-target.c
> +++ b/drivers/md/dm-qcow2-target.c
> @@ -1016,11 +1016,21 @@ static void qcow2_truncate_preallocations(struct dm_target *ti)
>  {
>  	struct qcow2_target *tgt = to_qcow2_target(ti);
>  	struct qcow2 *qcow2 = top_qcow2_protected(ti);
> -	loff_t end = qcow2->file_preallocated_area_start;
> +	loff_t used_end, end = qcow2->file_preallocated_area_start;
>  	int ret;
>  
>  	if (!(dm_table_get_mode(ti->table) & FMODE_WRITE))
>  		return;
> +
> +	/* Refcounts on disk are stale after failed md writeback */
> +	if (!tgt->md_writeback_error) {
> +		used_end = qcow2_calc_used_end(qcow2);
> +		if (used_end < 0)
> +			QC_ERR(ti, "dm-qcow2: Can't find used end: %lld", used_end);
> +		else if (used_end < end)
> +			end = used_end;
> +	}
> +
>  	if (end == qcow2->file_size)
>  		return;
>  
> diff --git a/drivers/md/dm-qcow2.h b/drivers/md/dm-qcow2.h
> index d9b8c38e093f..2c5eb7578316 100644
> --- a/drivers/md/dm-qcow2.h
> +++ b/drivers/md/dm-qcow2.h
> @@ -409,6 +409,7 @@ void qcow2_ref_dec(struct qcow2_target *tgt, u8 ref_index);
>  int qcow2_inflight_ref_switch(struct qcow2_target *tgt);
>  void qcow2_flush_deferred_activity(struct qcow2_target *tgt, struct qcow2 *qcow2);
>  int qcow2_truncate_safe(struct file *file, loff_t new_len);
> +loff_t qcow2_calc_used_end(struct qcow2 *qcow2);
>  
>  void qcow2_merge_backward_work(struct work_struct *work);
>  void qcow2_merge_backward_cancel(struct qcow2_target *tgt);
ploop?


More information about the Devel mailing list