virtio Notifications and Virtqueue Kicks
A virtqueue is a shared-memory ring (see The Virtqueue and Vring Layout): the guest driver and the host backend both read and write it directly, so moving data across it is free of any trap. But the two sides still need to tell each other “there is new work” — and those signals are the expensive part. The guest-to-host signal is a kick (the spec calls it an available buffer notification), implemented as a single write to a device register that causes one VM exit; the host-to-guest signal is a used buffer notification, delivered as an interrupt. Because each exit and each interrupt costs thousands of CPU cycles, virtio’s entire efficiency story is notification suppression: machinery — the
flagsbits, and the more preciseVIRTIO_F_EVENT_IDXscheme with itsused_event/avail_eventfields — that lets each side tell the other “do not signal me yet”, so that bursts of buffers are batched behind a single kick or a single interrupt. This note traces that machinery in the Linux 6.12 long-term-support (LTS) kernel and the OASIS virtio 1.3 specification, down to the KVMioeventfd/irqfdplumbing that makes the signals cheap in the first place.
Mental Model: Producing Is Free, Signaling Is Not
The right way to think about a virtqueue is as two lock-free single-producer/single-consumer rings plus two doorbells. Putting a descriptor into the available ring is a plain memory write — no hypervisor involvement. The doorbell is the only thing that crosses the virtualization boundary, and crossing it is the whole cost. So the optimization target is not “make the doorbell faster” (though ioeventfd/irqfd do that too) but “ring the doorbell as rarely as possible while never missing real work.”
sequenceDiagram participant D as Guest Driver participant R as Vring (shared memory) participant H as Host Backend Note over D,H: Producing buffers is free; only the doorbells cross the boundary D->>R: add bufs, bump avail->idx (plain writes) D->>D: kick_prepare: does host want a kick? alt host suppressed kicks (NO_NOTIFY / avail_event not reached) D--xH: no kick — batched, saved a VM exit else host wants a kick D->>H: notify: write queue index to notify reg (1 VM exit) H->>H: ioeventfd fires — no userspace exit if vhost end H->>R: consume bufs, write used ring, bump used->idx H->>H: should I interrupt the guest? alt guest suppressed interrupts (NO_INTERRUPT / used_event not reached) H--xD: no interrupt — batched, saved an IRQ else guest wants an interrupt H->>D: used buffer notification (irqfd / MSI-X injects IRQ) end
The two doorbells and their suppression gates. What it shows: after publishing buffers the driver consults virtqueue_kick_prepare() and skips the kick entirely if the host has signalled it does not need one; symmetrically the host skips the interrupt if the guest has signalled it does not need one. The insight to take: the fast path of a busy virtio device rings neither doorbell most of the time — work is discovered by polling the shared ring, and notifications fire only at the edges of a burst.
The Two Notifications and Their Crude Suppression Flags
The virtio 1.3 spec (§2.7) names the two directions precisely (virtio 1.3 spec). An available buffer notification is sent by the driver to tell the device that there are new buffers in the available ring; the spec notes the method is bus-specific “but generally it can be expensive.” A used buffer notification is sent by the device to tell the driver that it has finished with buffers and written them to the used ring.
Before the event-index refinement, suppression was a single bit on each side. From include/uapi/linux/virtio_ring.h (v6.12 header):
/* The Host uses this in used->flags to advise the Guest: don't kick me when
* you add a buffer. It's unreliable, so it's simply an optimization. Guest
* will still kick if it's out of buffers. */
#define VRING_USED_F_NO_NOTIFY 1
/* The Guest uses this in avail->flags to advise the Host: don't interrupt me
* when you consume a buffer. It's unreliable, so it's simply an
* optimization. */
#define VRING_AVAIL_F_NO_INTERRUPT 1The asymmetry of where each flag lives is the part everyone gets backwards, so walk it carefully:
VRING_USED_F_NO_NOTIFYis set by the host in the used ring’sflagsfield. It says: “do not kick me.” The guest reads it before kicking. (The host owns the used ring, so it writes the flag there.)VRING_AVAIL_F_NO_INTERRUPTis set by the guest in the available ring’sflagsfield. It says: “do not interrupt me.” The host reads it before raising an interrupt. (The guest owns the available ring, so it writes the flag there.)
The comments stress these are unreliable — purely advisory optimizations. A side that misses the flag and signals anyway is merely wasteful, never incorrect; and the guest will still kick if it runs out of buffers, regardless of the flag, so the device can never be permanently starved. This “advisory, not authoritative” property is what lets the whole scheme race-safely batch.
The coarse flag has a flaw: it is all-or-nothing. With interrupts disabled the driver must poll to find completions, and it has no way to say “interrupt me again once you reach this specific point.” That is exactly what the event-index feature adds.
VIRTIO_F_EVENT_IDX: Suppression with a Threshold
VIRTIO_F_EVENT_IDX is feature bit 29 (include/uapi/linux/virtio_ring.h). When both sides negotiate it, the crude flags are ignored and replaced by two 16-bit threshold fields, laid out at the tail of the opposite ring (the comment in the header is explicit: the used event index lives at the end of the available ring, and vice versa):
#define VIRTIO_RING_F_EVENT_IDX 29
/* The Guest publishes the used index for which it expects an interrupt
* at the end of the avail ring. Host should ignore the avail->flags field. */
/* The Host publishes the avail index for which it expects a kick
* at the end of the used ring. Guest should ignore the used->flags field. */
#define vring_used_event(vr) ((vr)->avail->ring[(vr)->num])
#define vring_avail_event(vr) (*(__virtio16 *)&(vr)->used->ring[(vr)->num])So there are two threshold values:
used_eventis written by the guest (at the end of the available ring). It means: “device, do not interrupt me until you have published a used entry at indexused_event.” Per the spec §2.7.7.1, the driver usesused_eventto advise the device that notifications are unnecessary until the device writes that index into the used ring.avail_eventis written by the host (at the end of the used ring). It means: “driver, do not kick me until you have published an available entry at indexavail_event.” Per spec §2.7.10.1, the device usesavail_eventsymmetrically.
This is strictly more powerful than the flag: instead of “off” or “on,” each side names the exact future index at which it wants the next signal. The receiver can then keep consuming a whole burst and the producer signals exactly once, when the named index is crossed — not before, not after.
The decision formula, walked symbol by symbol
The “should I signal?” test is the same on both sides, captured in one helper from virtio_ring.h:
static inline int vring_need_event(__u16 event_idx, __u16 new_idx, __u16 old)
{
return (__u16)(new_idx - event_idx - 1) < (__u16)(new_idx - old);
}The arguments: event_idx is the threshold the other side published (it wants a signal once this index is produced); old is the index value before this batch of additions; new_idx is the index value after. All three are free-running 16-bit counters that wrap, which is why every subtraction is cast back to __u16 — the comparison is in modular 16-bit arithmetic.
Read the inequality as: “is the threshold event_idx inside the half-open window of indices we just produced, i.e. (old, new_idx]?” The left side (new_idx - event_idx - 1) is the distance from the threshold to the new index, minus one; the right side (new_idx - old) is how many entries we just added. If the threshold falls within the batch we just published, the difference on the left is smaller than the batch size on the right, the test is true, and we must signal. If the threshold is some index we have not reached yet, the left side is large (wraps near 0xFFFF) and the test is false — suppress. The header even cross-references Xen’s identical “notification hold-off” logic with req_event/req_prod, which is where the idea originated.
Where the driver actually runs this
On the kick side, virtqueue_kick_prepare_split() (in drivers/virtio/virtio_ring.c, v6.12 source) decides whether to ring the doorbell:
static bool virtqueue_kick_prepare_split(struct virtqueue *_vq)
{
struct vring_virtqueue *vq = to_vvq(_vq);
u16 new, old;
bool needs_kick;
START_USE(vq);
/* We need to expose available array entries before checking avail event. */
virtio_mb(vq->weak_barriers);
old = vq->split.avail_idx_shadow - vq->num_added;
new = vq->split.avail_idx_shadow;
vq->num_added = 0;
if (vq->event) {
needs_kick = vring_need_event(virtio16_to_cpu(_vq->vdev,
vring_avail_event(&vq->split.vring)), new, old);
} else {
needs_kick = !(vq->split.vring.used->flags &
cpu_to_virtio16(_vq->vdev, VRING_USED_F_NO_NOTIFY));
}
END_USE(vq);
return needs_kick;
}Walking it: vq->event is true when VIRTIO_F_EVENT_IDX was negotiated. If so, the code computes old (avail index before this batch — derived by subtracting num_added, the count added since the last kick) and new (the current avail index), then feeds them with the host-published avail_event into vring_need_event(). Only if the host’s threshold falls inside (old, new] does it return true. Otherwise — the event-index path — it falls back to the crude VRING_USED_F_NO_NOTIFY flag. The virtio_mb() full memory barrier before the check is essential: the driver must make the new descriptors and the new avail->idx visible to the host before it reads the host’s threshold, or it could decide to suppress a kick the host actually needed (the classic missed-wakeup race).
The interrupt side is symmetric. When the driver harvests a used buffer in virtqueue_get_buf, it republishes its used_event threshold via virtio_store_mb() so the device knows the next index at which to interrupt:
/* If we expect an interrupt for the next entry, tell host
* by writing event index and flush out the write before
* the read in the next get_buf call. */
if (!(vq->split.avail_flags_shadow & VRING_AVAIL_F_NO_INTERRUPT))
virtio_store_mb(vq->weak_barriers,
&vring_used_event(&vq->split.vring),
cpu_to_virtio16(_vq->vdev, vq->last_used_idx));The companion routines virtqueue_disable_cb_split() and virtqueue_enable_cb_prepare_split() toggle interrupts around a polling burst — exactly the dance a NAPI poll loop or virtblk_done() uses (see below). virtqueue_enable_cb_delayed() is a refinement that re-enables the interrupt only after roughly 3/4 of the in-flight buffers are consumed, deliberately adding hysteresis so a steady stream of completions never re-arms an interrupt that would immediately fire again.
How a Kick Becomes a Cheap VM Exit: ioeventfd
What does “ring the doorbell” physically do? On a virtio-pci device the notify path is a single register write. From drivers/virtio/virtio_pci_common.c:
bool vp_notify(struct virtqueue *vq)
{
/* we write the queue's selector into the notification register to
* signal the other end */
iowrite16(vq->index, (void __iomem *)vq->priv);
return true;
}That iowrite16 of the queue index to the device’s notification region (mapped via vp_modern_map_vq_notify() in modern virtio) is the kick. In a naive emulated device, that write is an MMIO/PIO access that traps with a VM exit all the way out to the userspace VMM — the slow path described in MMIO and Port IO Emulation. The whole point of KVM’s ioeventfd is to short-circuit that.
KVM_IOEVENTFD “attaches an ioeventfd to a legal pio/mmio address within the guest. A guest write in the registered address will signal the provided event instead of triggering an exit” (per the v6.12 KVM API). The VMM registers the virtqueue’s notify address with an eventfd (the addr, len, and fd fields; optionally KVM_IOEVENTFD_FLAG_DATAMATCH so only a write of a specific datamatch value signals, and KVM_IOEVENTFD_FLAG_PIO if the address is a port rather than memory). Now when the guest kicks, the CPU still takes a lightweight VM exit into KVM, but KVM resolves it in the kernel by simply signaling the eventfd — it never returns to userspace. An in-kernel vhost backend has its kernel thread waiting on that eventfd, so the kick wakes vhost directly. This turns the most expensive part of a kick (the round trip to and from the userspace VMM) into an in-kernel eventfd signal.
How a Used Notification Becomes a Cheap Interrupt: irqfd
The reverse doorbell — the device telling the guest “I finished buffers” — is a device interrupt that lands in vring_interrupt():
irqreturn_t vring_interrupt(int irq, void *_vq)
{
struct vring_virtqueue *vq = to_vvq(_vq);
if (!more_used(vq)) { /* spurious: nothing new in used ring */
pr_debug("virtqueue interrupt with no work for %p\n", vq);
return IRQ_NONE;
}
...
if (vq->event)
data_race(vq->event_triggered = true);
if (vq->vq.callback)
vq->vq.callback(&vq->vq); /* e.g. skb_recv_done / virtblk_done */
return IRQ_HANDLED;
}Note the first check: an interrupt that arrives with !more_used() is treated as spurious and returns IRQ_NONE — a direct consequence of suppression being advisory, since the device may interrupt slightly more than necessary. The event_triggered flag records that an interrupt already fired so virtqueue_disable_cb() can skip re-writing the suppression state.
Getting that interrupt into the guest cheaply is the job of irqfd (the KVM_IRQFD ioctl). It binds an eventfd to a guest interrupt line (a Global System Interrupt, GSI): signaling the eventfd injects the interrupt into the guest. So a vhost backend, having written the used ring, signals its “call” eventfd; KVM’s irqfd machinery injects the corresponding interrupt — and with MSI-X plus hardware APIC virtualization / posted interrupts, that injection can reach a running vCPU with no VM exit at all. The full mechanics of both eventfd-based shortcuts live in the dedicated irqfd and ioeventfd note; the point here is that virtio’s notifications are designed to map onto exactly these two eventfd doorbells.
A Worked Example: the Block Completion Loop
virtblk_done() in drivers/block/virtio_blk.c is the canonical place to see interrupt suppression batch completions:
do {
virtqueue_disable_cb(vq);
while ((vbr = virtqueue_get_buf(vblk->vqs[qid].vq, &len)) != NULL) {
struct request *req = blk_mq_rq_from_pdu(vbr);
if (likely(!blk_should_fake_timeout(req->q)))
blk_mq_complete_request(req);
req_done = true;
}
} while (!virtqueue_enable_cb(vq));The pattern is: disable callbacks, drain every available completion from the used ring, then call virtqueue_enable_cb() — which re-enables interrupts and re-checks the ring. If a completion arrived in the tiny window after the drain but before re-enabling, virtqueue_enable_cb() returns false and the loop runs again, closing the race without ever missing work. One hardware interrupt can thus retire dozens of block requests. The submission side mirrors this: virtio_queue_rq() only kicks when bd->last is set (the block layer’s signal that this is the final request in a batch), and virtio_commit_rqs() performs the deferred virtqueue_notify() — so a burst of submitted I/Os rings the kick doorbell once. virtio-net does the same with netdev_xmit_more(), deferring the transmit kick while the stack still has packets queued.
Failure Modes and Subtleties
Missing the memory barrier. Every suppression decision reads a value the other side wrote and acts on data this side wrote. Omit the barrier between publishing buffers and reading the peer’s event index and you get a missed notification: the producer suppresses a signal the consumer was waiting for, and the queue stalls until something else (a timeout, another request) breaks the deadlock. This is why virtqueue_kick_prepare_split() issues virtio_mb() and virtqueue_get_buf uses virtio_store_mb(). The weak_barriers flag selects SMP barriers (for a software backend sharing cache coherency) versus full DMA barriers (for hardware/vDPA backends).
Spurious interrupts are normal. Because suppression is advisory, the device may interrupt when there is nothing new; vring_interrupt() returning IRQ_NONE on !more_used() is the expected handling, not an error. A monitoring system that alarms on a nonzero spurious-interrupt count for virtio devices is misconfigured.
The “interrupt storm” without event-index. Without VIRTIO_F_EVENT_IDX, the only tool is the all-or-nothing NO_INTERRUPT flag, and re-enabling interrupts mid-burst can immediately re-fire — the classic interrupt storm under heavy I/O. virtqueue_enable_cb_delayed() and the event-index threshold both exist to damp this; a backend or driver that negotiates neither will burn CPU in interrupt entry/exit at high packet rates.
ioeventfd not wired up. If a VMM forgets to register the notify address with KVM_IOEVENTFD, kicks still work — they just take the full slow-path VM exit to userspace on every kick, silently costing throughput. The symptom is high kvm_exit MMIO counts (visible via perf kvm stat or the kvm:kvm_mmio tracepoint) with no functional failure — easy to miss.
Alternatives and Related Mechanisms
Notification suppression is the software answer to expensive doorbells; the hardware answers are complementary. Posted Interrupts and AVIC make the host-to-guest doorbell itself exit-free, so even when the device does interrupt, the cost collapses. Polling-mode backends invert the trade entirely: a Data Plane Development Kit (DPDK) or SPDK vhost-user backend disables notifications permanently and busy-polls the rings, trading a dedicated CPU core for zero notification latency — ideal at line rate, wasteful when idle. The packed virtqueue layout (VIRTIO_F_RING_PACKED, config bit 34) reorganizes the ring so available/used state lives in one descriptor table with per-descriptor avail/used wrap bits, which is more cache-friendly but keeps the same event-suppression semantics through struct vring_packed_desc_event (its off_wrap and flags fields, with VRING_PACKED_EVENT_FLAG_DESC enabling per-descriptor events) — virtqueue_kick_prepare_packed() runs the identical vring_need_event() math.
Production Notes
In a tuned production KVM stack the steady state is: vhost-net/vhost-blk serving the rings, ioeventfd turning kicks into in-kernel eventfd signals, irqfd + MSI-X + posted interrupts turning completions into exit-free interrupts, and VIRTIO_F_EVENT_IDX plus NAPI/enable_cb_delayed batching so that at high load the device runs nearly notification-free — buffers are discovered by the consumer’s poll loop, and a doorbell rings only at the leading and trailing edge of a burst. This is why a well-configured virtio NIC can push tens of gigabits with single-digit kicks-per-millisecond, and why the first thing to check on a slow VM is the exit and interrupt rate (perf kvm stat record, the kvm:* tracepoints): a virtio device that exits or interrupts per buffer has either failed to negotiate event-index, lost its ioeventfd/irqfd wiring, or is being served by a userspace backend that cannot keep up.
See Also
- The Virtqueue and Vring Layout — the available/used ring structure these notifications gate
- virtio Device Model — feature negotiation, including how
VIRTIO_F_EVENT_IDXis agreed - virtio-net and virtio-blk — the devices whose datapaths rely on this batching
- irqfd and ioeventfd — the KVM eventfd plumbing that makes both doorbells cheap
- vhost (In-Kernel virtio Backend) — the backend that waits on the ioeventfd and signals the irqfd
- Posted Interrupts / APIC Virtualization (APICv and AVIC) — hardware that removes the interrupt VM exit
- VM Exit Reasons and Handling — why a kick is a VM exit and what it costs
- Linux Virtualization MOC — the parent map; “eliminate the VM exit” is its central theme