Skip to content

virtio-blk: handle completions on the queue's dedicated thread - #1466

Open
wkozaczuk wants to merge 1 commit into
cloudius-systems:masterfrom
wkozaczuk:virtio_blk_mq_threads
Open

virtio-blk: handle completions on the queue's dedicated thread#1466
wkozaczuk wants to merge 1 commit into
cloudius-systems:masterfrom
wkozaczuk:virtio_blk_mq_threads

Conversation

@wkozaczuk

Copy link
Copy Markdown
Collaborator

This changes virtio-blk driver to create as many completion threads as there are virt queues. So if there are more than a single queue, interrupts delivered are serviced by its own dedicated queue instead of a single shared one. The idea is to minimize the number of IPIs because hopefully a submission thread would be woken by a completion thread running on the same cpu.

This also reverts some of the changes made to have a single completion thread handle all queues.

Finally, this modifies run.py to explicitly set the number of virt queues to 1 as advertised instead of defaulting to the number of cpus.

It turns out that multiple queues (>1) do not really lead to higher throughput, as these results demonstrate:

./scripts/build fs=rofs image=fio

./scripts/run.py -e '/fio --thread --name=fiotest --filename=/dev/vblk1 --numjobs=4 --rw=read --size=128M --cpus_allowed_policy=split --cpus_allowed=0-3' --second-disk=./disk2.img //Read test, run 3 times

./scripts/run.py -e '/fio --thread --name=fiotest --filename=/dev/vblk1 --numjobs=4 --rw=write --size=128M --cpus_allowed_policy=split --cpus_allowed=0-3' --second-disk=./disk2.img //Write test, run 3 times

### Before introducing multiple queues - single queue, single completion thread
READ: bw=577MiB/s (605MB/s), 144MiB/s-145MiB/s (151MB/s-153MB/s), io=512MiB (537MB), run=880-887msec
READ: bw=576MiB/s (604MB/s), 144MiB/s-145MiB/s (151MB/s-153MB/s), io=512MiB (537MB), run=880-889msec
READ: bw=547MiB/s (574MB/s), 137MiB/s-139MiB/s (143MB/s-146MB/s), io=512MiB (537MB), run=922-936msec

WRITE: bw=511MiB/s (536MB/s), 128MiB/s-128MiB/s (134MB/s-134MB/s), io=512MiB (537MB), run=1002-1002msec
WRITE: bw=491MiB/s (515MB/s), 123MiB/s-123MiB/s (129MB/s-129MB/s), io=512MiB (537MB), run=1043-1043msec
WRITE: bw=511MiB/s (536MB/s), 128MiB/s-128MiB/s (134MB/s-134MB/s), io=512MiB (537MB), run=1002-1002msec

### As of master before this PR - 4 queues, single completion thread, no lock in `req_done()`
READ: bw=459MiB/s (481MB/s), 115MiB/s-116MiB/s (120MB/s-121MB/s), io=512MiB (537MB), run=1106-1116msec
READ: bw=459MiB/s (481MB/s), 115MiB/s-116MiB/s (120MB/s-122MB/s), io=512MiB (537MB), run=1101-1115msec
READ: bw=463MiB/s (485MB/s), 116MiB/s-116MiB/s (121MB/s-122MB/s), io=512MiB (537MB), run=1100-1107msec

WRITE: bw=454MiB/s (476MB/s), 114MiB/s-114MiB/s (119MB/s-120MB/s), io=512MiB (537MB), run=1119-1127msec
WRITE: bw=457MiB/s (479MB/s), 114MiB/s-116MiB/s (120MB/s-122MB/s), io=512MiB (537MB), run=1100-1121msec
WRITE: bw=444MiB/s (465MB/s), 111MiB/s-113MiB/s (116MB/s-119MB/s), io=512MiB (537MB), run=1129-1154msec

### With this patch - by default (see `run.py` change), single queue, single completion queue
READ: bw=560MiB/s (587MB/s), 140MiB/s-142MiB/s (147MB/s-149MB/s), io=512MiB (537MB), run=902-915msec
READ: bw=539MiB/s (565MB/s), 135MiB/s-136MiB/s (141MB/s-142MB/s), io=512MiB (537MB), run=942-950msec
READ: bw=558MiB/s (585MB/s), 140MiB/s-142MiB/s (146MB/s-148MB/s), io=512MiB (537MB), run=904-917msec

WRITE: bw=494MiB/s (518MB/s), 123MiB/s-124MiB/s (129MB/s-130MB/s), io=512MiB (537MB), run=1035-1037msec
WRITE: bw=514MiB/s (538MB/s), 128MiB/s-129MiB/s (135MB/s-135MB/s), io=512MiB (537MB), run=996-997msec
WRITE: bw=501MiB/s (526MB/s), 125MiB/s-126MiB/s (131MB/s-132MB/s), io=512MiB (537MB), run=1016-1021msec

### With this patch - 4 queues, 4 dedicated completion queues
READ: bw=464MiB/s (487MB/s), 116MiB/s-126MiB/s (122MB/s-132MB/s), io=512MiB (537MB), run=1017-1103msec
READ: bw=468MiB/s (490MB/s), 117MiB/s-123MiB/s (123MB/s-129MB/s), io=512MiB (537MB), run=1038-1095msec
READ: bw=441MiB/s (462MB/s), 110MiB/s-125MiB/s (116MB/s-131MB/s), io=512MiB (537MB), run=1022-1162msec

