Re: [PATCH 2/2] scsi: core: Fix stall if two threads request budget at the same time

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



Hi,

On Mon, Mar 30, 2020 at 6:41 PM Ming Lei <ming.lei@xxxxxxxxxx> wrote:
>
> On Mon, Mar 30, 2020 at 07:49:06AM -0700, Douglas Anderson wrote:
> > It is possible for two threads to be running
> > blk_mq_do_dispatch_sched() at the same time with the same "hctx".
> > This is because there can be more than one caller to
> > __blk_mq_run_hw_queue() with the same "hctx" and hctx_lock() doesn't
> > prevent more than one thread from entering.
> >
> > If more than one thread is running blk_mq_do_dispatch_sched() at the
> > same time with the same "hctx", they may have contention acquiring
> > budget.  The blk_mq_get_dispatch_budget() can eventually translate
> > into scsi_mq_get_budget().  If the device's "queue_depth" is 1 (not
> > uncommon) then only one of the two threads will be the one to
> > increment "device_busy" to 1 and get the budget.
> >
> > The losing thread will break out of blk_mq_do_dispatch_sched() and
> > will stop dispatching requests.  The assumption is that when more
> > budget is available later (when existing transactions finish) the
> > queue will be kicked again, perhaps in scsi_end_request().
> >
> > The winning thread now has budget and can go on to call
> > dispatch_request().  If dispatch_request() returns NULL here then we
> > have a potential problem.  Specifically we'll now call
>
> I guess this problem should be BFQ specific. Now there is definitely
> requests in BFQ queue wrt. this hctx. However, looks this request is
> only available from another loser thread, and it won't be retrieved in
> the winning thread via e->type->ops.dispatch_request().
>
> Just wondering why BFQ is implemented in this way?

Paolo can maybe comment why.

...but even if BFQ wanted to try to change this, I think it's
impossible to fully close the race.  There is no locking between the
call to has_work() and dispatch_request() and there can be two (or
more) threads running the code at the same time.  Without some type of
locking I think it will always be possible for dispatch_request() to
return NULL.  Are we OK with code that works most of the time but
still has a race?  ...or did I misunderstand how this all works?


