Netdev List
 help / color / mirror / Atom feed
* [RFC PATCH 0/3] virtio_net: introduce an RX queue watchdog for stuck detection
@ 2026-08-28  7:43 Longjun Tang
  2026-08-28  7:43 ` [RFC PATCH 1/3] virtio: add virtqueue_get_last_used_idx() helper Longjun Tang
                   ` (2 more replies)
  0 siblings, 3 replies; 4+ messages in thread
From: Longjun Tang @ 2026-08-28  7:43 UTC (permalink / raw)
  To: mst, kuba; +Cc: jasowangio, xuanzhuo, virtualization, netdev, tanglongjun

From: Longjun Tang <tanglongjun@kylinos.cn>

The virtio_net RX path relies on backend interrupt notifications. 
The backend(vhost_net/vhost-user) appends buffers to the used ring
and then notifies the guest, relying on the hypervisor(KVM) to inject
the interrupt. On receiving it, virtio_net schedules NAPI, drains the
used ring and refills descriptors.

If the hypervisor fails to inject the interrupt(e.g. a transient KVM
failure), the guest never schedules NAPI, so it neither consumes buffers
nor returns descriptors. The backend keeps appending until the ring is full,
then stops and, with no free descriptors left, also stops sending
notifications. Both sides now wait for the other, and the RX queue is
permanently stuck.

The root cause is the lost interrupt in the hypervisor, and the proper
fix belongs there. Nevertheless, I believe virtio_net needs a way to 
observe and diagnose it. Today, once a queue hangs, there is no signal
to the operator that anything is wrong: the RX path has no equivalent of
the TX watchdog (ndo_tx_timeout). This series lets virtio_net detect a 
stuck RX queue. It is detection only: the driver records the event and 
logs a warning, leaving recovery to a follow-up if desired.


Implementation
--------------

Patch 1 exports virtqueue_get_last_used_idx(), a read-only accessor for
the last_used_idx. Together with the existing virtqueue_poll(), it lets
a driver ask whether the device has produced buffers that the driver has
not consumed yet ("backlog").

Patch 2 adds a per-device RX watchdog that scans all RX queues once per
second. A queue is considered stuck when, for rx_watchdog_timeo seconds
(default 5, 0 disables it), all of the following hold simultaneously:
  - the queue has a non-zero backlog (used.idx != last_used_idx);
  - the driver makes no consumption progress (last_used_idx unchanged);
  - no new interrupt arrives (rq->calls unchanged).
On detection it logs a warning, rate-limited to once per timeout.

Patch 3 adds a per-queue rx_timeouts statistic, exposed via ethtool -S,
so the number of watchdog events can be observed per queue.


RFC
---

This is sent as an RFC to gather feedback on a few open points:

  - Scope: this series only detects the stuck queue. Is it better to
    keep detection and recovery separate, or should recovery (forcing a
    NAPI poll to drain the queue) be included?

  - Default: rx_watchdog_timeo defaults to 5 seconds (enabled). Should
    the watchdog be enabled by default, or disabled unless explicitly
    requested?

Parts of this series, including portions of this cover letter, were drafted
with AI assistance. I have carefully reviewed everything; 
questions and comments are welcome. 

Thanks!
Lange

Longjun Tang (3):
  virtio: add virtqueue_get_last_used_idx() helper
  virtio_net: introduce an RX queue watchdog for stuck detection
  virtio_net: add rx_timeouts per-queue statistic

 drivers/net/virtio_net.c     | 103 +++++++++++++++++++++++++++++++++++
 drivers/virtio/virtio_ring.c |  23 +++++++-
 include/linux/virtio.h       |   2 +
 3 files changed, 126 insertions(+), 2 deletions(-)


base-commit: 2709dd5ae32f0828f386327c76bba9f39f63a1c6
-- 
2.43.0


^ permalink raw reply	[flat|nested] 4+ messages in thread

* [RFC PATCH 1/3] virtio: add virtqueue_get_last_used_idx() helper
  2026-08-28  7:43 [RFC PATCH 0/3] virtio_net: introduce an RX queue watchdog for stuck detection Longjun Tang
