diff mbox series

[3/3] block/stream: introduce a bottom node

Message ID 1553866154-257311-4-git-send-email-andrey.shinkevich@virtuozzo.com
State New
Headers show
Series block/stream: get rid of the base | expand

Commit Message

Andrey Shinkevich March 29, 2019, 1:29 p.m. UTC
The bottom node is the intermediate block device that has the base as its
backing image. It is used instead of the base node while a block stream
job is running to avoid dependency on the base that may change due to the
parallel jobs. The change may take place due to a filter node as well that
is inserted between the base and the intermediate bottom node. It occurs
when the base node is the top one for another commit or stream job.

Suggested-by: Vladimir Sementsov-Ogievskiy <vsementsov@virtuozzo.com>
Signed-off-by: Andrey Shinkevich <andrey.shinkevich@virtuozzo.com>
---
 block/stream.c            | 56 +++++++++++++++++++++++++----------------------
 block/trace-events        |  2 +-
 blockdev.c                | 10 ++++++++-
 include/block/block_int.h |  6 ++---
 tests/qemu-iotests/245    |  4 ++--
 5 files changed, 45 insertions(+), 33 deletions(-)

Comments

Alberto Garcia March 29, 2019, 4:07 p.m. UTC | #1
On Fri 29 Mar 2019 02:29:14 PM CET, Andrey Shinkevich wrote:
> @@ -3237,7 +3238,14 @@ void qmp_block_stream(bool has_job_id, const char *job_id, const char *device,
>          job_flags |= JOB_MANUAL_DISMISS;
>      }
>  
> -    stream_start(has_job_id ? job_id : NULL, bs, base_bs, base_name,
> +    /* Find the bottom node that has the base as its backing image */
> +    bottom_node = bs;
> +    while ((iter = backing_bs(bottom_node)) != base_bs) {
> +        bottom_node = iter;
> +    }
> +    assert(bottom_node);
> +
> +    stream_start(has_job_id ? job_id : NULL, bs, bottom_node, base_name,
>                   job_flags, has_speed ? speed : 0, on_error, &local_err);

Isn't it simpler to pass 'base' to stream_start() and find the bottom
node there? (with bdrv_find_overlay()).

I think bottom should be an internal implementation detail of the
block-stream driver, callers don't need to know about it, or do they?

Berto
Vladimir Sementsov-Ogievskiy March 29, 2019, 4:15 p.m. UTC | #2
29.03.2019 19:07, Alberto Garcia wrote:
> On Fri 29 Mar 2019 02:29:14 PM CET, Andrey Shinkevich wrote:
>> @@ -3237,7 +3238,14 @@ void qmp_block_stream(bool has_job_id, const char *job_id, const char *device,
>>           job_flags |= JOB_MANUAL_DISMISS;
>>       }
>>   
>> -    stream_start(has_job_id ? job_id : NULL, bs, base_bs, base_name,
>> +    /* Find the bottom node that has the base as its backing image */
>> +    bottom_node = bs;
>> +    while ((iter = backing_bs(bottom_node)) != base_bs) {
>> +        bottom_node = iter;
>> +    }
>> +    assert(bottom_node);
>> +
>> +    stream_start(has_job_id ? job_id : NULL, bs, bottom_node, base_name,
>>                    job_flags, has_speed ? speed : 0, on_error, &local_err);
> 
> Isn't it simpler to pass 'base' to stream_start() and find the bottom
> node there? (with bdrv_find_overlay()).
> 
> I think bottom should be an internal implementation detail of the
> block-stream driver, callers don't need to know about it, or do they?
> 

My idea is that we should get rid of base before any yield, and better do it as
soon as possible.
Better to get rid of it in qapi interface, but we can't do it due to backward
compatibility.
So, I'd prefer to not deal with base in block/stream.c code at all, and only
qmp_block_stream is responsible to recalculate it's parameters to something more
meaningful before any context switch.
Andrey Shinkevich March 29, 2019, 4:24 p.m. UTC | #3
On 29/03/2019 19:07, Alberto Garcia wrote:
> On Fri 29 Mar 2019 02:29:14 PM CET, Andrey Shinkevich wrote:
>> @@ -3237,7 +3238,14 @@ void qmp_block_stream(bool has_job_id, const char *job_id, const char *device,
>>           job_flags |= JOB_MANUAL_DISMISS;
>>       }
>>   
>> -    stream_start(has_job_id ? job_id : NULL, bs, base_bs, base_name,
>> +    /* Find the bottom node that has the base as its backing image */
>> +    bottom_node = bs;
>> +    while ((iter = backing_bs(bottom_node)) != base_bs) {
>> +        bottom_node = iter;
>> +    }
>> +    assert(bottom_node);
>> +
>> +    stream_start(has_job_id ? job_id : NULL, bs, bottom_node, base_name,
>>                    job_flags, has_speed ? speed : 0, on_error, &local_err);
> 
> Isn't it simpler to pass 'base' to stream_start() and find the bottom
> node there? (with bdrv_find_overlay()).
> 
> I think bottom should be an internal implementation detail of the
> block-stream driver, callers don't need to know about it, or do they?
> 
> Berto
> 

Thank you for the idea to use the bdrv_find_overlay() with the similar
functionality.
Vladimir Sementsov-Ogievskiy March 29, 2019, 4:44 p.m. UTC | #4
29.03.2019 16:29, Andrey Shinkevich wrote:
> The bottom node is the intermediate block device that has the base as its
> backing image. It is used instead of the base node while a block stream
> job is running to avoid dependency on the base that may change due to the
> parallel jobs. The change may take place due to a filter node as well that
> is inserted between the base and the intermediate bottom node. It occurs
> when the base node is the top one for another commit or stream job.

Good to mention here that don't freeze backing child of bottom from this patch
(and iotest change shows it)

> 
> Suggested-by: Vladimir Sementsov-Ogievskiy <vsementsov@virtuozzo.com>
> Signed-off-by: Andrey Shinkevich <andrey.shinkevich@virtuozzo.com>
> ---
>   block/stream.c            | 56 +++++++++++++++++++++++++----------------------
>   block/trace-events        |  2 +-
>   blockdev.c                | 10 ++++++++-
>   include/block/block_int.h |  6 ++---
>   tests/qemu-iotests/245    |  4 ++--
>   5 files changed, 45 insertions(+), 33 deletions(-)
> 
> diff --git a/block/stream.c b/block/stream.c
> index c065e99..d4d0737 100644
> --- a/block/stream.c
> +++ b/block/stream.c
> @@ -31,7 +31,7 @@ enum {
>   
>   typedef struct StreamBlockJob {
>       BlockJob common;
> -    BlockDriverState *base;
> +    BlockDriverState *bottom;
>       BlockdevOnError on_error;
>       char *backing_file_str;
>       bool bs_read_only;
> @@ -56,7 +56,7 @@ static void stream_abort(Job *job)
>   
>       if (s->chain_frozen) {
>           BlockJob *bjob = &s->common;
> -        bdrv_unfreeze_backing_chain(blk_bs(bjob->blk), s->base);
> +        bdrv_unfreeze_backing_chain(blk_bs(bjob->blk), s->bottom);
>       }
>   }
>   
> @@ -65,11 +65,11 @@ static int stream_prepare(Job *job)
>       StreamBlockJob *s = container_of(job, StreamBlockJob, common.job);
>       BlockJob *bjob = &s->common;
>       BlockDriverState *bs = blk_bs(bjob->blk);
> -    BlockDriverState *base = s->base;
> +    BlockDriverState *base = backing_bs(s->bottom);
>       Error *local_err = NULL;
>       int ret = 0;
>   
> -    bdrv_unfreeze_backing_chain(bs, base);
> +    bdrv_unfreeze_backing_chain(bs, s->bottom);
>       s->chain_frozen = false;
>   
>       if (bs->backing) {
> @@ -112,7 +112,7 @@ static int coroutine_fn stream_run(Job *job, Error **errp)
>       StreamBlockJob *s = container_of(job, StreamBlockJob, common.job);
>       BlockBackend *blk = s->common.blk;
>       BlockDriverState *bs = blk_bs(blk);
> -    BlockDriverState *base = s->base;
> +    bool enable_cor = !!backing_bs(s->bottom);

s/!!/!

>       int64_t len;
>       int64_t offset = 0;
>       uint64_t delay_ns = 0;
> @@ -125,6 +125,11 @@ static int coroutine_fn stream_run(Job *job, Error **errp)
>           return 0;
>       }
>   
> +    if (bs == s->bottom) {
> +        /* Nothing to stream */
> +        return 0;
> +    }

I think, with this "if" covers the previous (if (!bs->backing)), so the previous should be deleted.

> +
>       len = bdrv_getlength(bs);
>       if (len < 0) {
>           return len;
> @@ -138,7 +143,7 @@ static int coroutine_fn stream_run(Job *job, Error **errp)
>        * backing chain since the copy-on-read operation does not take base into
>        * account.
>        */
> -    if (!base) {
> +    if (!enable_cor) {

s/!//

>           bdrv_enable_copy_on_read(bs);
>       }
>   
> @@ -161,9 +166,8 @@ static int coroutine_fn stream_run(Job *job, Error **errp)
>           } else if (ret >= 0) {
>               /* Copy if allocated in the intermediate images.  Limit to the
>                * known-unallocated area [offset, offset+n*BDRV_SECTOR_SIZE).  */
> -            ret = bdrv_is_allocated_above(backing_bs(bs), base,
> -                                          offset, n, &n);
> -
> +            ret = bdrv_is_allocated_above_inclusive(backing_bs(bs), s->bottom,
> +                                                    offset, n, &n);
>               /* Finish early if end of backing file has been reached */
>               if (ret == 0 && n == 0) {
>                   n = len - offset;
> @@ -200,7 +204,7 @@ static int coroutine_fn stream_run(Job *job, Error **errp)
>           }
>       }
>   
> -    if (!base) {
> +    if (!enable_cor) {

s/!//

>           bdrv_disable_copy_on_read(bs);
>       }
>   
> @@ -225,13 +229,14 @@ static const BlockJobDriver stream_job_driver = {
>   };
>   

The rest looks good for me, just use bdrv_find_overlay instead of similar loop as Alberto suggested.
Alberto Garcia April 1, 2019, 3:44 p.m. UTC | #5
On Fri 29 Mar 2019 05:15:43 PM CET, Vladimir Sementsov-Ogievskiy wrote:
>>> @@ -3237,7 +3238,14 @@ void qmp_block_stream(bool has_job_id, const char *job_id, const char *device,
>>>           job_flags |= JOB_MANUAL_DISMISS;
>>>       }
>>>   
>>> -    stream_start(has_job_id ? job_id : NULL, bs, base_bs, base_name,
>>> +    /* Find the bottom node that has the base as its backing image */
>>> +    bottom_node = bs;
>>> +    while ((iter = backing_bs(bottom_node)) != base_bs) {
>>> +        bottom_node = iter;
>>> +    }
>>> +    assert(bottom_node);
>>> +
>>> +    stream_start(has_job_id ? job_id : NULL, bs, bottom_node, base_name,
>>>                    job_flags, has_speed ? speed : 0, on_error, &local_err);
>> 
>> Isn't it simpler to pass 'base' to stream_start() and find the bottom
>> node there? (with bdrv_find_overlay()).
>> 
>> I think bottom should be an internal implementation detail of the
>> block-stream driver, callers don't need to know about it, or do they?
>> 
> My idea is that we should get rid of base before any yield, and better
> do it as soon as possible.

But you can do it at the beginning of stream_start() without exposing
'bottom' to the callers which, again, is an implementation detail.

Berto
Andrey Shinkevich April 2, 2019, 8:51 a.m. UTC | #6
On 01/04/2019 18:44, Alberto Garcia wrote:
> On Fri 29 Mar 2019 05:15:43 PM CET, Vladimir Sementsov-Ogievskiy wrote:
>>>> @@ -3237,7 +3238,14 @@ void qmp_block_stream(bool has_job_id, const char *job_id, const char *device,
>>>>            job_flags |= JOB_MANUAL_DISMISS;
>>>>        }
>>>>    
>>>> -    stream_start(has_job_id ? job_id : NULL, bs, base_bs, base_name,
>>>> +    /* Find the bottom node that has the base as its backing image */
>>>> +    bottom_node = bs;
>>>> +    while ((iter = backing_bs(bottom_node)) != base_bs) {
>>>> +        bottom_node = iter;
>>>> +    }
>>>> +    assert(bottom_node);
>>>> +
>>>> +    stream_start(has_job_id ? job_id : NULL, bs, bottom_node, base_name,
>>>>                     job_flags, has_speed ? speed : 0, on_error, &local_err);
>>>
>>> Isn't it simpler to pass 'base' to stream_start() and find the bottom
>>> node there? (with bdrv_find_overlay()).
>>>
>>> I think bottom should be an internal implementation detail of the
>>> block-stream driver, callers don't need to know about it, or do they?
>>>
>> My idea is that we should get rid of base before any yield, and better
>> do it as soon as possible.
> 
> But you can do it at the beginning of stream_start() without exposing
> 'bottom' to the callers which, again, is an implementation detail.
> 
> Berto
> 

We have got a latent trouble with the base involved into the process.
For instance, if we encounter a filter while searching for the base,
we will want to manage it somehow. If the base is identified by the
node name rather than by file name, it would be easier. So, we would
assign the node name to the base earliest as possible. Another approach
suggested by Vladimir supposed to eliminate the base from the interface.
It is a sensitive change and we all need to come to an agreement.
Vladimir Sementsov-Ogievskiy April 2, 2019, 8:59 a.m. UTC | #7
02.04.2019 11:51, Andrey Shinkevich wrote:
> 
> 
> On 01/04/2019 18:44, Alberto Garcia wrote:
>> On Fri 29 Mar 2019 05:15:43 PM CET, Vladimir Sementsov-Ogievskiy wrote:
>>>>> @@ -3237,7 +3238,14 @@ void qmp_block_stream(bool has_job_id, const char *job_id, const char *device,
>>>>>             job_flags |= JOB_MANUAL_DISMISS;
>>>>>         }
>>>>>     
>>>>> -    stream_start(has_job_id ? job_id : NULL, bs, base_bs, base_name,
>>>>> +    /* Find the bottom node that has the base as its backing image */
>>>>> +    bottom_node = bs;
>>>>> +    while ((iter = backing_bs(bottom_node)) != base_bs) {
>>>>> +        bottom_node = iter;
>>>>> +    }
>>>>> +    assert(bottom_node);
>>>>> +
>>>>> +    stream_start(has_job_id ? job_id : NULL, bs, bottom_node, base_name,
>>>>>                      job_flags, has_speed ? speed : 0, on_error, &local_err);
>>>>
>>>> Isn't it simpler to pass 'base' to stream_start() and find the bottom
>>>> node there? (with bdrv_find_overlay()).
>>>>
>>>> I think bottom should be an internal implementation detail of the
>>>> block-stream driver, callers don't need to know about it, or do they?
>>>>
>>> My idea is that we should get rid of base before any yield, and better
>>> do it as soon as possible.
>>
>> But you can do it at the beginning of stream_start() without exposing
>> 'bottom' to the callers which, again, is an implementation detail.
>>
>> Berto
>>
> 
> We have got a latent trouble with the base involved into the process.
> For instance, if we encounter a filter while searching for the base,
> we will want to manage it somehow. If the base is identified by the
> node name rather than by file name, it would be easier. So, we would
> assign the node name to the base earliest as possible. Another approach
> suggested by Vladimir supposed to eliminate the base from the interface.
> It is a sensitive change and we all need to come to an agreement.
> 


On the other hand we freeze backing chain in stream_start anyway. So, we'd like
to be sure anyway, that there are no yields before stream_start() call.
Alberto Garcia April 2, 2019, 2:42 p.m. UTC | #8
On Tue 02 Apr 2019 10:51:06 AM CEST, Andrey Shinkevich wrote:
>>>>> @@ -3237,7 +3238,14 @@ void qmp_block_stream(bool has_job_id, const char *job_id, const char *device,
>>>>>            job_flags |= JOB_MANUAL_DISMISS;
>>>>>        }
>>>>>    
>>>>> -    stream_start(has_job_id ? job_id : NULL, bs, base_bs, base_name,
>>>>> +    /* Find the bottom node that has the base as its backing image */
>>>>> +    bottom_node = bs;
>>>>> +    while ((iter = backing_bs(bottom_node)) != base_bs) {
>>>>> +        bottom_node = iter;
>>>>> +    }
>>>>> +    assert(bottom_node);
>>>>> +
>>>>> +    stream_start(has_job_id ? job_id : NULL, bs, bottom_node, base_name,
>>>>>                     job_flags, has_speed ? speed : 0, on_error, &local_err);
>>>>
>>>> Isn't it simpler to pass 'base' to stream_start() and find the
>>>> bottom node there? (with bdrv_find_overlay()).
>>>>
>>>> I think bottom should be an internal implementation detail of the
>>>> block-stream driver, callers don't need to know about it, or do
>>>> they?
>>>>
>>> My idea is that we should get rid of base before any yield, and
>>> better do it as soon as possible.
>> 
>> But you can do it at the beginning of stream_start() without exposing
>> 'bottom' to the callers which, again, is an implementation detail.
>
> We have got a latent trouble with the base involved into the process.
> For instance, if we encounter a filter while searching for the base,
> we will want to manage it somehow. If the base is identified by the
> node name rather than by file name, it would be easier. So, we would
> assign the node name to the base earliest as possible.

There's already the 'base-node' parameter, and converting 'base' into
'base-node' is already done early on by qmp_block_stream() so it doesn't
make much difference whether the user passed the former or the latter.

> Another approach suggested by Vladimir supposed to eliminate the base
> from the interface.  It is a sensitive change and we all need to come
> to an agreement.

Well, replacing 'base' with 'bottom' on the public API is a completely
different thing and I'm not sure if it's a good idea.

'base' is used to decide the new backing image after block-stream
completes, and its semantics are quite clear. But that does not mean
that 'base' must be used throughout the implementation of block-stream.

If it turns out that block-stream is best implemented using a 'bottom'
node istead of 'base' then the first thing that stream_start() can do is
calculate 'bottom' and discard 'base'.

Whether you do that at the beginning of stream_start() or at the end of
qmp_block_stream() does not make any difference in practice, the end
result is going to be the same.

The only reason why I'm proposing this is because I don't think 'base'
needs to be removed from the public API and therefore I see 'bottom' as
simply an implementation detail that doesn't need to be exposed outside
stream.c

Berto
Andrey Shinkevich April 3, 2019, 8:29 a.m. UTC | #9
On 29/03/2019 19:07, Alberto Garcia wrote:
> On Fri 29 Mar 2019 02:29:14 PM CET, Andrey Shinkevich wrote:
>> @@ -3237,7 +3238,14 @@ void qmp_block_stream(bool has_job_id, const char *job_id, const char *device,
>>           job_flags |= JOB_MANUAL_DISMISS;
>>       }
>>   
>> -    stream_start(has_job_id ? job_id : NULL, bs, base_bs, base_name,
>> +    /* Find the bottom node that has the base as its backing image */
>> +    bottom_node = bs;
>> +    while ((iter = backing_bs(bottom_node)) != base_bs) {
>> +        bottom_node = iter;
>> +    }
>> +    assert(bottom_node);
>> +
>> +    stream_start(has_job_id ? job_id : NULL, bs, bottom_node, base_name,
>>                    job_flags, has_speed ? speed : 0, on_error, &local_err);
> 
> Isn't it simpler to pass 'base' to stream_start() and find the bottom
> node there? (with bdrv_find_overlay()).
> 

I am going to move the bottom = bdrv_find_overlay() into the
stream_start() in v3, which is coming soon.

> I think bottom should be an internal implementation detail of the
> block-stream driver, callers don't need to know about it, or do they?
> 
> Berto
>
diff mbox series

Patch

diff --git a/block/stream.c b/block/stream.c
index c065e99..d4d0737 100644
--- a/block/stream.c
+++ b/block/stream.c
@@ -31,7 +31,7 @@  enum {
 
 typedef struct StreamBlockJob {
     BlockJob common;
-    BlockDriverState *base;
+    BlockDriverState *bottom;
     BlockdevOnError on_error;
     char *backing_file_str;
     bool bs_read_only;
@@ -56,7 +56,7 @@  static void stream_abort(Job *job)
 
     if (s->chain_frozen) {
         BlockJob *bjob = &s->common;
-        bdrv_unfreeze_backing_chain(blk_bs(bjob->blk), s->base);
+        bdrv_unfreeze_backing_chain(blk_bs(bjob->blk), s->bottom);
     }
 }
 
@@ -65,11 +65,11 @@  static int stream_prepare(Job *job)
     StreamBlockJob *s = container_of(job, StreamBlockJob, common.job);
     BlockJob *bjob = &s->common;
     BlockDriverState *bs = blk_bs(bjob->blk);
-    BlockDriverState *base = s->base;
+    BlockDriverState *base = backing_bs(s->bottom);
     Error *local_err = NULL;
     int ret = 0;
 
-    bdrv_unfreeze_backing_chain(bs, base);
+    bdrv_unfreeze_backing_chain(bs, s->bottom);
     s->chain_frozen = false;
 
     if (bs->backing) {
@@ -112,7 +112,7 @@  static int coroutine_fn stream_run(Job *job, Error **errp)
     StreamBlockJob *s = container_of(job, StreamBlockJob, common.job);
     BlockBackend *blk = s->common.blk;
     BlockDriverState *bs = blk_bs(blk);
-    BlockDriverState *base = s->base;
+    bool enable_cor = !!backing_bs(s->bottom);
     int64_t len;
     int64_t offset = 0;
     uint64_t delay_ns = 0;
@@ -125,6 +125,11 @@  static int coroutine_fn stream_run(Job *job, Error **errp)
         return 0;
     }
 
+    if (bs == s->bottom) {
+        /* Nothing to stream */
+        return 0;
+    }
+
     len = bdrv_getlength(bs);
     if (len < 0) {
         return len;
@@ -138,7 +143,7 @@  static int coroutine_fn stream_run(Job *job, Error **errp)
      * backing chain since the copy-on-read operation does not take base into
      * account.
      */
-    if (!base) {
+    if (!enable_cor) {
         bdrv_enable_copy_on_read(bs);
     }
 
@@ -161,9 +166,8 @@  static int coroutine_fn stream_run(Job *job, Error **errp)
         } else if (ret >= 0) {
             /* Copy if allocated in the intermediate images.  Limit to the
              * known-unallocated area [offset, offset+n*BDRV_SECTOR_SIZE).  */
-            ret = bdrv_is_allocated_above(backing_bs(bs), base,
-                                          offset, n, &n);
-
+            ret = bdrv_is_allocated_above_inclusive(backing_bs(bs), s->bottom,
+                                                    offset, n, &n);
             /* Finish early if end of backing file has been reached */
             if (ret == 0 && n == 0) {
                 n = len - offset;
@@ -200,7 +204,7 @@  static int coroutine_fn stream_run(Job *job, Error **errp)
         }
     }
 
-    if (!base) {
+    if (!enable_cor) {
         bdrv_disable_copy_on_read(bs);
     }
 
@@ -225,13 +229,14 @@  static const BlockJobDriver stream_job_driver = {
 };
 
 void stream_start(const char *job_id, BlockDriverState *bs,
-                  BlockDriverState *base, const char *backing_file_str,
+                  BlockDriverState *bottom, const char *backing_file_str,
                   int creation_flags, int64_t speed,
                   BlockdevOnError on_error, Error **errp)
 {
     StreamBlockJob *s;
     BlockDriverState *iter;
     bool bs_read_only;
+    int basic_flags = BLK_PERM_CONSISTENT_READ | BLK_PERM_WRITE_UNCHANGED;
 
     /* Make sure that the image is opened in read-write mode */
     bs_read_only = bdrv_is_read_only(bs);
@@ -245,37 +250,36 @@  void stream_start(const char *job_id, BlockDriverState *bs,
      * already have our own plans. Also don't allow resize as the image size is
      * queried only at the job start and then cached. */
     s = block_job_create(job_id, &stream_job_driver, NULL, bs,
-                         BLK_PERM_CONSISTENT_READ | BLK_PERM_WRITE_UNCHANGED |
-                         BLK_PERM_GRAPH_MOD,
-                         BLK_PERM_CONSISTENT_READ | BLK_PERM_WRITE_UNCHANGED |
-                         BLK_PERM_WRITE,
+                         basic_flags | BLK_PERM_GRAPH_MOD,
+                         basic_flags | BLK_PERM_WRITE,
                          speed, creation_flags, NULL, NULL, errp);
     if (!s) {
         goto fail;
     }
 
-    /* Block all intermediate nodes between bs and base, because they will
-     * disappear from the chain after this operation. The streaming job reads
-     * every block only once, assuming that it doesn't change, so block writes
-     * and resizes. */
-    for (iter = backing_bs(bs); iter && iter != base; iter = backing_bs(iter)) {
-        block_job_add_bdrv(&s->common, "intermediate node", iter, 0,
-                           BLK_PERM_CONSISTENT_READ | BLK_PERM_WRITE_UNCHANGED,
-                           &error_abort);
+    /*
+     * Block all intermediate nodes between bs and bottom (inclusive), because
+     * they will disappear from the chain after this operation. The streaming
+     * job reads every block only once, assuming that it doesn't change, so
+     * forbid writes and resizes.
+     */
+    for (iter = bs; iter != bottom; iter = backing_bs(iter)) {
+        block_job_add_bdrv(&s->common, "intermediate node", backing_bs(iter),
+                           0, basic_flags, &error_abort);
     }
 
-    if (bdrv_freeze_backing_chain(bs, base, errp) < 0) {
+    if (bdrv_freeze_backing_chain(bs, bottom, errp) < 0) {
         job_early_fail(&s->common.job);
         goto fail;
     }
 
-    s->base = base;
+    s->bottom = bottom;
     s->backing_file_str = g_strdup(backing_file_str);
     s->bs_read_only = bs_read_only;
     s->chain_frozen = true;
 
     s->on_error = on_error;
-    trace_stream_start(bs, base, s);
+    trace_stream_start(bs, bottom, s);
     job_start(&s->common.job);
     return;
 
diff --git a/block/trace-events b/block/trace-events
index e6bb5a8..36e7b79 100644
--- a/block/trace-events
+++ b/block/trace-events
@@ -20,7 +20,7 @@  bdrv_co_copy_range_to(void *src, uint64_t src_offset, void *dst, uint64_t dst_of
 
 # stream.c
 stream_one_iteration(void *s, int64_t offset, uint64_t bytes, int is_allocated) "s %p offset %" PRId64 " bytes %" PRIu64 " is_allocated %d"
-stream_start(void *bs, void *base, void *s) "bs %p base %p s %p"
+stream_start(void *bs, void *bottom, void *s) "bs %p bottom %p s %p"
 
 # commit.c
 commit_one_iteration(void *s, int64_t offset, uint64_t bytes, int is_allocated) "s %p offset %" PRId64 " bytes %" PRIu64 " is_allocated %d"
diff --git a/blockdev.c b/blockdev.c
index 4775a07..bc84312 100644
--- a/blockdev.c
+++ b/blockdev.c
@@ -3164,6 +3164,7 @@  void qmp_block_stream(bool has_job_id, const char *job_id, const char *device,
 {
     BlockDriverState *bs, *iter;
     BlockDriverState *base_bs = NULL;
+    BlockDriverState *bottom_node = NULL;
     AioContext *aio_context;
     Error *local_err = NULL;
     const char *base_name = NULL;
@@ -3237,7 +3238,14 @@  void qmp_block_stream(bool has_job_id, const char *job_id, const char *device,
         job_flags |= JOB_MANUAL_DISMISS;
     }
 
-    stream_start(has_job_id ? job_id : NULL, bs, base_bs, base_name,
+    /* Find the bottom node that has the base as its backing image */
+    bottom_node = bs;
+    while ((iter = backing_bs(bottom_node)) != base_bs) {
+        bottom_node = iter;
+    }
+    assert(bottom_node);
+
+    stream_start(has_job_id ? job_id : NULL, bs, bottom_node, base_name,
                  job_flags, has_speed ? speed : 0, on_error, &local_err);
     if (local_err) {
         error_propagate(errp, local_err);
diff --git a/include/block/block_int.h b/include/block/block_int.h
index 01e855a..8ab1144 100644
--- a/include/block/block_int.h
+++ b/include/block/block_int.h
@@ -1019,8 +1019,8 @@  int is_windows_drive(const char *filename);
  * @job_id: The id of the newly-created job, or %NULL to use the
  * device name of @bs.
  * @bs: Block device to operate on.
- * @base: Block device that will become the new base, or %NULL to
- * flatten the whole backing file chain onto @bs.
+ * @bottom_node: The intermediate block device right above the new base.
+ * If base is %NULL, the whole backing file chain is flattened onto @bs.
  * @backing_file_str: The file name that will be written to @bs as the
  * the new backing file if the job completes. Ignored if @base is %NULL.
  * @creation_flags: Flags that control the behavior of the Job lifetime.
@@ -1037,7 +1037,7 @@  int is_windows_drive(const char *filename);
  * BlockDriverState.
  */
 void stream_start(const char *job_id, BlockDriverState *bs,
-                  BlockDriverState *base, const char *backing_file_str,
+                  BlockDriverState *bottom_node, const char *backing_file_str,
                   int creation_flags, int64_t speed,
                   BlockdevOnError on_error, Error **errp);
 
diff --git a/tests/qemu-iotests/245 b/tests/qemu-iotests/245
index 7891a21..d11e73c 100644
--- a/tests/qemu-iotests/245
+++ b/tests/qemu-iotests/245
@@ -859,9 +859,9 @@  class TestBlockdevReopen(iotests.QMPTestCase):
                              device = 'hd0', base_node = 'hd2', speed = 512 * 1024)
         self.assert_qmp(result, 'return', {})
 
-        # We can't remove hd2 while the stream job is ongoing
+        # We can remove hd2 while the stream job is ongoing
         opts['backing']['backing'] = None
-        self.reopen(opts, {}, "Cannot change 'backing' link from 'hd1' to 'hd2'")
+        self.reopen(opts, {})
 
         # We can't remove hd1 while the stream job is ongoing
         opts['backing'] = None