> > blk_mq_put_dispatch_budget() which translates into
> > scsi_mq_put_budget().  That will mark the device as no longer busy but
> > doesn't do anything to kick the queue.  This violates the assumption
> > that the queue would be kicked when more budget was available.
> >
> > Pictorially:
> >
> > Thread A                          Thread B
> > ================================= ==================================
> > blk_mq_get_dispatch_budget() => 1
> > dispatch_request() => NULL
> >                                   blk_mq_get_dispatch_budget() => 0
> >                                   // because Thread A marked
> >                                   // "device_busy" in scsi_device
> > blk_mq_put_dispatch_budget()
> >
> > The above case was observed in reboot tests and caused a task to hang
> > forever waiting for IO to complete.  Traces showed that in fact two
> > tasks were running blk_mq_do_dispatch_sched() at the same time with
> > the same "hctx".  The task that got the budget did in fact see
> > dispatch_request() return NULL.  Both tasks returned and the system
> > went on for several minutes (until the hung task delay kicked in)
> > without the given "hctx" showing up again in traces.
> >
> > Let's attempt to fix this problem by detecting budget contention.  If
> > we're in the SCSI code's put_budget() function and we saw that someone
> > else might have wanted the budget we got then we'll kick the queue.
> >
> > The mechanism of kicking due to budget contention has the potential to
> > overcompensate and kick the queue more than strictly necessary, but it
> > shouldn't hurt.
> >
> > Signed-off-by: Douglas Anderson <dianders@xxxxxxxxxxxx>
> > ---
> >
> >  drivers/scsi/scsi_lib.c    | 27 ++++++++++++++++++++++++---
> >  drivers/scsi/scsi_scan.c   |  1 +
> >  include/scsi/scsi_device.h |  2 ++
> >  3 files changed, 27 insertions(+), 3 deletions(-)
> >
> > diff --git a/drivers/scsi/scsi_lib.c b/drivers/scsi/scsi_lib.c
> > index 610ee41fa54c..0530da909995 100644
> > --- a/drivers/scsi/scsi_lib.c
> > +++ b/drivers/scsi/scsi_lib.c
> > @@ -344,6 +344,21 @@ static void scsi_dec_host_busy(struct Scsi_Host *shost, struct scsi_cmnd *cmd)
> >       rcu_read_unlock();
> >  }
> >
> > +static void scsi_device_dec_busy(struct scsi_device *sdev)
> > +{
> > +     bool was_contention;
> > +     unsigned long flags;
> > +
> > +     spin_lock_irqsave(&sdev->budget_lock, flags);
> > +     atomic_dec(&sdev->device_busy);
> > +     was_contention = sdev->budget_contention;
> > +     sdev->budget_contention = false;
> > +     spin_unlock_irqrestore(&sdev->budget_lock, flags);
> > +
> > +     if (was_contention)
> > +             blk_mq_run_hw_queues(sdev->request_queue, true);
> > +}
> > +
> >  void scsi_device_unbusy(struct scsi_device *sdev, struct scsi_cmnd *cmd)
> >  {
> >       struct Scsi_Host *shost = sdev->host;
> > @@ -354,7 +369,7 @@ void scsi_device_unbusy(struct scsi_device *sdev, struct scsi_cmnd *cmd)
> >       if (starget->can_queue > 0)
> >               atomic_dec(&starget->target_busy);
> >
> > -     atomic_dec(&sdev->device_busy);
> > +     scsi_device_dec_busy(sdev);
> >  }
> >
> >  static void scsi_kick_queue(struct request_queue *q)
> > @@ -1624,16 +1639,22 @@ static void scsi_mq_put_budget(struct blk_mq_hw_ctx *hctx)
> >       struct request_queue *q = hctx->queue;
> >       struct scsi_device *sdev = q->queuedata;
> >
> > -     atomic_dec(&sdev->device_busy);
> > +     scsi_device_dec_busy(sdev);
> >  }
> >
> >  static bool scsi_mq_get_budget(struct blk_mq_hw_ctx *hctx)
> >  {
> >       struct request_queue *q = hctx->queue;
> >       struct scsi_device *sdev = q->queuedata;
> > +     unsigned long flags;
> >
> > -     if (scsi_dev_queue_ready(q, sdev))
> > +     spin_lock_irqsave(&sdev->budget_lock, flags);
> > +     if (scsi_dev_queue_ready(q, sdev)) {
> > +             spin_unlock_irqrestore(&sdev->budget_lock, flags);
> >               return true;
> > +     }
> > +     sdev->budget_contention = true;
> > +     spin_unlock_irqrestore(&sdev->budget_lock, flags);
>
> No, it really hurts performance by adding one per-sdev spinlock in fast path,
> and we actually tried to kill the atomic variable of 'sdev->device_busy'
> for high performance HBA.

It might be slow, but correctness trumps speed, right?  I tried to do
this with a 2nd atomic and without the spinlock but I kept having a
hole one way or the other.  I ended up just trying to keep the
spinlock section as small as possible.

If you know of a way to get rid of the spinlock that still makes the
code correct, I'd be super interested!  :-)  I certainly won't claim
that it's impossible to do, only that I didn't manage to come up with
a way.

-Doug



[Index of Archives]     [Linux RAID]     [Linux SCSI]     [Linux ATA RAID]     [IDE]     [Linux Wireless]     [Linux Kernel]     [ATH6KL]     [Linux Bluetooth]     [Linux Netdev]     [Kernel Newbies]     [Security]     [Git]     [Netfilter]     [Bugtraq]     [Yosemite News]     [MIPS Linux]     [ARM Linux]     [Linux Security]     [Device Mapper]

  Powered by Linux