WRITE: bw=343MiB/s (359MB/s), 85.7MiB/s-126MiB/s (89.8MB/s-132MB/s), io=512MiB (537MB), run=1014-1494msec
WRITE: bw=505MiB/s (529MB/s), 126MiB/s-138MiB/s (132MB/s-145MB/s), io=512MiB (537MB), run=925-1014msec
WRITE: bw=365MiB/s (383MB/s), 91.2MiB/s-127MiB/s (95.7MB/s-133MB/s), io=512MiB (537MB), run=1007-1403msec

This changes virtio-blk driver to create as many completion threads
as there are virt queues. So if there are more than single queue,
interrupts delivered are serviced by its own dedicated queue
instead of a single shared one. The idea is to minimize number of
IPIs because hopefully a submission thread would be woken by a
completion thread running on the same cpu.

This also reverts some of the changes made to have single completion
thread handle all queues.

Finally, this modifies `run.py` to explicitly set number of virt queues
to 1 as advertised instead of defulting to number of cpus.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors OSv’s virtio-blk multiqueue completion handling to use one completion thread per virtqueue (instead of a single shared completion thread), and updates the run script/documentation to align with the new behavior and the desired default queue count.

Changes:

  • Create and pin one virtio-blk completion thread per queue, and route MSI-X queue interrupts to the corresponding thread.
  • Rework completion draining logic to be per-queue (req_done(int qid)) using virtio_driver::wait_for_queue().
  • Update scripts/run.py to always pass an explicit num-queues to QEMU (defaulting to 1) and refresh multiqueue documentation.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
scripts/run.py Always emits an explicit num-queues=... for virtio-blk devices (default 1).
drivers/virtio-blk.hh Updates completion handler API to be per-queue (req_done(int qid)).
drivers/virtio-blk.cc Implements per-queue completion threads and MSI-X bindings; updates completion draining logic.
documentation/block-multiqueue.md Updates multiqueue design/behavior documentation to reflect the new completion model.
Suppressed comments (3)

drivers/virtio-blk.cc:217

  • On the MMIO (aarch64 SPI) path, the interrupt handler always wakes only the first completion thread. If multiple queues are active, other queues’ completions will not be processed. Consider waking all per-queue completion threads (and disabling interrupts on all queues) when _num_queues > 1.
    int_factory.create_spi_edge_interrupt = [this,single_thread]() {
        return new spi_interrupt(
            gic::irq_type::IRQ_TYPE_EDGE,
            _dev.get_irq(),
            [=] { return this->ack_irq(); },
            [=] { single_thread->wake_with_irq_disabled(); });

drivers/virtio-blk.cc:223

  • On the MMIO (GSI) path, only the first completion thread is woken. If multiple queues are active, completions on queues >0 will not be drained and I/O may hang. Consider disabling interrupts for all queues and waking all completion threads when _num_queues > 1.
    int_factory.create_gsi_edge_interrupt = [this,single_thread]() {
        return new gsi_edge_interrupt(
            _dev.get_irq(),
            [=] { if (this->ack_irq()) single_thread->wake_with_irq_disabled(); });

documentation/block-multiqueue.md:89

  • This paragraph has a stray closing parenthesis and its description of the non-MSI-X/MMIO completion path doesn’t match the current implementation (there’s no longer an “all-queue drain” thread). Please update the wording to reflect the actual behavior.
So all queues (vrings) and their MSI-X vectors are fully used; each queue
is serviced by its dedicated consumer thread, submission is lock-per-queue
and runs on the owning CPU). On the non-MSI-X / MMIO path a single shared
IRQ line fans into the same single-queue drain.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread drivers/virtio-blk.cc
_num_queues = virtio_driver::_num_queues;
// base class did not set up. Also, there is no point of creating more queues
// than number of cpus, as make_request() would never submit requests there.
_num_queues = std::min<u32>(virtio_driver::_num_queues, sched::cpus.size());
Comment thread scripts/run.py
Comment on lines +702 to +703
# Add multiqueue support for virtio-blk
cmdargs.virtio_blk_queue_suffix = ",num-queues=%d" % cmdargs.virtio_blk_queues
Comment thread drivers/virtio-blk.cc
Comment on lines +157 to +159
// It only makes sense and is actuall necessary to pin the consumer
// threads when there are more than one queue so that each is serviced
// on different cpu to make it thread-safe
Comment thread drivers/virtio-blk.cc
auto* q = get_virt_queue(qid);
bindings.push_back({ (unsigned)qid, [q] { q->disable_interrupts(); }, t });
auto* t = threads[qid];
bindings.push_back({ (unsigned)qid, [q,t] { q->disable_interrupts(); }, t });
Comment thread drivers/virtio-blk.cc
Comment on lines +202 to +206
int_factory.create_pci_interrupt = [this,single_thread](pci::device &pci_dev) {
return new pci_interrupt(
pci_dev,
[=] { return this->ack_irq(); },
[=] { t->wake_with_irq_disabled(); });
[=] { single_thread->wake_with_irq_disabled(); });
Comment thread drivers/virtio-blk.cc
Comment on lines +308 to +312
* wakes its thread. It sleeps until this queue's used ring is non-empty,
* then drains it. No lock needs to be held while the queue is drained,
* as the req_done() for each queue is handled on different cpu, and also
* is thread-safe when interacting with producer threads calling make_request()
* because it races only on the lock-free _used_ring_host_head counter.
Comment on lines +65 to 66
completions (re-arming interrupts on thus queue and re-checking before it
sleeps, so a completion arriving during the check is never missed), and when
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants