* [Devel] [PATCH VZ10 1/6] drivers/md/dm-qcow2: fix lower delta seek window clamp
2026-08-10 12:29 [Devel] [PATCH VZ10 0/6] dm-qcow: lseek improvements Andrey Zhadchenko
@ 2026-08-10 12:29 ` Andrey Zhadchenko
2026-08-10 12:29 ` [Devel] [PATCH VZ10 2/6] drivers/md/dm-qcow2: keep seek parse window within limit Andrey Zhadchenko
` (5 subsequent siblings)
6 siblings, 0 replies; 16+ messages in thread
From: Andrey Zhadchenko @ 2026-08-10 12:29 UTC (permalink / raw)
size may be smaller than old_qio->bi_iter.bi_size: we calculate it
beforehand with calc_front_qio_bytes().
https://virtuozzo.atlassian.net/browse/VSTOR-139407
Feature: dm-qcow2: block device over QCOW2 files driver
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
drivers/md/dm-qcow2-map.c | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/drivers/md/dm-qcow2-map.c b/drivers/md/dm-qcow2-map.c
index db21efb45e17a..542c7949321e5 100644
--- a/drivers/md/dm-qcow2-map.c
+++ b/drivers/md/dm-qcow2-map.c
@@ -4437,12 +4437,11 @@ static inline void seek_qio_next_clu(struct qio *qio, struct qcow2_map *map)
static struct qio *advance_and_spawn_lower_seek_qio(struct qio *old_qio, u32 size)
{
struct qio *new_qio;
- loff_t start, old_end;
+ loff_t start;
start = to_bytes(old_qio->bi_iter.bi_sector);
- old_end = start + old_qio->bi_iter.bi_size;
- if (old_end > old_qio->qcow2->lower->hdr.size)
+ if (start + size > old_qio->qcow2->lower->hdr.size)
size = old_qio->qcow2->lower->hdr.size - start;
new_qio = alloc_seek_qio(old_qio->qcow2->lower, old_qio, start + size);
--
2.43.5
^ permalink raw reply [flat|nested] 16+ messages in thread* [Devel] [PATCH VZ10 2/6] drivers/md/dm-qcow2: keep seek parse window within limit
2026-08-10 12:29 [Devel] [PATCH VZ10 0/6] dm-qcow: lseek improvements Andrey Zhadchenko
2026-08-10 12:29 ` [Devel] [PATCH VZ10 1/6] drivers/md/dm-qcow2: fix lower delta seek window clamp Andrey Zhadchenko
@ 2026-08-10 12:29 ` Andrey Zhadchenko
2026-08-14 9:55 ` Pavel Tikhomirov
2026-08-10 12:29 ` [Devel] [PATCH VZ10 3/6] drivers/md/dm-qcow2: fix seek constant comparison Andrey Zhadchenko
` (4 subsequent siblings)
6 siblings, 1 reply; 16+ messages in thread
From: Andrey Zhadchenko @ 2026-08-10 12:29 UTC (permalink / raw)
seek_qio_next_clu() always sets the window size to the whole
cluster, so the window may cross the scan limit. The top level qio
then parses metadata past the virtual disk end if the disk size is
not cluster aligned, and SEEK_HOLE may report "sector + size" past
the limit.
This is especially painful for lower delta seeks, since at top
level this is luckily alleviated by blkdev_llseek_wrapper().
Add seek_qio_set_sector(), which sets bi_sector and also clamps
bi_size if needed.
https://virtuozzo.atlassian.net/browse/VSTOR-139407
Feature: dm-qcow2: block device over QCOW2 files driver
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
drivers/md/dm-qcow2-map.c | 29 +++++++++++++++++------------
1 file changed, 17 insertions(+), 12 deletions(-)
diff --git a/drivers/md/dm-qcow2-map.c b/drivers/md/dm-qcow2-map.c
index 542c7949321e5..342e4af688509 100644
--- a/drivers/md/dm-qcow2-map.c
+++ b/drivers/md/dm-qcow2-map.c
@@ -4352,6 +4352,16 @@ struct qio_data_llseek_hole {
#define SEEK_QIO_DATA(qio) ((struct qio_data_llseek_hole *)qio->data)
+static void seek_qio_set_sector(struct qio *qio, sector_t bi_sector)
+{
+ loff_t pos = to_bytes(bi_sector);
+ loff_t end = round_up(pos + 1, qio->qcow2->clu_size);
+
+ qio->bi_iter.bi_sector = bi_sector;
+ end = min_t(loff_t, end, SEEK_QIO_DATA(qio)->lim);
+ qio->bi_iter.bi_size = end > pos ? end - pos : 0;
+}
+
static struct qio *alloc_seek_qio(struct qcow2 *qcow2, struct qio *parent, loff_t new_lim)
{
struct qio_data_llseek_hole *data;
@@ -4376,12 +4386,7 @@ static struct qio *alloc_seek_qio(struct qcow2 *qcow2, struct qio *parent, loff_
if (parent) {
data->higher = parent;
- qio->bi_iter.bi_sector = parent->bi_iter.bi_sector;
-
- if (to_bytes(qio->bi_iter.bi_sector) + parent->bi_iter.bi_size > new_lim)
- qio->bi_iter.bi_size = new_lim - to_bytes(qio->bi_iter.bi_sector);
- else
- qio->bi_iter.bi_size = parent->bi_iter.bi_size;
+ seek_qio_set_sector(qio, parent->bi_iter.bi_sector);
}
return qio;
@@ -4422,16 +4427,18 @@ static inline sector_t get_next_clu(struct qio *qio)
static inline void seek_qio_next_clu(struct qio *qio, struct qcow2_map *map)
{
+ sector_t bi_sector;
+
/*
* Whole L2 table is unmapped - skip to next l2 table,
* but only if there is no backing image
*/
if (map && !(map->level & L2_LEVEL) && !qio->qcow2->lower)
- qio->bi_iter.bi_sector = get_next_l2(qio);
+ bi_sector = get_next_l2(qio);
else
- qio->bi_iter.bi_sector = get_next_clu(qio);
+ bi_sector = get_next_clu(qio);
- qio->bi_iter.bi_size = qio->qcow2->clu_size;
+ seek_qio_set_sector(qio, bi_sector);
}
static struct qio *advance_and_spawn_lower_seek_qio(struct qio *old_qio, u32 size)
@@ -4567,9 +4574,7 @@ loff_t qcow2_llseek_hole(struct dm_target *ti, loff_t offset, int whence)
if (!qio)
return -ENOMEM;
- qio->bi_iter.bi_sector = to_sector(offset);
- qio->bi_iter.bi_size = qcow2->clu_size -
- to_bytes(qio->bi_iter.bi_sector) % qcow2->clu_size;
+ seek_qio_set_sector(qio, to_sector(offset));
ret = qcow2_llseek_hole_qio(qio, whence, &result);
/* In case of error remap ENXIO as it have special meaning for llseek */
--
2.43.5
^ permalink raw reply [flat|nested] 16+ messages in thread* Re: [Devel] [PATCH VZ10 2/6] drivers/md/dm-qcow2: keep seek parse window within limit
2026-08-10 12:29 ` [Devel] [PATCH VZ10 2/6] drivers/md/dm-qcow2: keep seek parse window within limit Andrey Zhadchenko
@ 2026-08-14 9:55 ` Pavel Tikhomirov
2026-08-14 10:26 ` Andrey Zhadchenko
0 siblings, 1 reply; 16+ messages in thread
From: Pavel Tikhomirov @ 2026-08-14 9:55 UTC (permalink / raw)
On 8/10/26 14:29, Andrey Zhadchenko wrote:
> seek_qio_next_clu() always sets the window size to the whole
> cluster, so the window may cross the scan limit. The top level qio
> then parses metadata past the virtual disk end if the disk size is
> not cluster aligned, and SEEK_HOLE may report "sector + size" past
> the limit.
> This is especially painful for lower delta seeks, since at top
> level this is luckily alleviated by blkdev_llseek_wrapper().
>
> Add seek_qio_set_sector(), which sets bi_sector and also clamps
> bi_size if needed.
>
> https://virtuozzo.atlassian.net/browse/VSTOR-139407
> Feature: dm-qcow2: block device over QCOW2 files driver
> Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
> ---
> drivers/md/dm-qcow2-map.c | 29 +++++++++++++++++------------
> 1 file changed, 17 insertions(+), 12 deletions(-)
>
> diff --git a/drivers/md/dm-qcow2-map.c b/drivers/md/dm-qcow2-map.c
> index 542c7949321e5..342e4af688509 100644
> --- a/drivers/md/dm-qcow2-map.c
> +++ b/drivers/md/dm-qcow2-map.c
> @@ -4352,6 +4352,16 @@ struct qio_data_llseek_hole {
>
> #define SEEK_QIO_DATA(qio) ((struct qio_data_llseek_hole *)qio->data)
>
> +static void seek_qio_set_sector(struct qio *qio, sector_t bi_sector)
> +{
> + loff_t pos = to_bytes(bi_sector);
> + loff_t end = round_up(pos + 1, qio->qcow2->clu_size);
> +
> + qio->bi_iter.bi_sector = bi_sector;
> + end = min_t(loff_t, end, SEEK_QIO_DATA(qio)->lim);
> + qio->bi_iter.bi_size = end > pos ? end - pos : 0;
> +}
> +
> static struct qio *alloc_seek_qio(struct qcow2 *qcow2, struct qio *parent, loff_t new_lim)
> {
> struct qio_data_llseek_hole *data;
> @@ -4376,12 +4386,7 @@ static struct qio *alloc_seek_qio(struct qcow2 *qcow2, struct qio *parent, loff_
>
> if (parent) {
> data->higher = parent;
> - qio->bi_iter.bi_sector = parent->bi_iter.bi_sector;
> -
> - if (to_bytes(qio->bi_iter.bi_sector) + parent->bi_iter.bi_size > new_lim)
> - qio->bi_iter.bi_size = new_lim - to_bytes(qio->bi_iter.bi_sector);
> - else
> - qio->bi_iter.bi_size = parent->bi_iter.bi_size;
> + seek_qio_set_sector(qio, parent->bi_iter.bi_sector);
Can new_lim be smaller than the end derived from qio->qcow2->clu_size?
It looks like now we ignore new_lim completely.
> }
>
> return qio;
> @@ -4422,16 +4427,18 @@ static inline sector_t get_next_clu(struct qio *qio)
>
> static inline void seek_qio_next_clu(struct qio *qio, struct qcow2_map *map)
> {
> + sector_t bi_sector;
> +
> /*
> * Whole L2 table is unmapped - skip to next l2 table,
> * but only if there is no backing image
> */
> if (map && !(map->level & L2_LEVEL) && !qio->qcow2->lower)
> - qio->bi_iter.bi_sector = get_next_l2(qio);
> + bi_sector = get_next_l2(qio);
> else
> - qio->bi_iter.bi_sector = get_next_clu(qio);
> + bi_sector = get_next_clu(qio);
>
> - qio->bi_iter.bi_size = qio->qcow2->clu_size;
> + seek_qio_set_sector(qio, bi_sector);
> }
>
> static struct qio *advance_and_spawn_lower_seek_qio(struct qio *old_qio, u32 size)
> @@ -4567,9 +4574,7 @@ loff_t qcow2_llseek_hole(struct dm_target *ti, loff_t offset, int whence)
> if (!qio)
> return -ENOMEM;
>
> - qio->bi_iter.bi_sector = to_sector(offset);
> - qio->bi_iter.bi_size = qcow2->clu_size -
> - to_bytes(qio->bi_iter.bi_sector) % qcow2->clu_size;
> + seek_qio_set_sector(qio, to_sector(offset));
>
> ret = qcow2_llseek_hole_qio(qio, whence, &result);
> /* In case of error remap ENXIO as it have special meaning for llseek */
--
Best regards, Pavel Tikhomirov
Senior Software Developer, Virtuozzo.
^ permalink raw reply [flat|nested] 16+ messages in thread* Re: [Devel] [PATCH VZ10 2/6] drivers/md/dm-qcow2: keep seek parse window within limit
2026-08-14 9:55 ` Pavel Tikhomirov
@ 2026-08-14 10:26 ` Andrey Zhadchenko
0 siblings, 0 replies; 16+ messages in thread
From: Andrey Zhadchenko @ 2026-08-14 10:26 UTC (permalink / raw)
On 8/14/26 11:55, Pavel Tikhomirov wrote:
>
>
> On 8/10/26 14:29, Andrey Zhadchenko wrote:
>> seek_qio_next_clu() always sets the window size to the whole
>> cluster, so the window may cross the scan limit. The top level qio
>> then parses metadata past the virtual disk end if the disk size is
>> not cluster aligned, and SEEK_HOLE may report "sector + size" past
>> the limit.
>> This is especially painful for lower delta seeks, since at top
>> level this is luckily alleviated by blkdev_llseek_wrapper().
>>
>> Add seek_qio_set_sector(), which sets bi_sector and also clamps
>> bi_size if needed.
>>
>> https://virtuozzo.atlassian.net/browse/VSTOR-139407
>> Feature: dm-qcow2: block device over QCOW2 files driver
>> Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
>> ---
>> drivers/md/dm-qcow2-map.c | 29 +++++++++++++++++------------
>> 1 file changed, 17 insertions(+), 12 deletions(-)
>>
>> diff --git a/drivers/md/dm-qcow2-map.c b/drivers/md/dm-qcow2-map.c
>> index 542c7949321e5..342e4af688509 100644
>> --- a/drivers/md/dm-qcow2-map.c
>> +++ b/drivers/md/dm-qcow2-map.c
>> @@ -4352,6 +4352,16 @@ struct qio_data_llseek_hole {
>>
>> #define SEEK_QIO_DATA(qio) ((struct qio_data_llseek_hole *)qio->data)
>>
>> +static void seek_qio_set_sector(struct qio *qio, sector_t bi_sector)
>> +{
>> + loff_t pos = to_bytes(bi_sector);
>> + loff_t end = round_up(pos + 1, qio->qcow2->clu_size);
>> +
>> + qio->bi_iter.bi_sector = bi_sector;
>> + end = min_t(loff_t, end, SEEK_QIO_DATA(qio)->lim);
>> + qio->bi_iter.bi_size = end > pos ? end - pos : 0;
>> +}
>> +
>> static struct qio *alloc_seek_qio(struct qcow2 *qcow2, struct qio *parent, loff_t new_lim)
>> {
>> struct qio_data_llseek_hole *data;
>> @@ -4376,12 +4386,7 @@ static struct qio *alloc_seek_qio(struct qcow2 *qcow2, struct qio *parent, loff_
>>
>> if (parent) {
>> data->higher = parent;
>> - qio->bi_iter.bi_sector = parent->bi_iter.bi_sector;
>> -
>> - if (to_bytes(qio->bi_iter.bi_sector) + parent->bi_iter.bi_size > new_lim)
>> - qio->bi_iter.bi_size = new_lim - to_bytes(qio->bi_iter.bi_sector);
>> - else
>> - qio->bi_iter.bi_size = parent->bi_iter.bi_size;
>> + seek_qio_set_sector(qio, parent->bi_iter.bi_sector);
>
> Can new_lim be smaller than the end derived from qio->qcow2->clu_size?
> It looks like now we ignore new_lim completely.
A few lines above we set
data->lim = new_lim;
seek_qio_set_sector respects this data->lim
>
>> }
>>
>> return qio;
>> @@ -4422,16 +4427,18 @@ static inline sector_t get_next_clu(struct qio *qio)
>>
>> static inline void seek_qio_next_clu(struct qio *qio, struct qcow2_map *map)
>> {
>> + sector_t bi_sector;
>> +
>> /*
>> * Whole L2 table is unmapped - skip to next l2 table,
>> * but only if there is no backing image
>> */
>> if (map && !(map->level & L2_LEVEL) && !qio->qcow2->lower)
>> - qio->bi_iter.bi_sector = get_next_l2(qio);
>> + bi_sector = get_next_l2(qio);
>> else
>> - qio->bi_iter.bi_sector = get_next_clu(qio);
>> + bi_sector = get_next_clu(qio);
>>
>> - qio->bi_iter.bi_size = qio->qcow2->clu_size;
>> + seek_qio_set_sector(qio, bi_sector);
>> }
>>
>> static struct qio *advance_and_spawn_lower_seek_qio(struct qio *old_qio, u32 size)
>> @@ -4567,9 +4574,7 @@ loff_t qcow2_llseek_hole(struct dm_target *ti, loff_t offset, int whence)
>> if (!qio)
>> return -ENOMEM;
>>
>> - qio->bi_iter.bi_sector = to_sector(offset);
>> - qio->bi_iter.bi_size = qcow2->clu_size -
>> - to_bytes(qio->bi_iter.bi_sector) % qcow2->clu_size;
>> + seek_qio_set_sector(qio, to_sector(offset));
>>
>> ret = qcow2_llseek_hole_qio(qio, whence, &result);
>> /* In case of error remap ENXIO as it have special meaning for llseek */
>
^ permalink raw reply [flat|nested] 16+ messages in thread
* [Devel] [PATCH VZ10 3/6] drivers/md/dm-qcow2: fix seek constant comparison
2026-08-10 12:29 [Devel] [PATCH VZ10 0/6] dm-qcow: lseek improvements Andrey Zhadchenko
2026-08-10 12:29 ` [Devel] [PATCH VZ10 1/6] drivers/md/dm-qcow2: fix lower delta seek window clamp Andrey Zhadchenko
2026-08-10 12:29 ` [Devel] [PATCH VZ10 2/6] drivers/md/dm-qcow2: keep seek parse window within limit Andrey Zhadchenko
@ 2026-08-10 12:29 ` Andrey Zhadchenko
2026-08-10 12:29 ` [Devel] [PATCH VZ10 4/6] drivers/md/dm-qcow2: fix seek skip to the next L2 table Andrey Zhadchenko
` (3 subsequent siblings)
6 siblings, 0 replies; 16+ messages in thread
From: Andrey Zhadchenko @ 2026-08-10 12:29 UTC (permalink / raw)
This is just an enum, not a mask.
https://virtuozzo.atlassian.net/browse/VSTOR-139407
Feature: dm-qcow2: block device over QCOW2 files driver
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
drivers/md/dm-qcow2-map.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/drivers/md/dm-qcow2-map.c b/drivers/md/dm-qcow2-map.c
index 342e4af688509..d6d15b04215c6 100644
--- a/drivers/md/dm-qcow2-map.c
+++ b/drivers/md/dm-qcow2-map.c
@@ -4520,7 +4520,7 @@ static int qcow2_llseek_hole_qio(struct qio *qio, int whence, loff_t *result)
}
}
- if (whence & SEEK_HOLE) {
+ if (whence == SEEK_HOLE) {
if (arg.zeroes || arg.unmapped) {
*result = to_bytes(qio->bi_iter.bi_sector);
ret = 0;
@@ -4536,7 +4536,7 @@ static int qcow2_llseek_hole_qio(struct qio *qio, int whence, loff_t *result)
}
}
- if (whence & SEEK_DATA) {
+ if (whence == SEEK_DATA) {
if (!arg.zeroes && !arg.unmapped) {
*result = to_bytes(qio->bi_iter.bi_sector);
ret = 0;
--
2.43.5
^ permalink raw reply [flat|nested] 16+ messages in thread* [Devel] [PATCH VZ10 4/6] drivers/md/dm-qcow2: fix seek skip to the next L2 table
2026-08-10 12:29 [Devel] [PATCH VZ10 0/6] dm-qcow: lseek improvements Andrey Zhadchenko
` (2 preceding siblings ...)
2026-08-10 12:29 ` [Devel] [PATCH VZ10 3/6] drivers/md/dm-qcow2: fix seek constant comparison Andrey Zhadchenko
@ 2026-08-10 12:29 ` Andrey Zhadchenko
2026-08-14 10:25 ` Pavel Tikhomirov
2026-08-10 12:29 ` [Devel] [PATCH VZ10 5/6] drivers/md/dm-qcow2: scan lower delta in one pass over empty L1 entry Andrey Zhadchenko
` (2 subsequent siblings)
6 siblings, 1 reply; 16+ messages in thread
From: Andrey Zhadchenko @ 2026-08-10 12:29 UTC (permalink / raw)
get_next_l2() adds the number of remaining clusters to the current
sector, so for a position which is not cluster aligned (e.g. the
llseek offset itself) the result overshoots the L2 table boundary by
the in-cluster offset.
Compute the boundary from the cluster index instead of adding to the
current position.
https://virtuozzo.atlassian.net/browse/VSTOR-139407
Feature: dm-qcow2: block device over QCOW2 files driver
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
drivers/md/dm-qcow2-map.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/drivers/md/dm-qcow2-map.c b/drivers/md/dm-qcow2-map.c
index d6d15b04215c6..776e89fc4eb1b 100644
--- a/drivers/md/dm-qcow2-map.c
+++ b/drivers/md/dm-qcow2-map.c
@@ -4405,12 +4405,12 @@ static struct qio *free_seek_qio_ret_higher(struct qio *qio)
static inline sector_t get_next_l2(struct qio *qio)
{
struct qcow2 *qcow2 = qio->qcow2;
- loff_t start, add;
+ u64 base_l2, add;
- start = to_bytes(qio->bi_iter.bi_sector);
- add = qcow2->l2_entries - (start / qcow2->clu_size) % qcow2->l2_entries;
+ base_l2 = to_bytes(qio->bi_iter.bi_sector) / qcow2->clu_size;
+ add = qcow2->l2_entries - base_l2 % qcow2->l2_entries;
- return qio->bi_iter.bi_sector + (qcow2->clu_size / to_bytes(1)) * add;
+ return to_sector((base_l2 + add) * qcow2->clu_size);
}
static inline sector_t get_next_clu(struct qio *qio)
--
2.43.5
^ permalink raw reply [flat|nested] 16+ messages in thread* Re: [Devel] [PATCH VZ10 4/6] drivers/md/dm-qcow2: fix seek skip to the next L2 table
2026-08-10 12:29 ` [Devel] [PATCH VZ10 4/6] drivers/md/dm-qcow2: fix seek skip to the next L2 table Andrey Zhadchenko
@ 2026-08-14 10:25 ` Pavel Tikhomirov
2026-08-14 10:33 ` Andrey Zhadchenko
0 siblings, 1 reply; 16+ messages in thread
From: Pavel Tikhomirov @ 2026-08-14 10:25 UTC (permalink / raw)
On 8/10/26 14:29, Andrey Zhadchenko wrote:
> get_next_l2() adds the number of remaining clusters to the current
> sector, so for a position which is not cluster aligned (e.g. the
> llseek offset itself) the result overshoots the L2 table boundary by
> the in-cluster offset.
>
> Compute the boundary from the cluster index instead of adding to the
> current position.
>
> https://virtuozzo.atlassian.net/browse/VSTOR-139407
> Feature: dm-qcow2: block device over QCOW2 files driver
> Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
> ---
> drivers/md/dm-qcow2-map.c | 8 ++++----
> 1 file changed, 4 insertions(+), 4 deletions(-)
>
> diff --git a/drivers/md/dm-qcow2-map.c b/drivers/md/dm-qcow2-map.c
> index d6d15b04215c6..776e89fc4eb1b 100644
> --- a/drivers/md/dm-qcow2-map.c
> +++ b/drivers/md/dm-qcow2-map.c
> @@ -4405,12 +4405,12 @@ static struct qio *free_seek_qio_ret_higher(struct qio *qio)
> static inline sector_t get_next_l2(struct qio *qio)
> {
> struct qcow2 *qcow2 = qio->qcow2;
> - loff_t start, add;
> + u64 base_l2, add;
>
> - start = to_bytes(qio->bi_iter.bi_sector);
> - add = qcow2->l2_entries - (start / qcow2->clu_size) % qcow2->l2_entries;
> + base_l2 = to_bytes(qio->bi_iter.bi_sector) / qcow2->clu_size;
> + add = qcow2->l2_entries - base_l2 % qcow2->l2_entries;
>
> - return qio->bi_iter.bi_sector + (qcow2->clu_size / to_bytes(1)) * add;
> + return to_sector((base_l2 + add) * qcow2->clu_size);
The above code seems too complex to what it actually tries to accomplish,
it's just an alignment, what about:
static inline sector_t get_next_l2(struct qio *qio)
{
struct qcow2 *qcow2 = qio->qcow2;
u64 l2_covered = (u64)qcow2->clu_size * qcow2->l2_entries;
return to_sector(round_up(to_bytes(qio->bi_iter.bi_sector) + 1,
l2_covered));
}
> }
>
> static inline sector_t get_next_clu(struct qio *qio)
--
Best regards, Pavel Tikhomirov
Senior Software Developer, Virtuozzo.
^ permalink raw reply [flat|nested] 16+ messages in thread* Re: [Devel] [PATCH VZ10 4/6] drivers/md/dm-qcow2: fix seek skip to the next L2 table
2026-08-14 10:25 ` Pavel Tikhomirov
@ 2026-08-14 10:33 ` Andrey Zhadchenko
0 siblings, 0 replies; 16+ messages in thread
From: Andrey Zhadchenko @ 2026-08-14 10:33 UTC (permalink / raw)
On 8/14/26 12:25, Pavel Tikhomirov wrote:
>
>
> On 8/10/26 14:29, Andrey Zhadchenko wrote:
>> get_next_l2() adds the number of remaining clusters to the current
>> sector, so for a position which is not cluster aligned (e.g. the
>> llseek offset itself) the result overshoots the L2 table boundary by
>> the in-cluster offset.
>>
>> Compute the boundary from the cluster index instead of adding to the
>> current position.
>>
>> https://virtuozzo.atlassian.net/browse/VSTOR-139407
>> Feature: dm-qcow2: block device over QCOW2 files driver
>> Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
>> ---
>> drivers/md/dm-qcow2-map.c | 8 ++++----
>> 1 file changed, 4 insertions(+), 4 deletions(-)
>>
>> diff --git a/drivers/md/dm-qcow2-map.c b/drivers/md/dm-qcow2-map.c
>> index d6d15b04215c6..776e89fc4eb1b 100644
>> --- a/drivers/md/dm-qcow2-map.c
>> +++ b/drivers/md/dm-qcow2-map.c
>> @@ -4405,12 +4405,12 @@ static struct qio *free_seek_qio_ret_higher(struct qio *qio)
>> static inline sector_t get_next_l2(struct qio *qio)
>> {
>> struct qcow2 *qcow2 = qio->qcow2;
>> - loff_t start, add;
>> + u64 base_l2, add;
>>
>> - start = to_bytes(qio->bi_iter.bi_sector);
>> - add = qcow2->l2_entries - (start / qcow2->clu_size) % qcow2->l2_entries;
>> + base_l2 = to_bytes(qio->bi_iter.bi_sector) / qcow2->clu_size;
>> + add = qcow2->l2_entries - base_l2 % qcow2->l2_entries;
>>
>> - return qio->bi_iter.bi_sector + (qcow2->clu_size / to_bytes(1)) * add;
>> + return to_sector((base_l2 + add) * qcow2->clu_size);
>
> The above code seems too complex to what it actually tries to accomplish,
> it's just an alignment, what about:
>
> static inline sector_t get_next_l2(struct qio *qio)
> {
> struct qcow2 *qcow2 = qio->qcow2;
> u64 l2_covered = (u64)qcow2->clu_size * qcow2->l2_entries;
>
> return to_sector(round_up(to_bytes(qio->bi_iter.bi_sector) + 1,
> l2_covered));
> }
Yeah, that looks much better. But probably no sense to convert sector to
bytes and then back to sector (as cluster_size for qcow2 is always a
power of 2 no less then 512). I will update this part for v2
>
>> }
>>
>> static inline sector_t get_next_clu(struct qio *qio)
>
^ permalink raw reply [flat|nested] 16+ messages in thread
* [Devel] [PATCH VZ10 5/6] drivers/md/dm-qcow2: scan lower delta in one pass over empty L1 entry
2026-08-10 12:29 [Devel] [PATCH VZ10 0/6] dm-qcow: lseek improvements Andrey Zhadchenko
` (3 preceding siblings ...)
2026-08-10 12:29 ` [Devel] [PATCH VZ10 4/6] drivers/md/dm-qcow2: fix seek skip to the next L2 table Andrey Zhadchenko
@ 2026-08-10 12:29 ` Andrey Zhadchenko
2026-08-14 11:09 ` Pavel Tikhomirov
2026-08-10 12:30 ` [Devel] [PATCH VZ10 6/6] drivers/md/dm-qcow2: seek unallocated L2 entries in one pass within md Andrey Zhadchenko
2026-08-14 11:55 ` [Devel] [PATCH VZ10 0/6] dm-qcow: lseek improvements Pavel Tikhomirov
6 siblings, 1 reply; 16+ messages in thread
From: Andrey Zhadchenko @ 2026-08-10 12:29 UTC (permalink / raw)
When an L1 entry is absent and there is a backing image, the seek
code descends into the lower delta one cluster at a time.
Change advance_and_spawn_lower_seek_qio() to take limit instead of
a size. Pass bigger range if L1 entry is empty.
On a 16G image (128K clusters, extended L2) with no L1 entries over a
backing image with data at 15G, warm-cache SEEK_DATA improves from
40.2 ms to 0.01 ms.
https://virtuozzo.atlassian.net/browse/VSTOR-139407
Feature: dm-qcow2: block device over QCOW2 files driver
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
drivers/md/dm-qcow2-map.c | 29 +++++++++++++++--------------
1 file changed, 15 insertions(+), 14 deletions(-)
diff --git a/drivers/md/dm-qcow2-map.c b/drivers/md/dm-qcow2-map.c
index 776e89fc4eb1b..6a73c32689eca 100644
--- a/drivers/md/dm-qcow2-map.c
+++ b/drivers/md/dm-qcow2-map.c
@@ -4441,26 +4441,19 @@ static inline void seek_qio_next_clu(struct qio *qio, struct qcow2_map *map)
seek_qio_set_sector(qio, bi_sector);
}
-static struct qio *advance_and_spawn_lower_seek_qio(struct qio *old_qio, u32 size)
+static struct qio *advance_and_spawn_lower_seek_qio(struct qio *old_qio, loff_t end)
{
+ struct qcow2 *lower = old_qio->qcow2->lower;
struct qio *new_qio;
- loff_t start;
- start = to_bytes(old_qio->bi_iter.bi_sector);
+ if (end > lower->hdr.size)
+ end = lower->hdr.size;
- if (start + size > old_qio->qcow2->lower->hdr.size)
- size = old_qio->qcow2->lower->hdr.size - start;
-
- new_qio = alloc_seek_qio(old_qio->qcow2->lower, old_qio, start + size);
+ new_qio = alloc_seek_qio(lower, old_qio, end);
if (!new_qio)
return NULL;
- if (old_qio->bi_iter.bi_size == size) {
- seek_qio_next_clu(old_qio, NULL);
- } else {
- old_qio->bi_iter.bi_sector += to_sector(size);
- old_qio->bi_iter.bi_size -= size;
- }
+ seek_qio_set_sector(old_qio, to_sector(end));
return new_qio;
}
@@ -4508,8 +4501,16 @@ static int qcow2_llseek_hole_qio(struct qio *qio, int whence, loff_t *result)
*/
if (to_bytes(qio->bi_iter.bi_sector) < qio->qcow2->lower->hdr.size) {
struct qio *new_qio;
+ loff_t end;
+
+ if (!(map.level & L2_LEVEL))
+ end = min_t(loff_t,
+ to_bytes(get_next_l2(qio)),
+ SEEK_QIO_DATA(qio)->lim);
+ else
+ end = to_bytes(qio->bi_iter.bi_sector) + size;
- new_qio = advance_and_spawn_lower_seek_qio(qio, size);
+ new_qio = advance_and_spawn_lower_seek_qio(qio, end);
if (!new_qio) {
ret = -ENOMEM;
break;
--
2.43.5
^ permalink raw reply [flat|nested] 16+ messages in thread* Re: [Devel] [PATCH VZ10 5/6] drivers/md/dm-qcow2: scan lower delta in one pass over empty L1 entry
2026-08-10 12:29 ` [Devel] [PATCH VZ10 5/6] drivers/md/dm-qcow2: scan lower delta in one pass over empty L1 entry Andrey Zhadchenko
@ 2026-08-14 11:09 ` Pavel Tikhomirov
2026-08-14 12:00 ` Andrey Zhadchenko
0 siblings, 1 reply; 16+ messages in thread
From: Pavel Tikhomirov @ 2026-08-14 11:09 UTC (permalink / raw)
On 8/10/26 14:29, Andrey Zhadchenko wrote:
> When an L1 entry is absent and there is a backing image, the seek
> code descends into the lower delta one cluster at a time.
>
> Change advance_and_spawn_lower_seek_qio() to take limit instead of
> a size. Pass bigger range if L1 entry is empty.
>
> On a 16G image (128K clusters, extended L2) with no L1 entries over a
> backing image with data at 15G, warm-cache SEEK_DATA improves from
> 40.2 ms to 0.01 ms.
>
> https://virtuozzo.atlassian.net/browse/VSTOR-139407
> Feature: dm-qcow2: block device over QCOW2 files driver
> Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
> ---
> drivers/md/dm-qcow2-map.c | 29 +++++++++++++++--------------
> 1 file changed, 15 insertions(+), 14 deletions(-)
>
> diff --git a/drivers/md/dm-qcow2-map.c b/drivers/md/dm-qcow2-map.c
> index 776e89fc4eb1b..6a73c32689eca 100644
> --- a/drivers/md/dm-qcow2-map.c
> +++ b/drivers/md/dm-qcow2-map.c
> @@ -4441,26 +4441,19 @@ static inline void seek_qio_next_clu(struct qio *qio, struct qcow2_map *map)
> seek_qio_set_sector(qio, bi_sector);
> }
>
> -static struct qio *advance_and_spawn_lower_seek_qio(struct qio *old_qio, u32 size)
> +static struct qio *advance_and_spawn_lower_seek_qio(struct qio *old_qio, loff_t end)
> {
> + struct qcow2 *lower = old_qio->qcow2->lower;
> struct qio *new_qio;
> - loff_t start;
>
> - start = to_bytes(old_qio->bi_iter.bi_sector);
> + if (end > lower->hdr.size)
> + end = lower->hdr.size;
>
> - if (start + size > old_qio->qcow2->lower->hdr.size)
> - size = old_qio->qcow2->lower->hdr.size - start;
> -
> - new_qio = alloc_seek_qio(old_qio->qcow2->lower, old_qio, start + size);
> + new_qio = alloc_seek_qio(lower, old_qio, end);
> if (!new_qio)
> return NULL;
>
> - if (old_qio->bi_iter.bi_size == size) {
> - seek_qio_next_clu(old_qio, NULL);
Removal of this branch is a bit suspicious. Previously we advanced old_qio to next cluster
after bi_size should become zero. After this change we don't advance. AI suggests that
this can lead to infinite loop as then we will return back from new_qio to old_qio it won't
continue the search correctly.
> - } else {
> - old_qio->bi_iter.bi_sector += to_sector(size);
> - old_qio->bi_iter.bi_size -= size;
> - }
> + seek_qio_set_sector(old_qio, to_sector(end));
>
> return new_qio;
> }
> @@ -4508,8 +4501,16 @@ static int qcow2_llseek_hole_qio(struct qio *qio, int whence, loff_t *result)
> */
> if (to_bytes(qio->bi_iter.bi_sector) < qio->qcow2->lower->hdr.size) {
> struct qio *new_qio;
> + loff_t end;
> +
> + if (!(map.level & L2_LEVEL))
> + end = min_t(loff_t,
> + to_bytes(get_next_l2(qio)),
> + SEEK_QIO_DATA(qio)->lim);
> + else
> + end = to_bytes(qio->bi_iter.bi_sector) + size;
>
> - new_qio = advance_and_spawn_lower_seek_qio(qio, size);
> + new_qio = advance_and_spawn_lower_seek_qio(qio, end);
> if (!new_qio) {
> ret = -ENOMEM;
> break;
--
Best regards, Pavel Tikhomirov
Senior Software Developer, Virtuozzo.
^ permalink raw reply [flat|nested] 16+ messages in thread* Re: [Devel] [PATCH VZ10 5/6] drivers/md/dm-qcow2: scan lower delta in one pass over empty L1 entry
2026-08-14 11:09 ` Pavel Tikhomirov
@ 2026-08-14 12:00 ` Andrey Zhadchenko
0 siblings, 0 replies; 16+ messages in thread
From: Andrey Zhadchenko @ 2026-08-14 12:00 UTC (permalink / raw)
On 8/14/26 13:09, Pavel Tikhomirov wrote:
>
>
> On 8/10/26 14:29, Andrey Zhadchenko wrote:
>> When an L1 entry is absent and there is a backing image, the seek
>> code descends into the lower delta one cluster at a time.
>>
>> Change advance_and_spawn_lower_seek_qio() to take limit instead of
>> a size. Pass bigger range if L1 entry is empty.
>>
>> On a 16G image (128K clusters, extended L2) with no L1 entries over a
>> backing image with data at 15G, warm-cache SEEK_DATA improves from
>> 40.2 ms to 0.01 ms.
>>
>> https://virtuozzo.atlassian.net/browse/VSTOR-139407
>> Feature: dm-qcow2: block device over QCOW2 files driver
>> Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
>> ---
>> drivers/md/dm-qcow2-map.c | 29 +++++++++++++++--------------
>> 1 file changed, 15 insertions(+), 14 deletions(-)
>>
>> diff --git a/drivers/md/dm-qcow2-map.c b/drivers/md/dm-qcow2-map.c
>> index 776e89fc4eb1b..6a73c32689eca 100644
>> --- a/drivers/md/dm-qcow2-map.c
>> +++ b/drivers/md/dm-qcow2-map.c
>> @@ -4441,26 +4441,19 @@ static inline void seek_qio_next_clu(struct qio *qio, struct qcow2_map *map)
>> seek_qio_set_sector(qio, bi_sector);
>> }
>>
>> -static struct qio *advance_and_spawn_lower_seek_qio(struct qio *old_qio, u32 size)
>> +static struct qio *advance_and_spawn_lower_seek_qio(struct qio *old_qio, loff_t end)
>> {
>> + struct qcow2 *lower = old_qio->qcow2->lower;
>> struct qio *new_qio;
>> - loff_t start;
>>
>> - start = to_bytes(old_qio->bi_iter.bi_sector);
>> + if (end > lower->hdr.size)
>> + end = lower->hdr.size;
>>
>> - if (start + size > old_qio->qcow2->lower->hdr.size)
>> - size = old_qio->qcow2->lower->hdr.size - start;
>> -
>> - new_qio = alloc_seek_qio(old_qio->qcow2->lower, old_qio, start + size);
>> + new_qio = alloc_seek_qio(lower, old_qio, end);
>> if (!new_qio)
>> return NULL;
>>
>> - if (old_qio->bi_iter.bi_size == size) {
>> - seek_qio_next_clu(old_qio, NULL);
>
> Removal of this branch is a bit suspicious. Previously we advanced old_qio to next cluster
> after bi_size should become zero. After this change we don't advance. AI suggests that
> this can lead to infinite loop as then we will return back from new_qio to old_qio it won't
> continue the search correctly.
seek_qio_set_sector(old_qio, to_sector(end)) will move old qio to the
next cluster (end) we want to scan in the top image. I don't see any
problem here. bi_size is rather irrelevant with new code using the end.
It is only needed to check that calc_front_qio_bytes() return doesn't
indicate that hole/data starts mid-cluster.
Can you provide a hypothetical example for such infinite loop?
>
>> - } else {
>> - old_qio->bi_iter.bi_sector += to_sector(size);
>> - old_qio->bi_iter.bi_size -= size;
>> - }
>> + seek_qio_set_sector(old_qio, to_sector(end));
>>
>> return new_qio;
>> }
>> @@ -4508,8 +4501,16 @@ static int qcow2_llseek_hole_qio(struct qio *qio, int whence, loff_t *result)
>> */
>> if (to_bytes(qio->bi_iter.bi_sector) < qio->qcow2->lower->hdr.size) {
>> struct qio *new_qio;
>> + loff_t end;
>> +
>> + if (!(map.level & L2_LEVEL))
>> + end = min_t(loff_t,
>> + to_bytes(get_next_l2(qio)),
>> + SEEK_QIO_DATA(qio)->lim);
>> + else
>> + end = to_bytes(qio->bi_iter.bi_sector) + size;
>>
>> - new_qio = advance_and_spawn_lower_seek_qio(qio, size);
>> + new_qio = advance_and_spawn_lower_seek_qio(qio, end);
>> if (!new_qio) {
>> ret = -ENOMEM;
>> break;
>
^ permalink raw reply [flat|nested] 16+ messages in thread
* [Devel] [PATCH VZ10 6/6] drivers/md/dm-qcow2: seek unallocated L2 entries in one pass within md
2026-08-10 12:29 [Devel] [PATCH VZ10 0/6] dm-qcow: lseek improvements Andrey Zhadchenko
` (4 preceding siblings ...)
2026-08-10 12:29 ` [Devel] [PATCH VZ10 5/6] drivers/md/dm-qcow2: scan lower delta in one pass over empty L1 entry Andrey Zhadchenko
@ 2026-08-10 12:30 ` Andrey Zhadchenko
2026-08-12 12:42 ` Vasileios Almpanis
2026-08-14 11:55 ` [Devel] [PATCH VZ10 0/6] dm-qcow: lseek improvements Pavel Tikhomirov
6 siblings, 1 reply; 16+ messages in thread
From: Andrey Zhadchenko @ 2026-08-10 12:30 UTC (permalink / raw)
When a cluster is unallocated but its L2 table exists, the seek code
walks it one cluster at a time: SEEK_DATA re-parses metadata for
every cluster of the range, and with a backing image each cluster
additionally costs a qio and completion allocation for the lower
delta descent.
If the unmapped range reaches the cluster border, prolong it over the
following unallocated L2 entries: they are cached in the very md page
the current entry was parsed from, so this is just scanning for
non-zero words. Then the lower delta is scanned in one pass over the
whole range, and SEEK_DATA without a backing image skips it in one go.
Entries with any subcluster or "reads as zeroes" bits set stop the
scan and spin parse_metadata() machinery.
On a 16G image (128K clusters, extended L2) with all L2 tables
allocated but no clusters mapped over a backing image with data at
15G, warm-cache SEEK_DATA improves from 44.8 ms to 0.4 ms.
https://virtuozzo.atlassian.net/browse/VSTOR-139407
Feature: dm-qcow2: block device over QCOW2 files driver
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
drivers/md/dm-qcow2-map.c | 44 +++++++++++++++++++++++++++++++++++++--
1 file changed, 42 insertions(+), 2 deletions(-)
diff --git a/drivers/md/dm-qcow2-map.c b/drivers/md/dm-qcow2-map.c
index 6a73c32689eca..3bda108db725f 100644
--- a/drivers/md/dm-qcow2-map.c
+++ b/drivers/md/dm-qcow2-map.c
@@ -4441,6 +4441,34 @@ static inline void seek_qio_next_clu(struct qio *qio, struct qcow2_map *map)
seek_qio_set_sector(qio, bi_sector);
}
+/*
+ * Manually scan L2 metadata pages until we hit something interesting.
+ * We do not take md_pages_lock here because we have no promises about
+ * concurrent writes anyway.
+ */
+static loff_t seek_extend_unmapped_end(struct qio *qio, struct qcow2_map *map,
+ loff_t end)
+{
+ struct qcow2 *qcow2 = qio->qcow2;
+ loff_t lim = SEEK_QIO_DATA(qio)->lim;
+ u32 step = 1 + qcow2->ext_l2;
+ u32 index = map->l2.index_in_page + step;
+ u64 *entries;
+
+ entries = kmap_local_page(map->l2.md->page);
+ for (; index + step <= PAGE_SIZE / sizeof(u64) && end < lim;
+ index += step) {
+ /* Zero test does not need be64_to_cpu() */
+ if (READ_ONCE(entries[index]) ||
+ (qcow2->ext_l2 && READ_ONCE(entries[index + 1])))
+ break;
+ end += qcow2->clu_size;
+ }
+ kunmap_local(entries);
+
+ return min_t(loff_t, end, lim);
+}
+
static struct qio *advance_and_spawn_lower_seek_qio(struct qio *old_qio, loff_t end)
{
struct qcow2 *lower = old_qio->qcow2->lower;
@@ -4503,12 +4531,15 @@ static int qcow2_llseek_hole_qio(struct qio *qio, int whence, loff_t *result)
struct qio *new_qio;
loff_t end;
- if (!(map.level & L2_LEVEL))
+ if (!(map.level & L2_LEVEL)) {
end = min_t(loff_t,
to_bytes(get_next_l2(qio)),
SEEK_QIO_DATA(qio)->lim);
- else
+ } else {
end = to_bytes(qio->bi_iter.bi_sector) + size;
+ if (!CLU_OFF(qio->qcow2, end))
+ end = seek_extend_unmapped_end(qio, &map, end);
+ }
new_qio = advance_and_spawn_lower_seek_qio(qio, end);
if (!new_qio) {
@@ -4552,6 +4583,15 @@ static int qcow2_llseek_hole_qio(struct qio *qio, int whence, loff_t *result)
qio->bi_iter.bi_sector += to_sector(size);
qio->bi_iter.bi_size -= size;
goto calc_subclu;
+ } else if (arg.unmapped && (map.level & L2_LEVEL)) {
+ loff_t end = to_bytes(qio->bi_iter.bi_sector) + size;
+
+ /* Skip the following unallocated entries in one go */
+ if (!CLU_OFF(qio->qcow2, end)) {
+ seek_qio_set_sector(qio,
+ seek_extend_unmapped_end(qio, &map, end));
+ continue;
+ }
}
}
--
2.43.5
^ permalink raw reply [flat|nested] 16+ messages in thread* Re: [Devel] [PATCH VZ10 6/6] drivers/md/dm-qcow2: seek unallocated L2 entries in one pass within md
2026-08-10 12:30 ` [Devel] [PATCH VZ10 6/6] drivers/md/dm-qcow2: seek unallocated L2 entries in one pass within md Andrey Zhadchenko
@ 2026-08-12 12:42 ` Vasileios Almpanis
2026-08-12 12:50 ` Andrey Zhadchenko
0 siblings, 1 reply; 16+ messages in thread
From: Vasileios Almpanis @ 2026-08-12 12:42 UTC (permalink / raw)
On 8/10/26 2:30 PM, Andrey Zhadchenko wrote:
> When a cluster is unallocated but its L2 table exists, the seek code
> walks it one cluster at a time: SEEK_DATA re-parses metadata for
> every cluster of the range, and with a backing image each cluster
> additionally costs a qio and completion allocation for the lower
> delta descent.
>
> If the unmapped range reaches the cluster border, prolong it over the
> following unallocated L2 entries: they are cached in the very md page
> the current entry was parsed from, so this is just scanning for
> non-zero words. Then the lower delta is scanned in one pass over the
> whole range, and SEEK_DATA without a backing image skips it in one go.
>
> Entries with any subcluster or "reads as zeroes" bits set stop the
> scan and spin parse_metadata() machinery.
>
> On a 16G image (128K clusters, extended L2) with all L2 tables
> allocated but no clusters mapped over a backing image with data at
> 15G, warm-cache SEEK_DATA improves from 44.8 ms to 0.4 ms.
>
> https://virtuozzo.atlassian.net/browse/VSTOR-139407
> Feature: dm-qcow2: block device over QCOW2 files driver
> Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
> ---
> drivers/md/dm-qcow2-map.c | 44 +++++++++++++++++++++++++++++++++++++--
> 1 file changed, 42 insertions(+), 2 deletions(-)
>
> diff --git a/drivers/md/dm-qcow2-map.c b/drivers/md/dm-qcow2-map.c
> index 6a73c32689eca..3bda108db725f 100644
> --- a/drivers/md/dm-qcow2-map.c
> +++ b/drivers/md/dm-qcow2-map.c
> @@ -4441,6 +4441,34 @@ static inline void seek_qio_next_clu(struct qio *qio, struct qcow2_map *map)
> seek_qio_set_sector(qio, bi_sector);
> }
>
> +/*
> + * Manually scan L2 metadata pages until we hit something interesting.
> + * We do not take md_pages_lock here because we have no promises about
> + * concurrent writes anyway.
> + */
> +static loff_t seek_extend_unmapped_end(struct qio *qio, struct qcow2_map *map,
> + loff_t end)
> +{
> + struct qcow2 *qcow2 = qio->qcow2;
> + loff_t lim = SEEK_QIO_DATA(qio)->lim;
> + u32 step = 1 + qcow2->ext_l2;
> + u32 index = map->l2.index_in_page + step;
> + u64 *entries;
> +
> + entries = kmap_local_page(map->l2.md->page);
> + for (; index + step <= PAGE_SIZE / sizeof(u64) && end < lim;
> + index += step) {
> + /* Zero test does not need be64_to_cpu() */
> + if (READ_ONCE(entries[index]) ||
> + (qcow2->ext_l2 && READ_ONCE(entries[index + 1])))
> + break;
> + end += qcow2->clu_size;
> + }
> + kunmap_local(entries);
> +
> + return min_t(loff_t, end, lim);
> +}
> +
> static struct qio *advance_and_spawn_lower_seek_qio(struct qio *old_qio, loff_t end)
> {
> struct qcow2 *lower = old_qio->qcow2->lower;
> @@ -4503,12 +4531,15 @@ static int qcow2_llseek_hole_qio(struct qio *qio, int whence, loff_t *result)
> struct qio *new_qio;
> loff_t end;
>
> - if (!(map.level & L2_LEVEL))
> + if (!(map.level & L2_LEVEL)) {
> end = min_t(loff_t,
> to_bytes(get_next_l2(qio)),
> SEEK_QIO_DATA(qio)->lim);
> - else
> + } else {
> end = to_bytes(qio->bi_iter.bi_sector) + size;
> + if (!CLU_OFF(qio->qcow2, end))
> + end = seek_extend_unmapped_end(qio, &map, end);
> + }
>
> new_qio = advance_and_spawn_lower_seek_qio(qio, end);
> if (!new_qio) {
> @@ -4552,6 +4583,15 @@ static int qcow2_llseek_hole_qio(struct qio *qio, int whence, loff_t *result)
> qio->bi_iter.bi_sector += to_sector(size);
> qio->bi_iter.bi_size -= size;
> goto calc_subclu;
> + } else if (arg.unmapped && (map.level & L2_LEVEL)) {
> + loff_t end = to_bytes(qio->bi_iter.bi_sector) + size;
> +
> + /* Skip the following unallocated entries in one go */
> + if (!CLU_OFF(qio->qcow2, end)) {
> + seek_qio_set_sector(qio,
shouldn't you wrap the result of seek_extend_unmapped_end in to_sector?
It returns bytes and seek_qio_set_sector takes sector_t and we would
immediately do to_bytes on something that is already bytes.
> + seek_extend_unmapped_end(qio, &map, end));
> + continue;
> + }
> }
> }
>
--
Best regards, Vasileios Almpanis
Software Developer, Virtuozzo.
^ permalink raw reply [flat|nested] 16+ messages in thread* Re: [Devel] [PATCH VZ10 6/6] drivers/md/dm-qcow2: seek unallocated L2 entries in one pass within md
2026-08-12 12:42 ` Vasileios Almpanis
@ 2026-08-12 12:50 ` Andrey Zhadchenko
0 siblings, 0 replies; 16+ messages in thread
From: Andrey Zhadchenko @ 2026-08-12 12:50 UTC (permalink / raw)
On 8/12/26 14:42, Vasileios Almpanis wrote:
>
> On 8/10/26 2:30 PM, Andrey Zhadchenko wrote:
>> When a cluster is unallocated but its L2 table exists, the seek code
>> walks it one cluster at a time: SEEK_DATA re-parses metadata for
>> every cluster of the range, and with a backing image each cluster
>> additionally costs a qio and completion allocation for the lower
>> delta descent.
>>
>> If the unmapped range reaches the cluster border, prolong it over the
>> following unallocated L2 entries: they are cached in the very md page
>> the current entry was parsed from, so this is just scanning for
>> non-zero words. Then the lower delta is scanned in one pass over the
>> whole range, and SEEK_DATA without a backing image skips it in one go.
>>
>> Entries with any subcluster or "reads as zeroes" bits set stop the
>> scan and spin parse_metadata() machinery.
>>
>> On a 16G image (128K clusters, extended L2) with all L2 tables
>> allocated but no clusters mapped over a backing image with data at
>> 15G, warm-cache SEEK_DATA improves from 44.8 ms to 0.4 ms.
>>
>> https://virtuozzo.atlassian.net/browse/VSTOR-139407
>> Feature: dm-qcow2: block device over QCOW2 files driver
>> Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
>> ---
>> ? drivers/md/dm-qcow2-map.c | 44 +++++++++++++++++++++++++++++++++++++--
>> ? 1 file changed, 42 insertions(+), 2 deletions(-)
>>
>> diff --git a/drivers/md/dm-qcow2-map.c b/drivers/md/dm-qcow2-map.c
>> index 6a73c32689eca..3bda108db725f 100644
>> --- a/drivers/md/dm-qcow2-map.c
>> +++ b/drivers/md/dm-qcow2-map.c
>> @@ -4441,6 +4441,34 @@ static inline void seek_qio_next_clu(struct qio
>> *qio, struct qcow2_map *map)
>> ????? seek_qio_set_sector(qio, bi_sector);
>> ? }
>> +/*
>> + * Manually scan L2 metadata pages until we hit something interesting.
>> + * We do not take md_pages_lock here because we have no promises about
>> + * concurrent writes anyway.
>> + */
>> +static loff_t seek_extend_unmapped_end(struct qio *qio, struct
>> qcow2_map *map,
>> +?????????????????????? loff_t end)
>> +{
>> +??? struct qcow2 *qcow2 = qio->qcow2;
>> +??? loff_t lim = SEEK_QIO_DATA(qio)->lim;
>> +??? u32 step = 1 + qcow2->ext_l2;
>> +??? u32 index = map->l2.index_in_page + step;
>> +??? u64 *entries;
>> +
>> +??? entries = kmap_local_page(map->l2.md->page);
>> +??? for (; index + step <= PAGE_SIZE / sizeof(u64) && end < lim;
>> +???????? index += step) {
>> +??????? /* Zero test does not need be64_to_cpu() */
>> +??????? if (READ_ONCE(entries[index]) ||
>> +??????????? (qcow2->ext_l2 && READ_ONCE(entries[index + 1])))
>> +??????????? break;
>> +??????? end += qcow2->clu_size;
>> +??? }
>> +??? kunmap_local(entries);
>> +
>> +??? return min_t(loff_t, end, lim);
>> +}
>> +
>> ? static struct qio *advance_and_spawn_lower_seek_qio(struct qio
>> *old_qio, loff_t end)
>> ? {
>> ????? struct qcow2 *lower = old_qio->qcow2->lower;
>> @@ -4503,12 +4531,15 @@ static int qcow2_llseek_hole_qio(struct qio
>> *qio, int whence, loff_t *result)
>> ????????????????? struct qio *new_qio;
>> ????????????????? loff_t end;
>> -??????????????? if (!(map.level & L2_LEVEL))
>> +??????????????? if (!(map.level & L2_LEVEL)) {
>> ????????????????????? end = min_t(loff_t,
>> ????????????????????????????? to_bytes(get_next_l2(qio)),
>> ????????????????????????????? SEEK_QIO_DATA(qio)->lim);
>> -??????????????? else
>> +??????????????? } else {
>> ????????????????????? end = to_bytes(qio->bi_iter.bi_sector) + size;
>> +??????????????????? if (!CLU_OFF(qio->qcow2, end))
>> +??????????????????????? end = seek_extend_unmapped_end(qio, &map, end);
>> +??????????????? }
>> ????????????????? new_qio = advance_and_spawn_lower_seek_qio(qio, end);
>> ????????????????? if (!new_qio) {
>> @@ -4552,6 +4583,15 @@ static int qcow2_llseek_hole_qio(struct qio
>> *qio, int whence, loff_t *result)
>> ????????????????? qio->bi_iter.bi_sector += to_sector(size);
>> ????????????????? qio->bi_iter.bi_size -= size;
>> ????????????????? goto calc_subclu;
>> +??????????? } else if (arg.unmapped && (map.level & L2_LEVEL)) {
>> +??????????????? loff_t end = to_bytes(qio->bi_iter.bi_sector) + size;
>> +
>> +??????????????? /* Skip the following unallocated entries in one go */
>> +??????????????? if (!CLU_OFF(qio->qcow2, end)) {
>> +??????????????????? seek_qio_set_sector(qio,
>
> shouldn't you wrap the result of seek_extend_unmapped_end in to_sector?
> It returns bytes and seek_qio_set_sector takes sector_t and we would
> immediately do to_bytes on something that is already bytes.
You are totally right. No idea how I missed that! Probably tests I
drafted into ploop do not cover that case.
>
>> +??????????????????????? seek_extend_unmapped_end(qio, &map, end));
>> +??????????????????? continue;
>> +??????????????? }
>> ????????????? }
>> ????????? }
>
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [Devel] [PATCH VZ10 0/6] dm-qcow: lseek improvements
2026-08-10 12:29 [Devel] [PATCH VZ10 0/6] dm-qcow: lseek improvements Andrey Zhadchenko
` (5 preceding siblings ...)
2026-08-10 12:30 ` [Devel] [PATCH VZ10 6/6] drivers/md/dm-qcow2: seek unallocated L2 entries in one pass within md Andrey Zhadchenko
@ 2026-08-14 11:55 ` Pavel Tikhomirov
6 siblings, 0 replies; 16+ messages in thread
From: Pavel Tikhomirov @ 2026-08-14 11:55 UTC (permalink / raw)
Looks reasonable, not gonna lie I don't fully understand last patch.
So except for specific comments to patches:
Reviewed-by: Pavel Tikhomirov <ptikhomirov@virtuozzo.com>
On 8/10/26 14:29, Andrey Zhadchenko wrote:
> This patchset is mostly about improving dm-qcow2 lseek speed. L2 md
> pages can now be scanned in batch with simple for() instead of heavy
> parse_metadata(). Seek qios for backing images are now called with
> a range bigger than 1 cluster if possible.
> Also fix a few clamping bugs and use correct comparison for seek
> modes.
>
> Andrey Zhadchenko (6):
> drivers/md/dm-qcow2: fix lower delta seek window clamp
> drivers/md/dm-qcow2: keep seek parse window within limit
> drivers/md/dm-qcow2: fix seek constant comparison
> drivers/md/dm-qcow2: fix seek skip to the next L2 table
> drivers/md/dm-qcow2: scan lower delta in one pass over empty L1 entry
> drivers/md/dm-qcow2: seek unallocated L2 entries in one pass within md
>
> drivers/md/dm-qcow2-map.c | 111 ++++++++++++++++++++++++++------------
> 1 file changed, 78 insertions(+), 33 deletions(-)
>
--
Best regards, Pavel Tikhomirov
Senior Software Developer, Virtuozzo.
^ permalink raw reply [flat|nested] 16+ messages in thread