From: Yufeng Wang <[email protected]>
Implement SQ/CQ doorbell polling in the guest kernel:
- virtio_ring.c: SQ/CQ DMA allocation, virtqueue_notify() writes
sq->idx via smp_store_release instead of MMIO kick; completion
detection via more_used() (direct used-ring check)
- virtio_sqcq_poll.c: per-device CQ poll thread with need_resched()
cooperative spin, time-based spin budget, 1ms idle sleep
- virtio_pci_modern.c: feature negotiation, PCI config space write
for SQ/CQ DMA addresses, poll thread lifecycle
- virtio.c: reject VIRTIO_F_SQCQ_POLL + VIRTIO_F_RING_PACKED
- virtio_scsi.c: 10s window io_stats diagnostics output
- do_softirq() after each completion callback (no hardware interrupt)
Signed-off-by: Yufeng Wang <[email protected]>
---
drivers/scsi/virtio_scsi.c | 113 ++++++++-
drivers/virtio/Makefile | 2 +-
drivers/virtio/virtio.c | 10 +
drivers/virtio/virtio_pci_common.c | 3 +
drivers/virtio/virtio_pci_modern.c | 39 +++
drivers/virtio/virtio_pci_modern_dev.c | 34 ++-
drivers/virtio/virtio_ring.c | 262 +++++++++++++++++++
drivers/virtio/virtio_sqcq_poll.c | 338 +++++++++++++++++++++++++
include/linux/virtio.h | 25 ++
include/linux/virtio_pci_modern.h | 2 +
10 files changed, 825 insertions(+), 3 deletions(-)
create mode 100644 drivers/virtio/virtio_sqcq_poll.c
diff --git a/drivers/scsi/virtio_scsi.c b/drivers/scsi/virtio_scsi.c
index 0ed8558dad72..f803bb62d22a 100644
--- a/drivers/scsi/virtio_scsi.c
+++ b/drivers/scsi/virtio_scsi.c
@@ -30,6 +30,7 @@
#include <scsi/scsi_devinfo.h>
#include <linux/seqlock.h>
#include <linux/dma-mapping.h>
+#include <linux/ktime.h>
#include "sd.h"
@@ -46,6 +47,8 @@ MODULE_PARM_DESC(virtscsi_poll_queues,
struct virtio_scsi_cmd {
struct scsi_cmnd *sc;
struct completion *comp;
+ u64 submit_ns; /* ktime_get_ns() at submission */
+ u64 notify_ns; /* ktime_get_ns() after SQ doorbell */
union {
struct virtio_scsi_cmd_req cmd;
struct virtio_scsi_cmd_req_pi cmd_pi;
@@ -95,6 +98,22 @@ struct virtio_scsi {
struct virtio_scsi_event events[VIRTIO_SCSI_EVENT_LEN];
__dma_from_device_group_end();
+ /* I/O latency statistics (debugging) */
+ spinlock_t lat_lock;
+ u64 lat_sum_ns;
+ u64 lat_min_ns;
+ u64 lat_max_ns;
+ u64 lat_count;
+ u64 host_sum_ns; /* notify → cq_detect (host transit) */
+ u64 cb_wait_sum_ns; /* cq_detect → process (poll sched latency) */
+ u64 det_count;
+ u64 last_cq_detect_ns; /* timestamp of last CQ detection */
+ unsigned long lat_last_print;
+
+ /* Completion interval tracking (matches host interval metric) */
+ u64 last_complete_ns; /* ktime of last completion */
+ u64 ema_cmp_int_ns; /* EMA of completion-to-completion interval */
+
struct virtio_scsi_vq req_vqs[];
};
@@ -122,6 +141,7 @@ static void virtscsi_complete_cmd(struct virtio_scsi
*vscsi, void *buf)
struct virtio_scsi_cmd *cmd = buf;
struct scsi_cmnd *sc = cmd->sc;
struct virtio_scsi_cmd_resp *resp = &cmd->resp.cmd;
+ u64 cb_entry_ns = ktime_get_ns();
dev_dbg(&sc->device->sdev_gendev,
"cmd %p response %u status %#02x sense_len %u\n",
@@ -175,6 +195,88 @@ static void virtscsi_complete_cmd(struct virtio_scsi
*vscsi, void *buf)
VIRTIO_SCSI_SENSE_SIZE));
}
+ /* I/O latency tracking */
+ if (cmd->submit_ns) {
+ u64 now = ktime_get_ns();
+ u64 lat = now - cmd->submit_ns;
+ unsigned long flags;
+
+ spin_lock_irqsave(&vscsi->lat_lock, flags);
+ vscsi->lat_sum_ns += lat;
+ vscsi->lat_count++;
+ if (lat < vscsi->lat_min_ns || vscsi->lat_min_ns == 0)
+ vscsi->lat_min_ns = lat;
+ if (lat > vscsi->lat_max_ns)
+ vscsi->lat_max_ns = lat;
+
+ /* Completion interval EMA (matches host interval metric) */
+ if (vscsi->last_complete_ns) {
+ u64 cmp_int = now - vscsi->last_complete_ns;
+
+ if (vscsi->ema_cmp_int_ns == 0)
+ vscsi->ema_cmp_int_ns = cmp_int;
+ else
+ vscsi->ema_cmp_int_ns =
+ vscsi->ema_cmp_int_ns -
+ vscsi->ema_cmp_int_ns / 8 +
+ cmp_int / 8;
+ }
+ vscsi->last_complete_ns = now;
+
+ if (cmd->notify_ns && vscsi->last_cq_detect_ns) {
+ u64 host_lat = vscsi->last_cq_detect_ns -
+ cmd->notify_ns;
+ u64 cb_wait = cb_entry_ns -
+ vscsi->last_cq_detect_ns;
+
+ if (host_lat < 10ULL * NSEC_PER_SEC) {
+ vscsi->host_sum_ns += host_lat;
+ vscsi->cb_wait_sum_ns += cb_wait;
+ vscsi->det_count++;
+ }
+ }
+
+ /* 10s window — single consolidated output */
+ if (time_after(jiffies, vscsi->lat_last_print +
+ msecs_to_jiffies(10000)) &&
+ vscsi->lat_count > 0) {
+ u64 avg_lat = vscsi->lat_sum_ns / vscsi->lat_count;
+ u64 host_lat = vscsi->det_count > 0 ?
+ vscsi->host_sum_ns / vscsi->det_count : 0;
+ u64 poll_lat = vscsi->det_count > 0 ?
+ vscsi->cb_wait_sum_ns / vscsi->det_count : 0;
+ u64 interval = 0;
+ unsigned int wk_kick;
+ unsigned long sleep_cnt;
+
+ wk_kick = virtqueue_get_sq_notify_stats(
+ vscsi->req_vqs[0].vq, &interval);
+ sleep_cnt =
virtio_sqcq_poll_get_sleep_count(vscsi->vdev);
+
+ dev_info(&vscsi->vdev->dev,
+ "io_stats: cq=%llu avg_lat=%lluns
host_lat=%lluns poll_lat=%lluns min_lat=%lluns max_lat=%lluns interval=%lluns
cmp_interval=%lluns wk_kick=%u sleep=%lu\n",
+ vscsi->lat_count,
+ avg_lat,
+ host_lat,
+ poll_lat,
+ vscsi->lat_min_ns,
+ vscsi->lat_max_ns,
+ interval,
+ vscsi->ema_cmp_int_ns,
+ wk_kick,
+ sleep_cnt);
+ vscsi->lat_sum_ns = 0;
+ vscsi->lat_count = 0;
+ vscsi->lat_min_ns = 0;
+ vscsi->lat_max_ns = 0;
+ vscsi->host_sum_ns = 0;
+ vscsi->cb_wait_sum_ns = 0;
+ vscsi->det_count = 0;
+ vscsi->lat_last_print = jiffies;
+ }
+ spin_unlock_irqrestore(&vscsi->lat_lock, flags);
+ }
+
scsi_done(sc);
}
@@ -204,6 +306,7 @@ static void virtscsi_req_done(struct virtqueue *vq)
int index = vq->index - VIRTIO_SCSI_VQ_BASE;
struct virtio_scsi_vq *req_vq = &vscsi->req_vqs[index];
+ vscsi->last_cq_detect_ns = virtqueue_get_cq_detect_ns(vq);
virtscsi_vq_done(vscsi, req_vq, virtscsi_complete_cmd);
};
@@ -514,8 +617,12 @@ static int virtscsi_add_cmd(struct virtio_scsi_vq *vq,
spin_unlock_irqrestore(&vq->vq_lock, flags);
- if (needs_kick)
+ if (needs_kick) {
+ cmd->notify_ns = ktime_get_ns();
virtqueue_notify(vq->vq);
+ } else {
+ cmd->notify_ns = 0;
+ }
return err;
}
@@ -605,6 +712,7 @@ static enum scsi_qc_status virtscsi_queuecommand(struct
Scsi_Host *shost,
}
kick = (sc->flags & SCMD_LAST) != 0;
+ cmd->submit_ns = ktime_get_ns();
ret = virtscsi_add_cmd(req_vq, cmd, req_size, sizeof(cmd->resp.cmd),
kick);
if (ret == -EIO) {
cmd->resp.cmd.response = VIRTIO_SCSI_S_BAD_TARGET;
@@ -895,6 +1003,9 @@ static int virtscsi_init(struct virtio_device *vdev,
virtscsi_init_vq(&vscsi->req_vqs[i - VIRTIO_SCSI_VQ_BASE],
vqs[i]);
+ spin_lock_init(&vscsi->lat_lock);
+ vscsi->lat_last_print = jiffies;
+
virtscsi_config_set(vdev, cdb_size, VIRTIO_SCSI_CDB_SIZE);
virtscsi_config_set(vdev, sense_size, VIRTIO_SCSI_SENSE_SIZE);
diff --git a/drivers/virtio/Makefile b/drivers/virtio/Makefile
index eefcfe90d6b8..769e95ee5be4 100644
--- a/drivers/virtio/Makefile
+++ b/drivers/virtio/Makefile
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: GPL-2.0
-obj-$(CONFIG_VIRTIO) += virtio.o virtio_ring.o
+obj-$(CONFIG_VIRTIO) += virtio.o virtio_ring.o virtio_sqcq_poll.o
obj-$(CONFIG_VIRTIO_ANCHOR) += virtio_anchor.o
obj-$(CONFIG_VIRTIO_PCI_LIB) += virtio_pci_modern_dev.o
obj-$(CONFIG_VIRTIO_PCI_LIB_LEGACY) += virtio_pci_legacy_dev.o
diff --git a/drivers/virtio/virtio.c b/drivers/virtio/virtio.c
index 5bdc6b82b30b..d440e920f8e6 100644
--- a/drivers/virtio/virtio.c
+++ b/drivers/virtio/virtio.c
@@ -221,6 +221,16 @@ static int virtio_features_ok(struct virtio_device *dev)
}
}
+ /* SQ/CQ poll only supports split ring; reject packed ring
+ * to avoid split-specific field access in virtqueue_notify().
+ */
+ if (virtio_has_feature(dev, VIRTIO_F_SQCQ_POLL) &&
+ virtio_has_feature(dev, VIRTIO_F_RING_PACKED)) {
+ dev_warn(&dev->dev,
+ "VIRTIO_F_SQCQ_POLL is incompatible with
VIRTIO_F_RING_PACKED\n");
+ return -ENODEV;
+ }
+
if (!virtio_has_feature(dev, VIRTIO_F_VERSION_1))
return 0;
diff --git a/drivers/virtio/virtio_pci_common.c
b/drivers/virtio/virtio_pci_common.c
index da97b6a988de..cd22651c73e2 100644
--- a/drivers/virtio/virtio_pci_common.c
+++ b/drivers/virtio/virtio_pci_common.c
@@ -268,6 +268,9 @@ void vp_del_vqs(struct virtio_device *vdev)
struct virtqueue *vq, *n;
int i;
+ /* Stop SQ/CQ poll thread before deleting virtqueues */
+ virtio_stop_sqcq_poll(vdev);
+
list_for_each_entry_safe(vq, n, &vdev->vqs, list) {
info = vp_is_avq(vdev, vq->index) ? vp_dev->admin_vq.info :
vp_dev->vqs[vq->index];
diff --git a/drivers/virtio/virtio_pci_modern.c
b/drivers/virtio/virtio_pci_modern.c
index 6d8ae2a6a8ca..7a36ec4584a6 100644
--- a/drivers/virtio/virtio_pci_modern.c
+++ b/drivers/virtio/virtio_pci_modern.c
@@ -18,6 +18,7 @@
#include <linux/virtio_pci_admin.h>
#define VIRTIO_PCI_NO_LEGACY
#define VIRTIO_RING_NO_LEGACY
+
#include "virtio_pci_common.h"
#define VIRTIO_AVQ_SGS_MAX 4
@@ -378,6 +379,9 @@ static void vp_transport_features(struct virtio_device
*vdev, u64 features)
if (features & BIT_ULL(VIRTIO_F_ADMIN_VQ))
__virtio_set_bit(vdev, VIRTIO_F_ADMIN_VQ);
+
+ if (features & BIT_ULL(VIRTIO_F_SQCQ_POLL))
+ __virtio_set_bit(vdev, VIRTIO_F_SQCQ_POLL);
}
static int __vp_check_common_size_one_feature(struct virtio_device *vdev, u32
fbit,
@@ -413,6 +417,9 @@ static int vp_check_common_size(struct virtio_device *vdev)
if (vp_check_common_size_one_feature(vdev, VIRTIO_F_ADMIN_VQ,
admin_queue_num))
return -EINVAL;
+ if (vp_check_common_size_one_feature(vdev, VIRTIO_F_SQCQ_POLL,
cqe_ring_hi))
+ return -EINVAL;
+
return 0;
}
@@ -578,6 +585,11 @@ static int vp_active_vq(struct virtqueue *vq, u16 msix_vec)
virtqueue_get_avail_addr(vq),
virtqueue_get_used_addr(vq));
+ if (virtqueue_sqcq_poll_active(vq))
+ vp_modern_queue_address_sqcq(mdev, index,
+ virtqueue_get_sq_dma_addr(vq),
+ virtqueue_get_cq_dma_addr(vq));
+
if (msix_vec != VIRTIO_MSI_NO_VECTOR) {
msix_vec = vp_modern_queue_vector(mdev, index, msix_vec);
if (msix_vec == VIRTIO_MSI_NO_VECTOR)
@@ -753,6 +765,27 @@ static int vp_modern_find_vqs(struct virtio_device *vdev,
unsigned int nvqs,
if (rc)
return rc;
+ /* Start SQ/CQ poll thread if feature negotiated */
+ if (virtio_has_feature(vdev, VIRTIO_F_SQCQ_POLL)) {
+ dev_info(&vdev->dev, "VIRTIO_F_SQCQ_POLL negotiated, starting
poll thread\n");
+ rc = virtio_start_sqcq_poll(vdev, 1000, 1000);
+ if (rc)
+ goto err_del_vqs;
+
+ list_for_each_entry(vq, &vdev->vqs, list) {
+ if (!virtqueue_sqcq_poll_active(vq))
+ continue;
+ rc = virtio_sqcq_poll_register_vq(vdev, vq);
+ if (rc) {
+ dev_err(&vdev->dev,
+ "failed to register vq %d (%s) with
poll thread: %d\n",
+ vq->index,
+ vq->name ? vq->name : "?", rc);
+ goto err_stop_poll;
+ }
+ }
+ }
+
/* Select and activate all queues. Has to be done last: once we do
* this, there's no way to go back except reset.
*/
@@ -760,6 +793,12 @@ static int vp_modern_find_vqs(struct virtio_device *vdev,
unsigned int nvqs,
vp_modern_set_queue_enable(&vp_dev->mdev, vq->index, true);
return 0;
+
+err_stop_poll:
+ virtio_stop_sqcq_poll(vdev);
+err_del_vqs:
+ vp_del_vqs(vdev);
+ return rc;
}
static void del_vq(struct virtio_pci_vq_info *info)
diff --git a/drivers/virtio/virtio_pci_modern_dev.c
b/drivers/virtio/virtio_pci_modern_dev.c
index 413a8c353463..642b9a58751c 100644
--- a/drivers/virtio/virtio_pci_modern_dev.c
+++ b/drivers/virtio/virtio_pci_modern_dev.c
@@ -211,6 +211,14 @@ static inline void check_offsets(void)
offsetof(struct virtio_pci_modern_common_cfg,
admin_queue_index));
BUILD_BUG_ON(VIRTIO_PCI_COMMON_ADM_Q_NUM !=
offsetof(struct virtio_pci_modern_common_cfg,
admin_queue_num));
+ BUILD_BUG_ON(VIRTIO_PCI_COMMON_SQE_LO !=
+ offsetof(struct virtio_pci_modern_common_cfg,
sqe_ring_lo));
+ BUILD_BUG_ON(VIRTIO_PCI_COMMON_SQE_HI !=
+ offsetof(struct virtio_pci_modern_common_cfg,
sqe_ring_hi));
+ BUILD_BUG_ON(VIRTIO_PCI_COMMON_CQE_LO !=
+ offsetof(struct virtio_pci_modern_common_cfg,
cqe_ring_lo));
+ BUILD_BUG_ON(VIRTIO_PCI_COMMON_CQE_HI !=
+ offsetof(struct virtio_pci_modern_common_cfg,
cqe_ring_hi));
}
/*
@@ -300,7 +308,7 @@ int vp_modern_probe(struct virtio_pci_modern_device *mdev)
mdev->common = vp_modern_map_capability(mdev, common,
sizeof(struct virtio_pci_common_cfg), 4, 0,
offsetofend(struct virtio_pci_modern_common_cfg,
- admin_queue_num),
+ cqe_ring_hi),
&mdev->common_len, NULL);
if (!mdev->common)
goto err_map_common;
@@ -607,6 +615,30 @@ void vp_modern_queue_address(struct
virtio_pci_modern_device *mdev,
}
EXPORT_SYMBOL_GPL(vp_modern_queue_address);
+/*
+ * vp_modern_queue_address_sqcq - set the SQ/CQ doorbell DMA addresses
+ * @mdev: the modern virtio-pci device
+ * @index: the queue index
+ * @sq_addr: DMA address of the SQ doorbell
+ * @cq_addr: DMA address of the CQ doorbell
+ */
+void vp_modern_queue_address_sqcq(struct virtio_pci_modern_device *mdev,
+ u16 index, u64 sq_addr, u64 cq_addr)
+{
+ struct virtio_pci_modern_common_cfg __iomem *cfg;
+
+ cfg = (struct virtio_pci_modern_common_cfg __iomem *)mdev->common;
+
+ vp_iowrite16(index, &cfg->cfg.queue_select);
+
+ vp_iowrite64_twopart(sq_addr, &cfg->sqe_ring_lo,
+ &cfg->sqe_ring_hi);
+
+ vp_iowrite64_twopart(cq_addr, &cfg->cqe_ring_lo,
+ &cfg->cqe_ring_hi);
+}
+EXPORT_SYMBOL_GPL(vp_modern_queue_address_sqcq);
+
/*
* vp_modern_set_queue_enable - enable a virtqueue
* @mdev: the modern virtio-pci device
diff --git a/drivers/virtio/virtio_ring.c b/drivers/virtio/virtio_ring.c
index 335692d41617..a3aaf123ee74 100644
--- a/drivers/virtio/virtio_ring.c
+++ b/drivers/virtio/virtio_ring.c
@@ -270,10 +270,25 @@ struct vring_virtqueue {
bool last_add_time_valid;
ktime_t last_add_time;
#endif
+
+ /* SQ/CQ polling state (VIRTIO_F_SQCQ_POLL) */
+ bool sqcq_poll; /* Is SQ/CQ polling active? */
+ struct vring_sq *sq; /* DMA-coherent SQ doorbell */
+ struct vring_cq *cq; /* DMA-coherent CQ doorbell */
+ dma_addr_t sq_dma_addr; /* DMA address of SQ */
+ dma_addr_t cq_dma_addr; /* DMA address of CQ */
+ atomic_t sq_mmio_count; /* MMIO kicks (sq_need_wakeup=1) */
+ u64 last_cq_detect_ns; /* ktime_get_ns() when CQ work first
detected */
+ u64 sq_last_notify_ns; /* ktime of last sq->idx store */
+ u64 sq_ema_interval_ns; /* EMA of sq->idx update interval (ns) */
};
static struct vring_desc_extra *vring_alloc_desc_extra(unsigned int num);
static void vring_free(struct virtqueue *_vq);
+static int virtio_alloc_sqcq(struct virtio_device *vdev,
+ struct vring_virtqueue *vq);
+static void virtio_free_sqcq(struct virtio_device *vdev,
+ struct vring_virtqueue *vq);
/*
* Helpers.
@@ -1334,6 +1349,10 @@ static struct virtqueue
*__vring_new_virtqueue_split(unsigned int index,
vq->layout = virtio_has_feature(vdev, VIRTIO_F_IN_ORDER) ?
VQ_LAYOUT_SPLIT_IN_ORDER : VQ_LAYOUT_SPLIT;
+ vq->sqcq_poll = false;
+ vq->sq = NULL;
+ vq->cq = NULL;
+
if (virtio_has_feature(vdev, VIRTIO_F_ORDER_PLATFORM))
vq->weak_barriers = false;
@@ -1385,6 +1404,17 @@ static struct virtqueue *vring_create_virtqueue_split(
to_vvq(vq)->we_own_ring = true;
+ if (virtio_has_feature(vdev, VIRTIO_F_SQCQ_POLL)) {
+ if (virtio_alloc_sqcq(vdev, to_vvq(vq))) {
+ /* vq is on vdev->vqs list; use vring_del_virtqueue()
+ * to remove and free. vring_free_split() would
+ * double-free via later vp_del_vqs().
+ */
+ vring_del_virtqueue(vq);
+ return NULL;
+ }
+ }
+
return vq;
}
@@ -2554,6 +2584,10 @@ static struct virtqueue
*__vring_new_virtqueue_packed(unsigned int index,
vq->layout = virtio_has_feature(vdev, VIRTIO_F_IN_ORDER) ?
VQ_LAYOUT_PACKED_IN_ORDER : VQ_LAYOUT_PACKED;
+ vq->sqcq_poll = false;
+ vq->sq = NULL;
+ vq->cq = NULL;
+
if (virtio_has_feature(vdev, VIRTIO_F_ORDER_PLATFORM))
vq->weak_barriers = false;
@@ -3013,6 +3047,9 @@ bool virtqueue_kick_prepare(struct virtqueue *_vq)
{
struct vring_virtqueue *vq = to_vvq(_vq);
+ if (vq->sqcq_poll)
+ return true;
+
return VIRTQUEUE_CALL(vq, kick_prepare);
}
EXPORT_SYMBOL_GPL(virtqueue_kick_prepare);
@@ -3032,6 +3069,45 @@ bool virtqueue_notify(struct virtqueue *_vq)
if (unlikely(vq->broken))
return false;
+ /* SQ/CQ polling mode: update SQ idx instead of MMIO kick. */
+ if (vq->sqcq_poll) {
+ u16 new_sq_idx = vq->split.avail_idx_shadow;
+ u8 need_wakeup;
+
+ /* Release: make descriptor writes visible before sq->idx */
+ smp_store_release(&vq->sq->idx, new_sq_idx);
+
+ /* Track submission interval (matches host's interval metric) */
+ {
+ u64 _now = ktime_get_ns();
+
+ if (vq->sq_last_notify_ns) {
+ u64 _int = _now - vq->sq_last_notify_ns;
+
+ if (vq->sq_ema_interval_ns == 0)
+ vq->sq_ema_interval_ns = _int;
+ else
+ vq->sq_ema_interval_ns =
+ vq->sq_ema_interval_ns -
+ vq->sq_ema_interval_ns / 8 +
+ _int / 8;
+ }
+ vq->sq_last_notify_ns = _now;
+ }
+
+ /* Acquire: order sq->idx store before reading sq_need_wakeup.
+ * Prevents stale read on ARM/RISC-V causing missed kick
+ * when host has set sq_need_wakeup=1 and gone to sleep.
+ */
+ need_wakeup = smp_load_acquire(&vq->sq->sq_need_wakeup);
+
+ /* Device poll thread is sleeping -- need MMIO kick to wake it
*/
+ if (!need_wakeup)
+ return true;
+ atomic_inc(&vq->sq_mmio_count);
+ /* Fall through to MMIO notify */
+ }
+
/* Prod other side to tell it about changes. */
if (!vq->notify(_vq)) {
vq->broken = true;
@@ -3230,6 +3306,12 @@ irqreturn_t vring_interrupt(int irq, void *_vq)
{
struct vring_virtqueue *vq = to_vvq(_vq);
+ /* SQ/CQ poll mode: wake the poll thread, let it handle completions */
+ if (vq->sqcq_poll) {
+ virtio_sqcq_poll_wake_thread(vq->vq.vdev);
+ return IRQ_HANDLED;
+ }
+
if (!more_used(vq)) {
pr_debug("virtqueue interrupt with no work for %p\n", vq);
return IRQ_NONE;
@@ -3250,6 +3332,7 @@ irqreturn_t vring_interrupt(int irq, void *_vq)
data_race(vq->event_triggered = true);
pr_debug("virtqueue callback for %p (%p)\n", vq, vq->vq.callback);
+
if (vq->vq.callback)
vq->vq.callback(&vq->vq);
@@ -3468,6 +3551,8 @@ static void vring_free(struct virtqueue *_vq)
kfree(vq->split.desc_state);
kfree(vq->split.desc_extra);
}
+ if (vq->sqcq_poll)
+ virtio_free_sqcq(vq->vq.vdev, vq);
}
void vring_del_virtqueue(struct virtqueue *_vq)
@@ -3524,6 +3609,8 @@ void vring_transport_features(struct virtio_device *vdev)
break;
case VIRTIO_F_IN_ORDER:
break;
+ case VIRTIO_F_SQCQ_POLL:
+ break;
default:
/* We don't understand this bit. */
__virtio_clear_bit(vdev, i);
@@ -3663,6 +3750,181 @@ dma_addr_t virtqueue_get_used_addr(const struct
virtqueue *_vq)
}
EXPORT_SYMBOL_GPL(virtqueue_get_used_addr);
+bool virtqueue_sqcq_poll_active(const struct virtqueue *_vq)
+{
+ const struct vring_virtqueue *vq = to_vvq(_vq);
+
+ return vq->sqcq_poll;
+}
+EXPORT_SYMBOL_GPL(virtqueue_sqcq_poll_active);
+
+/**
+ * virtqueue_disable_sqcq_poll - disable SQ/CQ poll mode for a virtqueue.
+ * @_vq: the virtqueue
+ *
+ * Clears sqcq_poll flag; restores MMIO kick + interrupt for this vq.
+ * Used for control/event queues that must not use the poll thread.
+ */
+void virtqueue_disable_sqcq_poll(struct virtqueue *_vq)
+{
+ struct vring_virtqueue *vq = to_vvq(_vq);
+
+ vq->sqcq_poll = false;
+}
+EXPORT_SYMBOL_GPL(virtqueue_disable_sqcq_poll);
+
+dma_addr_t virtqueue_get_sq_dma_addr(const struct virtqueue *_vq)
+{
+ const struct vring_virtqueue *vq = to_vvq(_vq);
+
+ return vq->sq_dma_addr;
+}
+EXPORT_SYMBOL_GPL(virtqueue_get_sq_dma_addr);
+
+dma_addr_t virtqueue_get_cq_dma_addr(const struct virtqueue *_vq)
+{
+ const struct vring_virtqueue *vq = to_vvq(_vq);
+
+ return vq->cq_dma_addr;
+}
+EXPORT_SYMBOL_GPL(virtqueue_get_cq_dma_addr);
+
+/* Completion detection and sleep/wake accessors for the poll thread. */
+
+bool virtqueue_cq_poll_has_work(struct virtqueue *_vq)
+{
+ struct vring_virtqueue *vq = to_vvq(_vq);
+
+ if (!vq->cq)
+ return false;
+
+ if (more_used(vq)) {
+ WRITE_ONCE(vq->last_cq_detect_ns, ktime_get_ns());
+ return true;
+ }
+
+ return false;
+}
+EXPORT_SYMBOL_GPL(virtqueue_cq_poll_has_work);
+
+unsigned int virtqueue_get_sq_notify_stats(struct virtqueue *_vq, u64
*interval)
+{
+ struct vring_virtqueue *vq = to_vvq(_vq);
+ unsigned int mmio;
+
+ mmio = atomic_xchg(&vq->sq_mmio_count, 0);
+ if (interval)
+ *interval = vq->sq_ema_interval_ns;
+ return mmio;
+}
+EXPORT_SYMBOL_GPL(virtqueue_get_sq_notify_stats);
+
+void virtqueue_cq_set_need_wakeup(struct virtqueue *_vq)
+{
+ struct vring_virtqueue *vq = to_vvq(_vq);
+
+ if (vq->cq)
+ /* Release: order prior used-ring reads before wakeup flag */
+ smp_store_release(&vq->cq->cq_need_wakeup, 1);
+}
+EXPORT_SYMBOL_GPL(virtqueue_cq_set_need_wakeup);
+
+void virtqueue_cq_clear_need_wakeup(struct virtqueue *_vq)
+{
+ struct vring_virtqueue *vq = to_vvq(_vq);
+
+ if (vq->cq)
+ WRITE_ONCE(vq->cq->cq_need_wakeup, 0);
+}
+EXPORT_SYMBOL_GPL(virtqueue_cq_clear_need_wakeup);
+
+bool virtqueue_cq_recheck(struct virtqueue *_vq)
+{
+ struct vring_virtqueue *vq = to_vvq(_vq);
+
+ if (!vq->cq)
+ return false;
+
+ /* Full barrier: order cq_need_wakeup store before used ring read */
+ smp_mb();
+ return more_used(vq);
+}
+EXPORT_SYMBOL_GPL(virtqueue_cq_recheck);
+
+u64 virtqueue_get_cq_detect_ns(const struct virtqueue *_vq)
+{
+ const struct vring_virtqueue *vq = to_vvq(_vq);
+
+ return READ_ONCE(vq->last_cq_detect_ns);
+}
+EXPORT_SYMBOL_GPL(virtqueue_get_cq_detect_ns);
+
+/*
+ * virtio_alloc_sqcq - allocate SQ/CQ doorbell structures for polling mode
+ * @vdev: the virtio device
+ * @vq: the vring_virtqueue to allocate for
+ *
+ * Called when VIRTIO_F_SQCQ_POLL is negotiated. Returns 0 on success.
+ */
+static int virtio_alloc_sqcq(struct virtio_device *vdev,
+ struct vring_virtqueue *vq)
+{
+ struct device *dma_dev = vdev->dev.parent;
+
+ /* Control and event queues use traditional interrupt-driven I/O. */
+ if (vq->vq.name &&
+ (strcmp(vq->vq.name, "control") == 0 ||
+ strcmp(vq->vq.name, "event") == 0))
+ return 0;
+ size_t total = sizeof(struct vring_sq) + sizeof(struct vring_cq);
+ void *buf;
+
+ buf = dma_alloc_coherent(dma_dev, total, &vq->sq_dma_addr, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+
+ memset(buf, 0, total);
+
+ vq->sq = buf;
+ vq->cq = buf + sizeof(struct vring_sq);
+ vq->cq_dma_addr = vq->sq_dma_addr + sizeof(struct vring_sq);
+ vq->sqcq_poll = true;
+ atomic_set(&vq->sq_mmio_count, 0);
+ vq->last_cq_detect_ns = 0;
+ vq->sq_last_notify_ns = 0;
+ vq->sq_ema_interval_ns = 0;
+
+ /* Diagnostic: dump SQ/CQ addresses for debug */
+ dev_dbg(&vdev->dev,
+ "vq %s: SQ/CQ doorbells kvirt=%p sq_dma=0x%llx cq_dma=0x%llx\n",
+ vq->vq.name ? vq->vq.name : "?",
+ buf,
+ (unsigned long long)vq->sq_dma_addr,
+ (unsigned long long)vq->cq_dma_addr);
+
+ return 0;
+}
+
+/*
+ * virtio_free_sqcq - free SQ/CQ doorbell structures
+ * @vdev: the virtio device
+ * @vq: the vring_virtqueue to free for
+ */
+static void virtio_free_sqcq(struct virtio_device *vdev,
+ struct vring_virtqueue *vq)
+{
+ struct device *dma_dev = vdev->dev.parent;
+ size_t total = sizeof(struct vring_sq) + sizeof(struct vring_cq);
+
+ if (vq->sq) {
+ dma_free_coherent(dma_dev, total,
+ vq->sq, vq->sq_dma_addr);
+ vq->sq = NULL;
+ vq->cq = NULL;
+ }
+ vq->sqcq_poll = false;
+}
+
/* Only available for split ring */
const struct vring *virtqueue_get_vring(const struct virtqueue *vq)
{
diff --git a/drivers/virtio/virtio_sqcq_poll.c
b/drivers/virtio/virtio_sqcq_poll.c
new file mode 100644
index 000000000000..411493218e93
--- /dev/null
+++ b/drivers/virtio/virtio_sqcq_poll.c
@@ -0,0 +1,338 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * virtio SQ/CQ polling thread.
+ *
+ * Replaces interrupt-based completion notification with a dedicated
+ * kernel thread that checks the used ring for completions. Uses the
+ * io_uring sqpoll pattern: busy-wait while active, then set
+ * NEED_WAKEUP and sleep when idle for a configurable timeout.
+ *
+ * Copyright (C) 2026
+ */
+
+#include <linux/kernel.h>
+#include <linux/device.h>
+#include <linux/sched.h>
+#include <linux/completion.h>
+#include <linux/mutex.h>
+#include <linux/wait.h>
+#include <linux/sched/clock.h>
+#include <linux/slab.h>
+#include <linux/list.h>
+#include <linux/kthread.h>
+#include <linux/virtio.h>
+#include <linux/virtio_config.h>
+#include <linux/virtio_ring.h>
+#include <linux/interrupt.h>
+
+/**
+ * struct virtio_sqcq_poll_data - per-device poll thread state.
+ *
+ * Follows the Host vhost_sqcq_poll_thread pattern.
+ */
+struct virtio_sqcq_poll_data {
+ struct mutex lock;
+
+ /* List of virtqueues served by this poll thread */
+ struct list_head vq_list;
+
+ struct task_struct *thread;
+ struct wait_queue_head wait;
+
+ unsigned int idle_timeout_us; /* Sleep timeout (us) */
+ unsigned int busy_spin_us; /* Busy-spin budget (us) */
+
+ unsigned long state;
+#define VIRTIO_SQ_THREAD_SHOULD_STOP 0
+
+ struct completion exited;
+ struct device *dev; /* for dev_info() logging */
+
+ /* Poll thread diagnostic statistics */
+ unsigned long stat_sleep_count;
+};
+
+/**
+ * struct virtio_sqcq_vq_node - links a virtqueue to its poll_data.
+ */
+struct virtio_sqcq_vq_node {
+ struct virtqueue *vq;
+ struct list_head node;
+};
+
+static void virtio_sqcq_poll_wake(struct virtio_sqcq_poll_data *sqd)
+{
+ wake_up(&sqd->wait);
+}
+
+/**
+ * virtio_sqcq_poll_wake_thread - wake the CQ poll thread from IRQ context.
+ * @vdev: the virtio device
+ *
+ * Called by vring_interrupt() when SQ/CQ poll mode is active. Wakes the
+ * poll thread so it can check the used ring and invoke completion
+ * callbacks in its own context, instead of handling completions directly
+ * from the hard-IRQ handler.
+ */
+void virtio_sqcq_poll_wake_thread(struct virtio_device *vdev)
+{
+ struct virtio_sqcq_poll_data *sqd = vdev->sqcq_poll_data;
+
+ if (sqd)
+ virtio_sqcq_poll_wake(sqd);
+}
+EXPORT_SYMBOL_GPL(virtio_sqcq_poll_wake_thread);
+
+unsigned long virtio_sqcq_poll_get_sleep_count(struct virtio_device *vdev)
+{
+ struct virtio_sqcq_poll_data *sqd = vdev->sqcq_poll_data;
+
+ if (!sqd)
+ return 0;
+
+ return xchg(&sqd->stat_sleep_count, 0);
+}
+EXPORT_SYMBOL_GPL(virtio_sqcq_poll_get_sleep_count);
+
+static int virtio_sqcq_poll_thread(void *data)
+{
+ struct virtio_sqcq_poll_data *sqd = data;
+ struct virtio_sqcq_vq_node *node;
+ ktime_t last_io_ts = ktime_get();
+ u64 spin_budget_ns = (u64)sqd->busy_spin_us * NSEC_PER_USEC;
+ int vq_count = 0;
+
+ /* Count registered vqs for the startup message */
+ mutex_lock(&sqd->lock);
+ list_for_each_entry(node, &sqd->vq_list, node)
+ vq_count++;
+ mutex_unlock(&sqd->lock);
+
+ dev_info(sqd->dev, "poll thread running, %d vqs, spin=%uus,
idle=%uus\n",
+ vq_count, sqd->busy_spin_us, sqd->idle_timeout_us);
+
+ while (!kthread_should_stop()) {
+ bool did_work = false;
+
+ if (test_bit(VIRTIO_SQ_THREAD_SHOULD_STOP, &sqd->state)) {
+ dev_info(sqd->dev, "poll thread stopping\n");
+ break;
+ }
+
+ if (signal_pending(current)) {
+ flush_signals(current);
+ continue;
+ }
+
+ mutex_lock(&sqd->lock);
+
+ /* Process completions on all registered virtqueues */
+ list_for_each_entry(node, &sqd->vq_list, node) {
+ while (virtqueue_cq_poll_has_work(node->vq)) {
+ if (node->vq->callback)
+ node->vq->callback(node->vq);
+ if (local_softirq_pending())
+ do_softirq();
+ did_work = true;
+ }
+ }
+
+ if (did_work) {
+ mutex_unlock(&sqd->lock);
+ last_io_ts = ktime_get();
+ continue;
+ }
+
+ /* No work — spin within the time budget from last_io_ts.
+ * need_resched() + cpu_relax() for cooperative yielding,
+ * same pattern as Host vhost_sqcq_poll_thread.
+ */
+ if (spin_budget_ns > 0 &&
+ ktime_to_ns(ktime_sub(ktime_get(), last_io_ts)) <
+ (s64)spin_budget_ns) {
+ mutex_unlock(&sqd->lock);
+ if (need_resched())
+ cond_resched();
+ cpu_relax();
+ continue;
+ }
+
+ /* Spin budget expired — set cq_need_wakeup on all VQs
+ * and sleep. Double-check for work before sleeping to
+ * close the lost-wakeup race.
+ */
+ {
+ bool found_work = false;
+ DEFINE_WAIT(wait);
+
+ list_for_each_entry(node, &sqd->vq_list, node)
+ virtqueue_cq_set_need_wakeup(node->vq);
+
+ prepare_to_wait(&sqd->wait, &wait,
+ TASK_INTERRUPTIBLE);
+
+ list_for_each_entry(node, &sqd->vq_list, node) {
+ if (virtqueue_cq_recheck(node->vq)) {
+ found_work = true;
+ break;
+ }
+ }
+
+ if (!found_work) {
+ mutex_unlock(&sqd->lock);
+ schedule_timeout(
+ usecs_to_jiffies(sqd->idle_timeout_us));
+ sqd->stat_sleep_count++;
+ mutex_lock(&sqd->lock);
+ }
+
+ finish_wait(&sqd->wait, &wait);
+
+ list_for_each_entry(node, &sqd->vq_list, node)
+ virtqueue_cq_clear_need_wakeup(node->vq);
+ }
+
+ mutex_unlock(&sqd->lock);
+ }
+
+ complete(&sqd->exited);
+ return 0;
+}
+
+/**
+ * virtio_start_sqcq_poll - start the SQ/CQ poll thread for a device.
+ * @vdev: the virtio device
+ * @idle_timeout_us: idle timeout in microseconds before sleeping
+ * @busy_spin_us: busy-spin duration in microseconds before yielding
+ *
+ * Returns 0 on success, negative error on failure.
+ */
+int virtio_start_sqcq_poll(struct virtio_device *vdev,
+ unsigned int idle_timeout_us,
+ unsigned int busy_spin_us)
+{
+ struct virtio_sqcq_poll_data *sqd;
+
+ sqd = kzalloc(sizeof(*sqd), GFP_KERNEL);
+ if (!sqd)
+ return -ENOMEM;
+
+ mutex_init(&sqd->lock);
+ init_waitqueue_head(&sqd->wait);
+ INIT_LIST_HEAD(&sqd->vq_list);
+ init_completion(&sqd->exited);
+ sqd->idle_timeout_us = idle_timeout_us ? idle_timeout_us : 1000;
+ sqd->busy_spin_us = busy_spin_us;
+ sqd->dev = &vdev->dev;
+
+ sqd->thread = kthread_create(virtio_sqcq_poll_thread, sqd,
+ "virtio-sqcq/%s", dev_name(&vdev->dev));
+ if (IS_ERR(sqd->thread)) {
+ kfree(sqd);
+ return PTR_ERR(sqd->thread);
+ }
+
+ vdev->sqcq_poll_data = sqd;
+ wake_up_process(sqd->thread);
+
+ dev_info(sqd->dev, "SQ/CQ poll thread started, idle_timeout=%uus,
spin_us=%u\n",
+ sqd->idle_timeout_us, sqd->busy_spin_us);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(virtio_start_sqcq_poll);
+
+/**
+ * virtio_stop_sqcq_poll - stop the SQ/CQ poll thread for a device.
+ * @vdev: the virtio device
+ */
+void virtio_stop_sqcq_poll(struct virtio_device *vdev)
+{
+ struct virtio_sqcq_poll_data *sqd = vdev->sqcq_poll_data;
+ struct virtio_sqcq_vq_node *node, *tmp;
+
+ if (!sqd)
+ return;
+
+ set_bit(VIRTIO_SQ_THREAD_SHOULD_STOP, &sqd->state);
+
+ dev_info(sqd->dev, "SQ/CQ poll thread stopping\n");
+ virtio_sqcq_poll_wake(sqd);
+ wait_for_completion(&sqd->exited);
+
+ /* Reap kthread task_struct to avoid zombie thread leak.
+ * Thread already exited via complete(); safe to call on
+ * already-exited kthread.
+ */
+ kthread_stop(sqd->thread);
+
+ /* Free all vq nodes */
+ list_for_each_entry_safe(node, tmp, &sqd->vq_list, node) {
+ list_del(&node->node);
+ kfree(node);
+ }
+
+ vdev->sqcq_poll_data = NULL;
+ kfree(sqd);
+}
+EXPORT_SYMBOL_GPL(virtio_stop_sqcq_poll);
+
+/**
+ * virtio_sqcq_poll_register_vq - register a virtqueue with the poll thread.
+ * @vdev: the virtio device
+ * @vq: the virtqueue to register
+ *
+ * The virtqueue must have VIRTIO_F_SQCQ_POLL active.
+ * Returns 0 on success, negative error on failure.
+ */
+int virtio_sqcq_poll_register_vq(struct virtio_device *vdev,
+ struct virtqueue *vq)
+{
+ struct virtio_sqcq_poll_data *sqd = vdev->sqcq_poll_data;
+ struct virtio_sqcq_vq_node *node;
+
+ if (!sqd || !virtqueue_sqcq_poll_active(vq))
+ return -EINVAL;
+
+ node = kmalloc(sizeof(*node), GFP_KERNEL);
+ if (!node)
+ return -ENOMEM;
+
+ node->vq = vq;
+
+ mutex_lock(&sqd->lock);
+ list_add_tail(&node->node, &sqd->vq_list);
+ mutex_unlock(&sqd->lock);
+
+ dev_info(sqd->dev, "vq index=%d (%s) registered with poll thread\n",
+ vq->index, vq->name ? vq->name : "?");
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(virtio_sqcq_poll_register_vq);
+
+/**
+ * virtio_sqcq_poll_unregister_vq - unregister a virtqueue from the poll
thread.
+ * @vdev: the virtio device
+ * @vq: the virtqueue to unregister
+ */
+void virtio_sqcq_poll_unregister_vq(struct virtio_device *vdev,
+ struct virtqueue *vq)
+{
+ struct virtio_sqcq_poll_data *sqd = vdev->sqcq_poll_data;
+ struct virtio_sqcq_vq_node *node, *tmp;
+
+ if (!sqd)
+ return;
+
+ mutex_lock(&sqd->lock);
+ list_for_each_entry_safe(node, tmp, &sqd->vq_list, node) {
+ if (node->vq == vq) {
+ list_del(&node->node);
+ kfree(node);
+ break;
+ }
+ }
+ mutex_unlock(&sqd->lock);
+}
+EXPORT_SYMBOL_GPL(virtio_sqcq_poll_unregister_vq);
diff --git a/include/linux/virtio.h b/include/linux/virtio.h
index 3bbc4cb6a672..8300e0946719 100644
--- a/include/linux/virtio.h
+++ b/include/linux/virtio.h
@@ -125,6 +125,28 @@ dma_addr_t virtqueue_get_desc_addr(const struct virtqueue
*vq);
dma_addr_t virtqueue_get_avail_addr(const struct virtqueue *vq);
dma_addr_t virtqueue_get_used_addr(const struct virtqueue *vq);
+/* SQ/CQ polling (VIRTIO_F_SQCQ_POLL) */
+bool virtqueue_sqcq_poll_active(const struct virtqueue *vq);
+void virtqueue_disable_sqcq_poll(struct virtqueue *vq);
+dma_addr_t virtqueue_get_sq_dma_addr(const struct virtqueue *vq);
+dma_addr_t virtqueue_get_cq_dma_addr(const struct virtqueue *vq);
+u64 virtqueue_get_cq_detect_ns(const struct virtqueue *vq);
+int virtio_start_sqcq_poll(struct virtio_device *vdev,
+ unsigned int idle_timeout_us,
+ unsigned int busy_spin_us);
+void virtio_stop_sqcq_poll(struct virtio_device *vdev);
+void virtio_sqcq_poll_wake_thread(struct virtio_device *vdev);
+int virtio_sqcq_poll_register_vq(struct virtio_device *vdev,
+ struct virtqueue *vq);
+void virtio_sqcq_poll_unregister_vq(struct virtio_device *vdev,
+ struct virtqueue *vq);
+bool virtqueue_cq_poll_has_work(struct virtqueue *vq);
+unsigned int virtqueue_get_sq_notify_stats(struct virtqueue *vq, u64
*interval);
+unsigned long virtio_sqcq_poll_get_sleep_count(struct virtio_device *vdev);
+void virtqueue_cq_set_need_wakeup(struct virtqueue *vq);
+void virtqueue_cq_clear_need_wakeup(struct virtqueue *vq);
+bool virtqueue_cq_recheck(struct virtqueue *vq);
+
int virtqueue_resize(struct virtqueue *vq, u32 num,
void (*recycle)(struct virtqueue *vq, void *buf),
void (*recycle_done)(struct virtqueue *vq));
@@ -165,6 +187,8 @@ struct virtio_admin_cmd {
* @debugfs_dir: debugfs directory entry.
* @debugfs_filter_features: features to be filtered set by debugfs.
*/
+struct virtio_sqcq_poll_data;
+
struct virtio_device {
int index;
bool failed;
@@ -182,6 +206,7 @@ struct virtio_device {
VIRTIO_DECLARE_FEATURES(features);
void *priv;
union virtio_map vmap;
+ struct virtio_sqcq_poll_data *sqcq_poll_data; /* SQ/CQ poll thread
state */
#ifdef CONFIG_VIRTIO_DEBUG
struct dentry *debugfs_dir;
u64 debugfs_filter_features[VIRTIO_FEATURES_U64S];
diff --git a/include/linux/virtio_pci_modern.h
b/include/linux/virtio_pci_modern.h
index 9a3f2fc53bd6..36810f9d23f3 100644
--- a/include/linux/virtio_pci_modern.h
+++ b/include/linux/virtio_pci_modern.h
@@ -145,6 +145,8 @@ u16 vp_modern_config_vector(struct virtio_pci_modern_device
*mdev,
void vp_modern_queue_address(struct virtio_pci_modern_device *mdev,
u16 index, u64 desc_addr, u64 driver_addr,
u64 device_addr);
+void vp_modern_queue_address_sqcq(struct virtio_pci_modern_device *mdev,
+ u16 index, u64 sq_addr, u64 cq_addr);
void vp_modern_set_queue_enable(struct virtio_pci_modern_device *mdev,
u16 idx, bool enable);
bool vp_modern_get_queue_enable(struct virtio_pci_modern_device *mdev,
--
2.34.1