@ 2026-08-28  7:43 ` Longjun Tang
  2026-08-28  7:43 ` [RFC PATCH 2/3] virtio_net: introduce an RX queue watchdog for stuck detection Longjun Tang
  2026-08-28  7:43 ` [RFC PATCH 3/3] virtio_net: add rx_timeouts per-queue statistic Longjun Tang
  2 siblings, 0 replies; 4+ messages in thread
From: Longjun Tang @ 2026-08-28  7:43 UTC (permalink / raw)
  To: mst, kuba; +Cc: jasowangio, xuanzhuo, virtualization, netdev, tanglongjun

From: Longjun Tang <tanglongjun@kylinos.cn>

Export a read-only accessor for the last consumed used ring index.
It's useful for drivers that need to detect whether the device has
produced buffers that have not been consumed yet, by passing the
returned value to virtqueue_poll().

The split ring now writes the field with WRITE_ONCE() (the packed
ring already did) to document that the new reader can run concurrently
with the poll path.

Assisted-by: Kilo:deepseek-v4-pro
Signed-off-by: Longjun Tang <tanglongjun@kylinos.cn>
---
 drivers/virtio/virtio_ring.c | 23 +++++++++++++++++++++--
 include/linux/virtio.h       |  2 ++
 2 files changed, 23 insertions(+), 2 deletions(-)

diff --git a/drivers/virtio/virtio_ring.c b/drivers/virtio/virtio_ring.c
index 5c169fbb418a..7b21fe573cde 100644
--- a/drivers/virtio/virtio_ring.c
+++ b/drivers/virtio/virtio_ring.c
@@ -1001,7 +1001,7 @@ static void *virtqueue_get_buf_ctx_split(struct vring_virtqueue *vq,
 	/* detach_buf_split clears data, so grab it now. */
 	ret = vq->split.desc_state[i].data;
 	detach_buf_split(vq, i, ctx);
-	vq->last_used_idx++;
+	WRITE_ONCE(vq->last_used_idx, vq->last_used_idx + 1);
 	/* 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. */
@@ -1068,7 +1068,7 @@ static void *virtqueue_get_buf_ctx_split_in_order(struct vring_virtqueue *vq,
 	ret = vq->split.desc_state[last_used].data;
 	detach_buf_split_in_order(vq, last_used, ctx);
 
-	vq->last_used_idx++;
+	WRITE_ONCE(vq->last_used_idx, vq->last_used_idx + 1);
 	vq->last_used += (vq->vq.num_free - num_free);
 	/* If we expect an interrupt for the next entry, tell host
 	 * by writing event index and flush out the write before
@@ -3197,6 +3197,25 @@ bool virtqueue_poll(struct virtqueue *_vq, unsigned int last_used_idx)
 }
 EXPORT_SYMBOL_GPL(virtqueue_poll);
 
+/**
+ * virtqueue_get_last_used_idx - get the last consumed used ring index
+ * @_vq: the struct virtqueue we're talking about.
+ *
+ * Returns the index of the most recently consumed used buffer. It is
+ * mainly useful to detect whether the device has produced buffers that
+ * have not been consumed yet, by passing the returned value to
+ * virtqueue_poll().
+ *
+ * This does not need to be serialized.
+ */
+unsigned int virtqueue_get_last_used_idx(const struct virtqueue *_vq)
+{
+	const struct vring_virtqueue *vq = to_vvq(_vq);
+
+	return READ_ONCE(vq->last_used_idx);
+}
+EXPORT_SYMBOL_GPL(virtqueue_get_last_used_idx);
+
 /**
  * virtqueue_enable_cb - restart callbacks after disable_cb.
  * @_vq: the struct virtqueue we're talking about.
diff --git a/include/linux/virtio.h b/include/linux/virtio.h
index f923e42cfd01..1aed4d9235c8 100644
--- a/include/linux/virtio.h
+++ b/include/linux/virtio.h
@@ -112,6 +112,8 @@ unsigned virtqueue_enable_cb_prepare(struct virtqueue *vq);
 
 bool virtqueue_poll(struct virtqueue *vq, unsigned);
 
+unsigned int virtqueue_get_last_used_idx(const struct virtqueue *vq);
+
 bool virtqueue_enable_cb_delayed(struct virtqueue *vq);
 
 void *virtqueue_detach_unused_buf(struct virtqueue *vq);
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 4+ messages in thread

* [RFC PATCH 2/3] virtio_net: introduce an RX queue watchdog for stuck detection
  2026-08-28  7:43 [RFC PATCH 0/3] virtio_net: introduce an RX queue watchdog for stuck detection Longjun Tang
  2026-08-28  7:43 ` [RFC PATCH 1/3] virtio: add virtqueue_get_last_used_idx() helper Longjun Tang
