• Douglas Anderson's avatar
    blk-mq: Rerun dispatching in the case of budget contention · a0823421
    Douglas Anderson authored
    If ever a thread running blk-mq code tries to get budget and fails it
    immediately stops doing work and assumes that whenever budget is freed
    up that queues will be kicked and whatever work the thread was trying
    to do will be tried again.
    
    One path where budget is freed and queues are kicked in the normal
    case can be seen in scsi_finish_command().  Specifically:
    - scsi_finish_command()
      - scsi_device_unbusy()
        - # Decrement "device_busy", AKA release budget
      - scsi_io_completion()
        - scsi_end_request()
          - blk_mq_run_hw_queues()
    
    The above is all well and good.  The problem comes up when a thread
    claims the budget but then releases it without actually dispatching
    any work.  Since we didn't schedule any work we'll never run the path
    of finishing work / kicking the queues.
    
    This isn't often actually a problem which is why this issue has
    existed for a while and nobody noticed.  Specifically we only get into
    this situation when we unexpectedly found that we weren't going to do
    any work.  Code that later receives new work kicks the queues.  All
    good, right?
    
    The problem shows up, however, if timing is just wrong and we hit a
    race.  To see this race let's think about the case where we only have
    a budget of 1 (only one thread can hold budget).  Now imagine that a
    thread got budget and then decided not to dispatch work.  It's about
    to call put_budget() but then the thread gets context switched out for
    a long, long time.  While in this state, any and all kicks of the
    queue (like the when we received new work) will be no-ops because
    nobody can get budget.  Finally the thread holding budget gets to run
    again and returns.  All the normal kicks will have been no-ops and we
    have an I/O stall.
    
    As you can see from the above, you need just the right timing to see
    the race.  To start with, the only case it happens if we thought we
    had work, actually managed to get the budget, but then actually didn't
    have work.  That's pretty rare to start with.  Even then, there's
    usually a very small amount of time between realizing that there's no
    work and putting the budget.  During this small amount of time new
    work has to come in and the queue kick has to make it all the way to
    trying to get the budget and fail.  It's pretty unlikely.
    
    One case where this could have failed is illustrated by an example of
    threads running blk_mq_do_dispatch_sched():
    
    * Threads A and B both run has_work() at the same time with the same
      "hctx".  Imagine has_work() is exact.  There's no lock, so it's OK
      if Thread A and B both get back true.
    * Thread B gets interrupted for a long time right after it decides
      that there is work.  Maybe its CPU gets an interrupt and the
      interrupt handler is slow.
    * Thread A runs, get budget, dispatches work.
    * Thread A's work finishes and budget is released.
    * Thread B finally runs again and gets budget.
    * Since Thread A already took care of the work and no new work has
      come in, Thread B will get NULL from dispatch_request().  I believe
      this is specifically why dispatch_request() is allowed to return
      NULL in the first place if has_work() must be exact.
    * Thread B will now be holding the budget and is about to call
      put_budget(), but hasn't called it yet.
    * Thread B gets interrupted for a long time (again).  Dang interrupts.
    * Now Thread C (maybe with a different "hctx" but the same queue)
      comes along and runs blk_mq_do_dispatch_sched().
    * Thread C won't do anything because it can't get budget.
    * Finally Thread B will run again and put the budget without kicking
      any queues.
    
    Even though the example above is with blk_mq_do_dispatch_sched() I
    believe the race is possible any time someone is holding budget but
    doesn't do work.
    
    Unfortunately, the unlikely has become more likely if you happen to be
    using the BFQ I/O scheduler.  BFQ, by design, sometimes returns "true"
    for has_work() but then NULL for dispatch_request() and stays in this
    state for a while (currently up to 9 ms).  Suddenly you only need one
    race to hit, not two races in a row.  With my current setup this is
    easy to reproduce in reboot tests and traces have actually shown that
    we hit a race similar to the one described above.
    
    Note that we only need to fix blk_mq_do_dispatch_sched() and
    blk_mq_do_dispatch_ctx() and not the other places that put budget.  In
    other cases we know that we have work to do on at least one "hctx" and
    code already exists to kick that "hctx"'s queue.  When that work
    finally finishes all the queues will be kicked using the normal flow.
    
    One last note is that (at least in the SCSI case) budget is shared by
    all "hctx"s that have the same queue.  Thus we need to make sure to
    kick the whole queue, not just re-run dispatching on a single "hctx".
    Signed-off-by: default avatarDouglas Anderson <dianders@chromium.org>
    Reviewed-by: default avatarMing Lei <ming.lei@redhat.com>
    Signed-off-by: default avatarJens Axboe <axboe@kernel.dk>
    a0823421
blk-mq-sched.c 16.3 KB