From: Laurent Vivier <lvivier@redhat.com>
To: passt-dev@passt.top
Cc: Laurent Vivier <lvivier@redhat.com>
Subject: [PATCH 6/7] vhost-user: Add per-qpair worker threads
Date: Fri, 31 Jul 2026 18:46:27 +0200 [thread overview]
Message-ID: <20260731164628.3556997-7-lvivier@redhat.com> (raw)
In-Reply-To: <20260731164628.3556997-1-lvivier@redhat.com>
Add pthread-based worker threads for vhost-user queue pair processing.
Each queue pair gets its own worker thread with a dedicated epollfd,
enabling parallel packet processing across queue pairs.
Thread 0 handles control events (listen sockets, vhost-user commands,
repair, netlink) plus queue pair 0. Threads 1-15 handle only their
assigned queue pair events.
Threads start/stop dynamically via SET_VRING_ENABLE. Virtqueue kick
eventfds are registered on the appropriate worker thread's epollfd.
Global operations (fwd_scan_ports_timer, ndp_timer) run only on
thread 0.
Thread safety relies on infrastructure from the threadsafe series
(pthread_rwlock on flow table, pthread_mutex on socket pools,
per-qpair buffers, lock-free flow migration).
Signed-off-by: Laurent Vivier <lvivier@redhat.com>
---
flow.c | 7 +--
passt.c | 129 +++++++++++++++++++++++++++++++++++++--------------
threading.c | 2 -
vhost_user.c | 32 ++++++++++---
vhost_user.h | 1 +
vu_common.c | 13 +++---
6 files changed, 130 insertions(+), 54 deletions(-)
diff --git a/flow.c b/flow.c
index 7821b3f91285..91070c9beb2e 100644
--- a/flow.c
+++ b/flow.c
@@ -457,12 +457,7 @@ static void flow_set_state(struct flow_common *f, enum flow_state state)
*/
int flow_epollfd(const struct flow_common *f)
{
- /* mapping 1:1 between qpair and threadid
- * return threading_epollfd(f->qpair);
- * but for the moment we have only one thread
- */
- (void)f;
- return threading_epollfd(THREADING_ID_DEFAULT);
+ return threading_epollfd(f->qpair);
}
/**
diff --git a/passt.c b/passt.c
index 469e2525f545..33b19264aa6f 100644
--- a/passt.c
+++ b/passt.c
@@ -55,6 +55,7 @@
#include "netlink.h"
#include "epoll_ctl.h"
#include "threading.h"
+#include "flow_table.h"
char pkt_buf[PKT_BUF_BYTES] __attribute__ ((aligned(PAGE_SIZE)));
@@ -100,18 +101,40 @@ struct passt_stats {
unsigned long events[EPOLL_NUM_TYPES];
};
+/**
+ * struct qp_context - Per-queue pair context for vhost-user operations
+ * @vdev: Pointer to vhost-user device
+ * @qpid: Queue pair identifier
+ * @stats: Per-queue statistics
+ * @flow_timer_run: Last time the flow timers ran for this queue pair
+ * @tcp_timer_run: Last time TCP timers ran for this queue pair
+ * @keepalive_run: Last time TCP keepalives ran for this queue pair
+ * @inactivity_run: Last time TCP inactivity scan ran for this queue pair
+ */
+struct qp_context {
+ struct vu_dev *vdev;
+ unsigned int qpid;
+ struct passt_stats stats;
+ struct timespec flow_timer_run;
+ struct timespec tcp_timer_run;
+ time_t keepalive_run;
+ time_t inactivity_run;
+};
+
+static struct qp_context qp_context[VHOST_USER_MAX_VQS / 2];
+
/**
* post_handler() - Run periodic and deferred tasks for L4 protocol handlers
* @c: Execution context
* @now: Current timestamp
- * @timer_run: Last time the flow timers ran
+ * @flow_timer_run: Last time the flow timers ran
* @tcp_timer_run: Last time TCP timers ran
* @keepalive_run: Last time keepalives ran
* @inactivity_run: Last time inactivity scan ran
* @qpair: Queue pair to process
*/
static void post_handler(struct ctx *c, const struct timespec *now,
- struct timespec *timer_run,
+ struct timespec *flow_timer_run,
struct timespec *tcp_timer_run,
time_t *keepalive_run,
time_t *inactivity_run, unsigned int qpair)
@@ -120,11 +143,13 @@ static void post_handler(struct ctx *c, const struct timespec *now,
tcp_defer_handler(c, now, tcp_timer_run, keepalive_run,
inactivity_run, qpair);
- flow_defer_handler(c, now, timer_run, qpair);
- fwd_scan_ports_timer(c, now);
+ flow_defer_handler(c, now, flow_timer_run, qpair);
+ if (qpair == 0) {
+ fwd_scan_ports_timer(c, now);
- if (!c->no_ndp)
- ndp_timer(c, now);
+ if (!c->no_ndp)
+ ndp_timer(c, now);
+ }
}
/**
@@ -172,16 +197,14 @@ static void exit_handler(int signal)
/**
* print_stats() - Print event statistics table to stderr
* @c: Execution context
- * @stats: Event counters
* @now: Current timestamp
*/
-static void print_stats(const struct ctx *c, const struct passt_stats *stats,
- const struct timespec *now)
+static void print_stats(const struct ctx *c, const struct timespec *now)
{
static struct timespec before;
- static int lines_printed;
+ static int lines_printed = 20;
long long elapsed_ns;
- int i;
+ unsigned int i, j;
if (!c->stats)
return;
@@ -194,11 +217,12 @@ static void print_stats(const struct ctx *c, const struct passt_stats *stats,
before = *now;
- if (!(lines_printed % 20)) {
+ if (lines_printed >= 20) {
+ lines_printed = 0;
/* Table header */
for (i = 1; i < EPOLL_NUM_TYPES; i++) {
- int j;
+ FPRINTF(stderr, " \t");
for (j = 0; j < i * (6 + 1); j++) {
if (j && !(j % (6 + 1)))
FPRINTF(stderr, "|");
@@ -209,11 +233,19 @@ static void print_stats(const struct ctx *c, const struct passt_stats *stats,
}
}
- FPRINTF(stderr, " ");
- for (i = 1; i < EPOLL_NUM_TYPES; i++)
- FPRINTF(stderr, " %6lu", stats->events[i]);
- FPRINTF(stderr, "\n");
- lines_printed++;
+ for (j = 0; j < VHOST_USER_MAX_VQS / 2; j++) {
+
+ if (!threading_is_active(j))
+ continue;
+
+ FPRINTF(stderr, "%u\t ", j);
+
+ for (i = 1; i < EPOLL_NUM_TYPES; i++)
+ FPRINTF(stderr, " %6lu", qp_context[j].stats.events[i]);
+
+ FPRINTF(stderr, "\n");
+ lines_printed++;
+ }
}
/**
@@ -224,10 +256,8 @@ static void print_stats(const struct ctx *c, const struct passt_stats *stats,
*/
static void passt_worker(void *opaque, int nfds, struct epoll_event *events)
{
- static time_t keepalive_run, inactivity_run;
- static struct passt_stats stats = { 0 };
- static struct timespec flow_timer_run, tcp_timer_run;
- struct ctx *c = opaque;
+ struct qp_context *qpc = opaque;
+ struct ctx *c = qpc->vdev->context;
struct timespec now;
int i;
@@ -259,7 +289,7 @@ static void passt_worker(void *opaque, int nfds, struct epoll_event *events)
pasta_netns_quit_timer_handler(c, ref);
break;
case EPOLL_TYPE_TCP:
- tcp_sock_handler(c, ref, eventmask, &now, QPAIR_DEFAULT);
+ tcp_sock_handler(c, ref, eventmask, &now, qpc->qpid);
break;
case EPOLL_TYPE_TCP_SPLICE:
tcp_splice_sock_handler(c, ref, eventmask, &now);
@@ -268,24 +298,24 @@ static void passt_worker(void *opaque, int nfds, struct epoll_event *events)
tcp_listen_handler(c, ref, &now);
break;
case EPOLL_TYPE_TCP_TIMER:
- tcp_timer_handler(c, ref, &now, QPAIR_DEFAULT);
+ tcp_timer_handler(c, ref, &now, qpc->qpid);
break;
case EPOLL_TYPE_UDP_LISTEN:
udp_listen_sock_handler(c, ref, eventmask, &now,
- QPAIR_DEFAULT);
+ qpc->qpid);
break;
case EPOLL_TYPE_UDP:
udp_sock_handler(c, ref, eventmask, &now,
- QPAIR_DEFAULT);
+ qpc->qpid);
break;
case EPOLL_TYPE_PING:
- icmp_sock_handler(c, ref, &now, QPAIR_DEFAULT);
+ icmp_sock_handler(c, ref, &now, qpc->qpid);
break;
case EPOLL_TYPE_VHOST_CMD:
vu_control_handler(c->vdev, c->fd_tap, eventmask);
break;
case EPOLL_TYPE_VHOST_KICK:
- vu_kick_cb(c->vdev, ref, &now);
+ vu_kick_cb(qpc->vdev, ref, &now);
break;
case EPOLL_TYPE_REPAIR_LISTEN:
repair_listen_handler(c, eventmask);
@@ -306,14 +336,16 @@ static void passt_worker(void *opaque, int nfds, struct epoll_event *events)
/* Can't happen */
assert(0);
}
- stats.events[ref.type]++;
- print_stats(c, &stats, &now);
+
+ qpc->stats.events[ref.type]++;
+ print_stats(c, &now);
}
- post_handler(c, &now, &flow_timer_run, &tcp_timer_run,
- &keepalive_run, &inactivity_run, QPAIR_DEFAULT);
+ post_handler(c, &now, &qpc->flow_timer_run, &qpc->tcp_timer_run,
+ &qpc->keepalive_run, &qpc->inactivity_run, qpc->qpid);
- migrate_handler(c, &now);
+ if (qpc->qpid == 0)
+ migrate_handler(c, &now);
}
/**
@@ -376,6 +408,7 @@ int main(int argc, char **argv)
madvise(pkt_buf, sizeof(pkt_buf), MADV_HUGEPAGE);
threading_init();
+ vu_preinit(c);
if (getrlimit(RLIMIT_NOFILE, &limit))
die_perror("Failed to get maximum value of open files limit");
@@ -445,6 +478,34 @@ int main(int argc, char **argv)
isolate_postfork(c);
- threading_worker_set(THREADING_ID_DEFAULT, passt_worker, c);
+ /* thread #0 is specific to main process */
+ if (c->mode == MODE_VU) {
+ unsigned int i;
+
+ for (i = 0; i < VHOST_USER_MAX_VQS / 2; i++) {
+ struct qp_context *qpc = &qp_context[i];
+
+ qpc->vdev = c->vdev;
+ qpc->qpid = i;
+ qpc->flow_timer_run = now;
+ qpc->tcp_timer_run = now;
+ qpc->keepalive_run = 0;
+ qpc->inactivity_run = 0;
+
+ threading_worker_set(i, passt_worker, qpc);
+ }
+ } else {
+ struct qp_context *qpc = &qp_context[0];
+
+ qpc->vdev = c->vdev;
+ qpc->qpid = 0;
+ qpc->flow_timer_run = now;
+ qpc->tcp_timer_run = now;
+ qpc->keepalive_run = 0;
+ qpc->inactivity_run = 0;
+
+ threading_worker_set(THREADING_ID_DEFAULT, passt_worker, qpc);
+ }
+
threading_start_thread(THREADING_ID_DEFAULT);
}
diff --git a/threading.c b/threading.c
index e1ed644f5864..5f3453b02d99 100644
--- a/threading.c
+++ b/threading.c
@@ -169,7 +169,6 @@ void threading_start_thread(unsigned int threadid)
* threading_stop_thread() - Stop and join a worker thread
* @threadid: Thread index to stop
*/
-/* cppcheck-suppress unusedFunction */
void threading_stop_thread(unsigned int threadid)
{
if (threadid >= ARRAY_SIZE(threads))
@@ -193,7 +192,6 @@ void threading_stop_thread(unsigned int threadid)
* Return: true if the thread at the given index has been created and is active,
* false otherwise
*/
-/* cppcheck-suppress unusedFunction */
bool threading_is_active(unsigned int threadid)
{
if (threadid >= ARRAY_SIZE(threads))
diff --git a/vhost_user.c b/vhost_user.c
index ad316aee12c3..ad5e062726a9 100644
--- a/vhost_user.c
+++ b/vhost_user.c
@@ -45,6 +45,7 @@
#include "migrate.h"
#include "epoll_ctl.h"
#include "threading.h"
+#include "vu_common.h"
/* vhost-user version we are compatible with */
#define VHOST_USER_VERSION 1
@@ -737,7 +738,7 @@ static bool vu_get_vring_base_exec(struct vu_dev *vdev,
vdev->vq[idx].call_fd = -1;
}
if (vdev->vq[idx].kick_fd != -1) {
- epoll_del(threading_epollfd(THREADING_ID_DEFAULT), vdev->vq[idx].kick_fd);
+ epoll_del(threading_epollfd(idx / 2), vdev->vq[idx].kick_fd);
close(vdev->vq[idx].kick_fd);
vdev->vq[idx].kick_fd = -1;
}
@@ -758,7 +759,7 @@ static void vu_set_watch(const struct vu_dev *vdev, int idx)
.queue = idx
};
- epoll_add(threading_epollfd(THREADING_ID_DEFAULT), EPOLLIN, ref);
+ epoll_add(threading_epollfd(idx / 2), EPOLLIN, ref);
}
/**
@@ -803,7 +804,7 @@ static bool vu_set_vring_kick_exec(struct vu_dev *vdev,
vu_check_queue_msg_file(vmsg);
if (vdev->vq[idx].kick_fd != -1) {
- epoll_del(threading_epollfd(THREADING_ID_DEFAULT), vdev->vq[idx].kick_fd);
+ epoll_del(threading_epollfd(idx / 2), vdev->vq[idx].kick_fd);
close(vdev->vq[idx].kick_fd);
vdev->vq[idx].kick_fd = -1;
}
@@ -955,6 +956,8 @@ static bool vu_get_queue_num_exec(struct vu_dev *vdev,
* @vmsg: vhost-user message
*
* Return: false as no reply is requested
+ *
+ * #syscalls:vu madvise
*/
static bool vu_set_vring_enable_exec(struct vu_dev *vdev,
struct vhost_user_msg *vmsg)
@@ -968,7 +971,16 @@ static bool vu_set_vring_enable_exec(struct vu_dev *vdev,
if (idx >= VHOST_USER_MAX_VQS)
die("Invalid vring_enable index: %u", idx);
+ if (vdev->vq[idx].enable == enable)
+ return false;
+
vdev->vq[idx].enable = enable;
+
+ if (vdev->vq[idx & ~1].enable || vdev->vq[(idx & ~1) + 1].enable)
+ threading_start_thread(idx / 2);
+ else
+ threading_stop_thread(idx / 2);
+
return false;
}
@@ -1047,6 +1059,16 @@ static bool vu_check_device_state_exec(struct vu_dev *vdev,
return true;
}
+/**
+ * vu_preinit() - Pre-initialize vhost-user device storage
+ * @c: execution context
+ */
+void vu_preinit(struct ctx *c)
+{
+ c->vdev = &vdev_storage;
+ c->vdev->context = c;
+}
+
/**
* vu_init() - Initialize vhost-user device structure
* @c: execution context
@@ -1055,8 +1077,6 @@ void vu_init(struct ctx *c)
{
unsigned i;
- c->vdev = &vdev_storage;
- c->vdev->context = c;
for (i = 0; i < VHOST_USER_MAX_VQS; i++) {
c->vdev->vq[i] = (struct vu_virtq){
.call_fd = -1,
@@ -1096,7 +1116,7 @@ void vu_cleanup(struct vu_dev *vdev)
vq->err_fd = -1;
}
if (vq->kick_fd != -1) {
- epoll_del(threading_epollfd(THREADING_ID_DEFAULT), vq->kick_fd);
+ epoll_del(threading_epollfd(i / 2), vq->kick_fd);
close(vq->kick_fd);
vq->kick_fd = -1;
}
diff --git a/vhost_user.h b/vhost_user.h
index d2e51d3e86c3..1a5614e83c32 100644
--- a/vhost_user.h
+++ b/vhost_user.h
@@ -230,6 +230,7 @@ static inline bool vu_queue_started(const struct vu_virtq *vq)
}
void vu_print_capabilities(void);
+void vu_preinit(struct ctx *c);
void vu_init(struct ctx *c);
void vu_cleanup(struct vu_dev *vdev);
void vu_log_write(const struct vu_dev *vdev, uint64_t address,
diff --git a/vu_common.c b/vu_common.c
index 0cd19b36737d..c3e3dfc05f26 100644
--- a/vu_common.c
+++ b/vu_common.c
@@ -177,7 +177,7 @@ static void vu_handle_tx(struct vu_dev *vdev, int index,
assert(QPAIR_IS_FROMGUEST(index));
- tap_flush_pools(QPAIR_DEFAULT);
+ tap_flush_pools(QPAIR_FROM_QUEUE(index));
count = 0;
out_sg_count = 0;
@@ -199,12 +199,13 @@ static void vu_handle_tx(struct vu_dev *vdev, int index,
data = IOV_TAIL(elem[count].out_sg, elem[count].out_num, 0);
if (IOV_DROP_HEADER(&data, struct virtio_net_hdr_mrg_rxbuf)) {
- tap_add_packet(vdev->context, QPAIR_DEFAULT, &data, now);
+ tap_add_packet(vdev->context, QPAIR_FROM_QUEUE(index),
+ &data, now);
}
count++;
}
- tap_handler(vdev->context, QPAIR_DEFAULT, now);
+ tap_handler(vdev->context, QPAIR_FROM_QUEUE(index), now);
if (count) {
int i;
@@ -230,12 +231,12 @@ void vu_kick_cb(struct vu_dev *vdev, union epoll_ref ref,
rc = eventfd_read(ref.fd, &kick_data);
if (rc == -1)
- die_perror("vhost-user kick eventfd_read()");
+ return;
trace("vhost-user: got kick_data: %016"PRIx64" idx: %d",
kick_data, ref.queue);
- if (QPAIR_IS_FROMGUEST(ref.queue))
- vu_handle_tx(vdev, ref.queue, now);
+
+ vu_handle_tx(vdev, ref.queue, now);
}
/**
--
2.54.0
next prev parent reply other threads:[~2026-07-31 16:46 UTC|newest]
Thread overview: 8+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-31 16:46 [PATCH 0/7] multithreading: Add worker threads for queue pair processing Laurent Vivier
2026-07-31 16:46 ` [PATCH 1/7] vhost_user: Reset vq enable flag in vu_cleanup() Laurent Vivier
2026-07-31 16:46 ` [PATCH 2/7] threading: Add basic threading infrastructure Laurent Vivier
2026-07-31 16:46 ` [PATCH 3/7] passt: Integrate main event loop with " Laurent Vivier
2026-07-31 16:46 ` [PATCH 4/7] flow: Delegate epoll file descriptor management to threading subsystem Laurent Vivier
2026-07-31 16:46 ` [PATCH 5/7] ctx: Remove epollfd from context structure Laurent Vivier
2026-07-31 16:46 ` Laurent Vivier [this message]
2026-07-31 16:46 ` [PATCH 7/7] virtio: Prevent crash on virtqueue exhaustion with temporary workaround Laurent Vivier
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260731164628.3556997-7-lvivier@redhat.com \
--to=lvivier@redhat.com \
--cc=passt-dev@passt.top \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
Code repositories for project(s) associated with this public inbox
https://passt.top/passt
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for IMAP folder(s).