@ 2026-08-28  7:43 ` Longjun Tang
  2026-08-28  7:43 ` [RFC PATCH 3/3] virtio_net: add rx_timeouts per-queue statistic Longjun Tang
  2 siblings, 0 replies; 4+ messages in thread
From: Longjun Tang @ 2026-08-28  7:43 UTC (permalink / raw)
  To: mst, kuba; +Cc: jasowangio, xuanzhuo, virtualization, netdev, tanglongjun

From: Longjun Tang <tanglongjun@kylinos.cn>

The backend raises an interrupt after adding buffers to the used ring,
and the hypervisor injects it. If one is lost (e.g., a transient KVM
failure), NAPI will not be scheduled and the driver stops consuming
buffers. The ring eventually fills up, the backend stops notifying, and
the queue is left permanently stuck.

Add a per-device watchdog that scans the RX queues once per second and
detects a queue that has a non-zero backlog while making no consumption
progress and receiving no new interrupt for rx_watchdog_timeo seconds
(default 5, 0 disables it). On detection it logs a warning, rate-limited
to once per timeout.

Assisted-by: Kilo:deepseek-v4-pro
Signed-off-by: Longjun Tang <tanglongjun@kylinos.cn>
---
 drivers/net/virtio_net.c | 96 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 96 insertions(+)

diff --git a/drivers/net/virtio_net.c b/drivers/net/virtio_net.c
index e34c52d059d3..02d0c4a67360 100644
--- a/drivers/net/virtio_net.c
+++ b/drivers/net/virtio_net.c
@@ -36,6 +36,10 @@ module_param(csum, bool, 0444);
 module_param(gso, bool, 0444);
 module_param(napi_tx, bool, 0644);
 
+/* RX queue stall timeout in seconds; 0 disables the watchdog. */
+static unsigned int rx_watchdog_timeo = 5;
+module_param(rx_watchdog_timeo, uint, 0644);
+
 #define VIRTIO_OFFLOAD_MAP_MIN	46
 #define VIRTIO_OFFLOAD_MAP_MAX	47
 #define VIRTIO_FEATURES_MAP_MIN	65
@@ -330,6 +334,11 @@ struct receive_queue {
 	/* The number of rx notifications */
 	u16 calls;
 
+	/* RX watchdog state for lost-interrupt detection. */
+	u16 watchdog_last_used_idx;
+	u16 watchdog_calls;
+	unsigned long watchdog_jiffies;
+
 	/* Is dynamic interrupt moderation enabled? */
 	bool dim_enabled;
 
@@ -441,6 +450,9 @@ struct virtnet_info {
 	/* Work struct for setting rx mode */
 	struct work_struct rx_mode_work;
 
+	/* RX watchdog timer for lost-interrupt detection */
+	struct timer_list rx_watchdog;
+
 	/* OK to queue work setting RX mode? */
 	bool rx_mode_work_enabled;
 
@@ -3046,6 +3058,85 @@ static int virtnet_poll(struct napi_struct *napi, int budget)
 	return received;
 }
 
+/*
+ * The RX path is entirely event driven: the driver relies on the
+ * hypervisor injecting an interrupt for every used buffer. If an
+ * interrupt is lost (e.g. a transient KVM failure), NAPI is never
+ * scheduled, the driver stops consuming buffers, the backend fills the
+ * ring and, once it runs out of descriptors, stops sending further
+ * notifications. Both sides then wait for the other and the queue is
+ * permanently stuck.
+ *
+ * This watchdog detects that state: a queue is considered stalled when
+ * it has a non-zero backlog, makes no consumption progress and receives
+ * no new interrupt for rx_watchdog_timeo seconds. On detection it logs
+ * a warning.
+ */
+static void virtnet_rx_watchdog(struct timer_list *t)
+{
+	struct virtnet_info *vi = timer_container_of(vi, t, rx_watchdog);
+	unsigned long timeout = rx_watchdog_timeo * HZ;
+	int i;
+
+	if (!rx_watchdog_timeo)
+		return;
+
+	for (i = 0; i < vi->curr_queue_pairs; i++) {
+		struct receive_queue *rq = &vi->rq[i];
+		u16 last_used = virtqueue_get_last_used_idx(rq->vq);
+		u16 calls = rq->calls;
+		bool backlog = virtqueue_poll(rq->vq, last_used);
+
+		if (!backlog || last_used != rq->watchdog_last_used_idx ||
+		    calls != rq->watchdog_calls) {
+			/* No pending data, or the queue made progress, or a
+			 * new interrupt arrived: restart the window.
+			 */
+			rq->watchdog_last_used_idx = last_used;
+			rq->watchdog_calls = calls;
+			rq->watchdog_jiffies = jiffies;
+			continue;
+		}
+
+		if (time_after(jiffies, rq->watchdog_jiffies + timeout)) {
+			unsigned int stall_ms =
+				jiffies_to_msecs(jiffies - rq->watchdog_jiffies);
+
+			netdev_warn(vi->dev, "RX queue %u stalled for %u ms\n",
+				    i, stall_ms);
+
+			/* Rate-limit to one event per timeout. */
+			rq->watchdog_jiffies = jiffies;
+		}
+	}
+
+	mod_timer(&vi->rx_watchdog, jiffies + HZ);
+}
+
+static void virtnet_rx_watchdog_start(struct virtnet_info *vi)
+{
+	int i;
+
+	if (!rx_watchdog_timeo)
+		return;
+
+	for (i = 0; i < vi->curr_queue_pairs; i++) {
+		struct receive_queue *rq = &vi->rq[i];
+
+		rq->watchdog_last_used_idx =
+			virtqueue_get_last_used_idx(rq->vq);
+		rq->watchdog_calls = rq->calls;
+		rq->watchdog_jiffies = jiffies;
+	}
+
+	mod_timer(&vi->rx_watchdog, jiffies + HZ);
+}
+
+static void virtnet_rx_watchdog_stop(struct virtnet_info *vi)
+{
+	timer_delete_sync(&vi->rx_watchdog);
+}
+
 static void virtnet_disable_queue_pair(struct virtnet_info *vi, int qp_index)
 {
 	virtnet_napi_tx_disable(&vi->sq[qp_index]);
@@ -3209,6 +3300,8 @@ static int virtnet_open(struct net_device *dev)
 		netif_carrier_on(dev);
 	}
 
+	virtnet_rx_watchdog_start(vi);
+
 	return 0;
 
 err_enable_qp:
@@ -3802,6 +3895,8 @@ static int virtnet_close(struct net_device *dev)
 	struct virtnet_info *vi = netdev_priv(dev);
 	int i;
 
+	virtnet_rx_watchdog_stop(vi);
+
 	/* Prevent the config change callback from changing carrier
 	 * after close
 	 */
@@ -6869,6 +6964,7 @@ static int virtnet_probe(struct virtio_device *vdev)
 
 	INIT_WORK(&vi->config_work, virtnet_config_changed_work);
 	INIT_WORK(&vi->rx_mode_work, virtnet_rx_mode_work);
+	timer_setup(&vi->rx_watchdog, virtnet_rx_watchdog, 0);
 
 	if (virtio_has_feature(vdev, VIRTIO_NET_F_MRG_RXBUF)) {
 		vi->mergeable_rx_bufs = true;
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 4+ messages in thread

* [RFC PATCH 3/3] virtio_net: add rx_timeouts per-queue statistic
  2026-08-28  7:43 [RFC PATCH 0/3] virtio_net: introduce an RX queue watchdog for stuck detection Longjun Tang
  2026-08-28  7:43 ` [RFC PATCH 1/3] virtio: add virtqueue_get_last_used_idx() helper Longjun Tang
  2026-08-28  7:43 ` [RFC PATCH 2/3] virtio_net: introduce an RX queue watchdog for stuck detection Longjun Tang
@ 2026-08-28  7:43 ` Longjun Tang
  2 siblings, 0 replies; 4+ messages in thread
From: Longjun Tang @ 2026-08-28  7:43 UTC (permalink / raw)
  To: mst, kuba; +Cc: jasowangio, xuanzhuo, virtualization, netdev, tanglongjun

From: Longjun Tang <tanglongjun@kylinos.cn>

Add an rx_timeouts field to the per-queue RX statistics, expose
it via ethtool and increment it when the RX watchdog fires,
so RX watchdog timeouts can be observed per queue.

Assisted-by: Kilo:deepseek-v4-pro
Signed-off-by: Longjun Tang <tanglongjun@kylinos.cn>
---
 drivers/net/virtio_net.c | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/drivers/net/virtio_net.c b/drivers/net/virtio_net.c
index 02d0c4a67360..44a093604d82 100644
--- a/drivers/net/virtio_net.c
+++ b/drivers/net/virtio_net.c
@@ -134,6 +134,7 @@ struct virtnet_rq_stats {
 	u64_stats_t xdp_redirects;
 	u64_stats_t xdp_drops;
 	u64_stats_t kicks;
+	u64_stats_t rx_timeouts;
 };
 
 #define VIRTNET_SQ_STAT(name, m) {name, offsetof(struct virtnet_sq_stats, m), -1}
@@ -167,6 +168,7 @@ static const struct virtnet_stat_desc virtnet_rq_stats_desc[] = {
 	VIRTNET_RQ_STAT("xdp_redirects", xdp_redirects),
 	VIRTNET_RQ_STAT("xdp_drops",     xdp_drops),
 	VIRTNET_RQ_STAT("kicks",         kicks),
+	VIRTNET_RQ_STAT("rx_timeouts",   rx_timeouts),
 };
 
 static const struct virtnet_stat_desc virtnet_sq_stats_desc_qstat[] = {
@@ -3069,8 +3071,9 @@ static int virtnet_poll(struct napi_struct *napi, int budget)
  *
  * This watchdog detects that state: a queue is considered stalled when
  * it has a non-zero backlog, makes no consumption progress and receives
- * no new interrupt for rx_watchdog_timeo seconds. On detection it logs
- * a warning.
+ * no new interrupt for rx_watchdog_timeo seconds. On detection it
+ * records the event in the per-queue rx_timeouts statistic and logs a
+ * warning.
  */
 static void virtnet_rx_watchdog(struct timer_list *t)
 {
@@ -3102,6 +3105,10 @@ static void virtnet_rx_watchdog(struct timer_list *t)
 			unsigned int stall_ms =
 				jiffies_to_msecs(jiffies - rq->watchdog_jiffies);
 
+			u64_stats_update_begin(&rq->stats.syncp);
+			u64_stats_inc(&rq->stats.rx_timeouts);
+			u64_stats_update_end(&rq->stats.syncp);
+
 			netdev_warn(vi->dev, "RX queue %u stalled for %u ms\n",
 				    i, stall_ms);
 
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2026-08-28  7:44 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-28  7:43 [RFC PATCH 0/3] virtio_net: introduce an RX queue watchdog for stuck detection Longjun Tang
2026-08-28  7:43 ` [RFC PATCH 1/3] virtio: add virtqueue_get_last_used_idx() helper Longjun Tang
2026-08-28  7:43 ` [RFC PATCH 2/3] virtio_net: introduce an RX queue watchdog for stuck detection Longjun Tang
2026-08-28  7:43 ` [RFC PATCH 3/3] virtio_net: add rx_timeouts per-queue statistic Longjun Tang

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox