* [PATCH net-next v6 0/6] selftests: net: make iou-zcrx tests multithreaded
@ 2026-08-31 17:34 Juanlu Herrero
2026-08-31 17:34 ` [PATCH net-next v6 1/6] selftests: net: fix get_refill_ring_size() to use its local variable Juanlu Herrero
` (5 more replies)
0 siblings, 6 replies; 15+ messages in thread
From: Juanlu Herrero @ 2026-08-31 17:34 UTC (permalink / raw)
To: dw, netdev
Cc: io-uring, linux-kselftest, linux-kernel, kuba, asml.silence,
pabeni, shuah, juanlu
This series extends the iou-zcrx selftests to be multithreaded while
preserving existing single-threaded tests.
Patches 1 and 2 are prep fixes/cleanups. Patch 3 refactors the server's
global state. Patches 4 and 5 add the multithreaded client and server.
Finally, patch 6 adds the python side multithreaded test case.
Changes since v5:
- early return in process_recvzc in the oneshot recvs branch (David)
v5: https://lore.kernel.org/all/20260814012348.46958-1-juanlu@fastmail.com/
v4: https://lore.kernel.org/netdev/20260729221825.42773-1-juanlu@fastmail.com/
v3: https://lore.kernel.org/netdev/20260722203950.58550-1-juanlu@fastmail.com/
v2: https://lore.kernel.org/netdev/cover.1776444379.git.juanlu@fastmail.com/
v1: https://lore.kernel.org/netdev/20260408163816.2760-1-juanlu@fastmail.com/
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Juanlu Herrero <juanlu@fastmail.com>
---
Juanlu Herrero (6):
selftests: net: fix get_refill_ring_size() to use its local variable
selftests: net: remove unused variable in process_recvzc()
selftests: net: refactor server state into struct thread_ctx
selftests: net: add multithread client support to iou-zcrx
selftests: net: add multithread server support to iou-zcrx
selftests: net: add rss_multiqueue test variant to iou-zcrx
tools/testing/selftests/drivers/net/hw/Makefile | 7 +-
tools/testing/selftests/drivers/net/hw/iou-zcrx.c | 407 ++++++++++++++-------
tools/testing/selftests/drivers/net/hw/iou-zcrx.py | 50 ++-
3 files changed, 333 insertions(+), 131 deletions(-)
---
base-commit: 1bb784eb6e38fd73143f021608e4ef3095d0c0d7
change-id: 20260817-iou-zcrx-e8e46108a0bc
Best regards,
--
Juanlu Herrero <juanlu@fastmail.com>
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH net-next v6 1/6] selftests: net: fix get_refill_ring_size() to use its local variable
2026-08-31 17:34 [PATCH net-next v6 0/6] selftests: net: make iou-zcrx tests multithreaded Juanlu Herrero
@ 2026-08-31 17:34 ` Juanlu Herrero
2026-08-31 18:09 ` David Wei
2026-08-31 17:34 ` [PATCH net-next v6 2/6] selftests: net: remove unused variable in process_recvzc() Juanlu Herrero
` (4 subsequent siblings)
5 siblings, 1 reply; 15+ messages in thread
From: Juanlu Herrero @ 2026-08-31 17:34 UTC (permalink / raw)
To: dw, netdev
Cc: io-uring, linux-kselftest, linux-kernel, kuba, asml.silence,
pabeni, shuah, juanlu
In preparation for multi-threaded rss selftests, fix
get_refill_ring_size() to use its local `size` variable instead of
assigning to the file-global `ring_size`.
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Juanlu Herrero <juanlu@fastmail.com>
---
tools/testing/selftests/drivers/net/hw/iou-zcrx.c | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/tools/testing/selftests/drivers/net/hw/iou-zcrx.c b/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
index f6a8fc5fac24..df6b62204343 100644
--- a/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
+++ b/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
@@ -132,10 +132,10 @@ static inline size_t get_refill_ring_size(unsigned int rq_entries)
{
size_t size;
- ring_size = rq_entries * sizeof(struct io_uring_zcrx_rqe);
+ size = rq_entries * sizeof(struct io_uring_zcrx_rqe);
/* add space for the header (head/tail/etc.) */
- ring_size += page_size;
- return ALIGN_UP(ring_size, page_size);
+ size += page_size;
+ return ALIGN_UP(size, page_size);
}
static void setup_zcrx(struct io_uring *ring)
--
2.50.1
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH net-next v6 2/6] selftests: net: remove unused variable in process_recvzc()
2026-08-31 17:34 [PATCH net-next v6 0/6] selftests: net: make iou-zcrx tests multithreaded Juanlu Herrero
2026-08-31 17:34 ` [PATCH net-next v6 1/6] selftests: net: fix get_refill_ring_size() to use its local variable Juanlu Herrero
@ 2026-08-31 17:34 ` Juanlu Herrero
2026-08-31 18:09 ` David Wei
2026-08-31 17:34 ` [PATCH net-next v6 3/6] selftests: net: refactor server state into struct thread_ctx Juanlu Herrero
` (3 subsequent siblings)
5 siblings, 1 reply; 15+ messages in thread
From: Juanlu Herrero @ 2026-08-31 17:34 UTC (permalink / raw)
To: dw, netdev
Cc: io-uring, linux-kselftest, linux-kernel, kuba, asml.silence,
pabeni, shuah, juanlu
Remove the unused `sqe` variable in preparation for the multiqueue rss
selftest changes to process_recvzc() in the following commits.
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Juanlu Herrero <juanlu@fastmail.com>
---
tools/testing/selftests/drivers/net/hw/iou-zcrx.c | 1 -
1 file changed, 1 deletion(-)
diff --git a/tools/testing/selftests/drivers/net/hw/iou-zcrx.c b/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
index df6b62204343..c6dbd0ad5368 100644
--- a/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
+++ b/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
@@ -269,7 +269,6 @@ static void process_recvzc(struct io_uring *ring, struct io_uring_cqe *cqe)
unsigned rq_mask = rq_ring.ring_entries - 1;
struct io_uring_zcrx_cqe *rcqe;
struct io_uring_zcrx_rqe *rqe;
- struct io_uring_sqe *sqe;
uint64_t mask;
char *data;
ssize_t n;
--
2.50.1
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH net-next v6 3/6] selftests: net: refactor server state into struct thread_ctx
2026-08-31 17:34 [PATCH net-next v6 0/6] selftests: net: make iou-zcrx tests multithreaded Juanlu Herrero
2026-08-31 17:34 ` [PATCH net-next v6 1/6] selftests: net: fix get_refill_ring_size() to use its local variable Juanlu Herrero
2026-08-31 17:34 ` [PATCH net-next v6 2/6] selftests: net: remove unused variable in process_recvzc() Juanlu Herrero
@ 2026-08-31 17:34 ` Juanlu Herrero
2026-08-31 17:34 ` [PATCH net-next v6 4/6] selftests: net: add multithread client support to iou-zcrx Juanlu Herrero
` (2 subsequent siblings)
5 siblings, 0 replies; 15+ messages in thread
From: Juanlu Herrero @ 2026-08-31 17:34 UTC (permalink / raw)
To: dw, netdev
Cc: io-uring, linux-kselftest, linux-kernel, kuba, asml.silence,
pabeni, shuah, juanlu
Move server-side state (io_uring ring, zcrx area, refill ring, receive
tracking) from global variables into a local struct thread_ctx. This is
a pure refactor with no behavior change: run_server still allocates a
single context on the stack and runs single-threaded, using io_uring
accept and recvzc as before.
This prepares the ground for the multithread server support in the
following commits, which spawns N worker threads each with their own
struct thread_ctx.
Assisted-by: Claude:claude-opus-4-8
Reviewed-by: David Wei <dw@davidwei.uk>
Signed-off-by: Juanlu Herrero <juanlu@fastmail.com>
---
tools/testing/selftests/drivers/net/hw/iou-zcrx.c | 156 +++++++++++-----------
1 file changed, 80 insertions(+), 76 deletions(-)
diff --git a/tools/testing/selftests/drivers/net/hw/iou-zcrx.c b/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
index c6dbd0ad5368..9b62fd0703e6 100644
--- a/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
+++ b/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
@@ -87,14 +87,18 @@ static unsigned int cfg_rx_buf_len;
static bool cfg_dry_run;
static char *payload;
-static void *area_ptr;
-static void *ring_ptr;
-static size_t ring_size;
-static struct io_uring_zcrx_rq rq_ring;
-static unsigned long area_token;
-static int connfd;
-static bool stop;
-static size_t received;
+
+struct thread_ctx {
+ struct io_uring ring;
+ void *area_ptr;
+ void *ring_ptr;
+ size_t ring_size;
+ struct io_uring_zcrx_rq rq_ring;
+ unsigned long area_token;
+ int connfd;
+ bool stop;
+ size_t received;
+};
static unsigned long gettimeofday_ms(void)
{
@@ -138,7 +142,7 @@ static inline size_t get_refill_ring_size(unsigned int rq_entries)
return ALIGN_UP(size, page_size);
}
-static void setup_zcrx(struct io_uring *ring)
+static void setup_zcrx(struct thread_ctx *ctx)
{
unsigned int ifindex;
unsigned int rq_entries = 4096;
@@ -149,44 +153,44 @@ static void setup_zcrx(struct io_uring *ring)
error(1, 0, "bad interface name: %s", cfg_ifname);
if (cfg_rx_buf_len && cfg_rx_buf_len != page_size) {
- area_ptr = mmap(NULL,
- AREA_SIZE,
- PROT_READ | PROT_WRITE,
- MAP_ANONYMOUS | MAP_PRIVATE |
- MAP_HUGETLB | MAP_HUGE_2MB,
- -1,
- 0);
- if (area_ptr == MAP_FAILED) {
+ ctx->area_ptr = mmap(NULL,
+ AREA_SIZE,
+ PROT_READ | PROT_WRITE,
+ MAP_ANONYMOUS | MAP_PRIVATE |
+ MAP_HUGETLB | MAP_HUGE_2MB,
+ -1,
+ 0);
+ if (ctx->area_ptr == MAP_FAILED) {
printf("Can't allocate huge pages\n");
exit(SKIP_CODE);
}
} else {
- area_ptr = mmap(NULL,
- AREA_SIZE,
- PROT_READ | PROT_WRITE,
- MAP_ANONYMOUS | MAP_PRIVATE,
- 0,
- 0);
- if (area_ptr == MAP_FAILED)
+ ctx->area_ptr = mmap(NULL,
+ AREA_SIZE,
+ PROT_READ | PROT_WRITE,
+ MAP_ANONYMOUS | MAP_PRIVATE,
+ 0,
+ 0);
+ if (ctx->area_ptr == MAP_FAILED)
error(1, 0, "mmap(): zero copy area");
}
- ring_size = get_refill_ring_size(rq_entries);
- ring_ptr = mmap(NULL,
- ring_size,
- PROT_READ | PROT_WRITE,
- MAP_ANONYMOUS | MAP_PRIVATE,
- 0,
- 0);
+ ctx->ring_size = get_refill_ring_size(rq_entries);
+ ctx->ring_ptr = mmap(NULL,
+ ctx->ring_size,
+ PROT_READ | PROT_WRITE,
+ MAP_ANONYMOUS | MAP_PRIVATE,
+ 0,
+ 0);
struct io_uring_region_desc region_reg = {
- .size = ring_size,
- .user_addr = (__u64)(unsigned long)ring_ptr,
+ .size = ctx->ring_size,
+ .user_addr = (__u64)(unsigned long)ctx->ring_ptr,
.flags = IORING_MEM_REGION_TYPE_USER,
};
struct io_uring_zcrx_area_reg area_reg = {
- .addr = (__u64)(unsigned long)area_ptr,
+ .addr = (__u64)(unsigned long)ctx->area_ptr,
.len = AREA_SIZE,
.flags = 0,
};
@@ -200,7 +204,7 @@ static void setup_zcrx(struct io_uring *ring)
.rx_buf_len = cfg_rx_buf_len,
};
- ret = io_uring_register_ifq(ring, (void *)®);
+ ret = io_uring_register_ifq(&ctx->ring, (void *)®);
if (cfg_rx_buf_len && (ret == -EINVAL || ret == -EOPNOTSUPP ||
ret == -ERANGE)) {
printf("Large chunks are not supported %i\n", ret);
@@ -209,64 +213,64 @@ static void setup_zcrx(struct io_uring *ring)
error(1, 0, "io_uring_register_ifq(): %d", ret);
}
- rq_ring.khead = (unsigned int *)((char *)ring_ptr + reg.offsets.head);
- rq_ring.ktail = (unsigned int *)((char *)ring_ptr + reg.offsets.tail);
- rq_ring.rqes = (struct io_uring_zcrx_rqe *)((char *)ring_ptr + reg.offsets.rqes);
- rq_ring.rq_tail = 0;
- rq_ring.ring_entries = reg.rq_entries;
+ ctx->rq_ring.khead = (unsigned int *)((char *)ctx->ring_ptr + reg.offsets.head);
+ ctx->rq_ring.ktail = (unsigned int *)((char *)ctx->ring_ptr + reg.offsets.tail);
+ ctx->rq_ring.rqes = (struct io_uring_zcrx_rqe *)((char *)ctx->ring_ptr + reg.offsets.rqes);
+ ctx->rq_ring.rq_tail = 0;
+ ctx->rq_ring.ring_entries = reg.rq_entries;
- area_token = area_reg.rq_area_token;
+ ctx->area_token = area_reg.rq_area_token;
}
-static void add_accept(struct io_uring *ring, int sockfd)
+static void add_accept(struct thread_ctx *ctx, int sockfd)
{
struct io_uring_sqe *sqe;
- sqe = io_uring_get_sqe(ring);
+ sqe = io_uring_get_sqe(&ctx->ring);
io_uring_prep_accept(sqe, sockfd, NULL, NULL, 0);
sqe->user_data = 1;
}
-static void add_recvzc(struct io_uring *ring, int sockfd)
+static void add_recvzc(struct thread_ctx *ctx, int sockfd)
{
struct io_uring_sqe *sqe;
- sqe = io_uring_get_sqe(ring);
+ sqe = io_uring_get_sqe(&ctx->ring);
io_uring_prep_rw(IORING_OP_RECV_ZC, sqe, sockfd, NULL, 0, 0);
sqe->ioprio |= IORING_RECV_MULTISHOT;
sqe->user_data = 2;
}
-static void add_recvzc_oneshot(struct io_uring *ring, int sockfd, size_t len)
+static void add_recvzc_oneshot(struct thread_ctx *ctx, int sockfd, size_t len)
{
struct io_uring_sqe *sqe;
- sqe = io_uring_get_sqe(ring);
+ sqe = io_uring_get_sqe(&ctx->ring);
io_uring_prep_rw(IORING_OP_RECV_ZC, sqe, sockfd, NULL, len, 0);
sqe->ioprio |= IORING_RECV_MULTISHOT;
sqe->user_data = 2;
}
-static void process_accept(struct io_uring *ring, struct io_uring_cqe *cqe)
+static void process_accept(struct thread_ctx *ctx, struct io_uring_cqe *cqe)
{
if (cqe->res < 0)
error(1, 0, "accept()");
- if (connfd)
+ if (ctx->connfd)
error(1, 0, "Unexpected second connection");
- connfd = cqe->res;
+ ctx->connfd = cqe->res;
if (cfg_oneshot)
- add_recvzc_oneshot(ring, connfd, page_size);
+ add_recvzc_oneshot(ctx, ctx->connfd, page_size);
else
- add_recvzc(ring, connfd);
+ add_recvzc(ctx, ctx->connfd);
}
-static void process_recvzc(struct io_uring *ring, struct io_uring_cqe *cqe)
+static void process_recvzc(struct thread_ctx *ctx, struct io_uring_cqe *cqe)
{
- unsigned rq_mask = rq_ring.ring_entries - 1;
+ unsigned int rq_mask = ctx->rq_ring.ring_entries - 1;
struct io_uring_zcrx_cqe *rcqe;
struct io_uring_zcrx_rqe *rqe;
uint64_t mask;
@@ -275,7 +279,7 @@ static void process_recvzc(struct io_uring *ring, struct io_uring_cqe *cqe)
int i;
if (cqe->res == 0 && cqe->flags == 0 && cfg_oneshot_recvs == 0) {
- stop = true;
+ ctx->stop = true;
return;
}
@@ -284,56 +288,56 @@ static void process_recvzc(struct io_uring *ring, struct io_uring_cqe *cqe)
if (cfg_oneshot) {
if (cqe->res == 0 && cqe->flags == 0 && cfg_oneshot_recvs) {
- add_recvzc_oneshot(ring, connfd, page_size);
+ add_recvzc_oneshot(ctx, ctx->connfd, page_size);
cfg_oneshot_recvs--;
}
} else if (!(cqe->flags & IORING_CQE_F_MORE)) {
- add_recvzc(ring, connfd);
+ add_recvzc(ctx, ctx->connfd);
}
rcqe = (struct io_uring_zcrx_cqe *)(cqe + 1);
n = cqe->res;
mask = (1ULL << IORING_ZCRX_AREA_SHIFT) - 1;
- data = (char *)area_ptr + (rcqe->off & mask);
+ data = (char *)ctx->area_ptr + (rcqe->off & mask);
for (i = 0; i < n; i++) {
- if (*(data + i) != payload[(received + i)])
+ if (*(data + i) != payload[(ctx->received + i)])
error(1, 0, "payload mismatch at %d", i);
}
- received += n;
+ ctx->received += n;
- rqe = &rq_ring.rqes[(rq_ring.rq_tail & rq_mask)];
- rqe->off = (rcqe->off & ~IORING_ZCRX_AREA_MASK) | area_token;
+ rqe = &ctx->rq_ring.rqes[(ctx->rq_ring.rq_tail & rq_mask)];
+ rqe->off = (rcqe->off & ~IORING_ZCRX_AREA_MASK) | ctx->area_token;
rqe->len = cqe->res;
- io_uring_smp_store_release(rq_ring.ktail, ++rq_ring.rq_tail);
+ io_uring_smp_store_release(ctx->rq_ring.ktail, ++ctx->rq_ring.rq_tail);
}
-static void server_loop(struct io_uring *ring)
+static void server_loop(struct thread_ctx *ctx)
{
struct io_uring_cqe *cqe;
unsigned int count = 0;
unsigned int head;
int i, ret;
- io_uring_submit_and_wait(ring, 1);
+ io_uring_submit_and_wait(&ctx->ring, 1);
- io_uring_for_each_cqe(ring, head, cqe) {
+ io_uring_for_each_cqe(&ctx->ring, head, cqe) {
if (cqe->user_data == 1)
- process_accept(ring, cqe);
+ process_accept(ctx, cqe);
else if (cqe->user_data == 2)
- process_recvzc(ring, cqe);
+ process_recvzc(ctx, cqe);
else
error(1, 0, "unknown cqe");
count++;
}
- io_uring_cq_advance(ring, count);
+ io_uring_cq_advance(&ctx->ring, count);
}
static void run_server(void)
{
+ struct thread_ctx ctx = {};
unsigned int flags = 0;
- struct io_uring ring;
int fd, enable, ret;
uint64_t tstop;
@@ -356,22 +360,22 @@ static void run_server(void)
flags |= IORING_SETUP_SUBMIT_ALL;
flags |= IORING_SETUP_CQE32;
- io_uring_queue_init(512, &ring, flags);
+ io_uring_queue_init(512, &ctx.ring, flags);
- setup_zcrx(&ring);
+ setup_zcrx(&ctx);
if (cfg_dry_run)
return;
if (listen(fd, 1024) < 0)
error(1, 0, "listen()");
- add_accept(&ring, fd);
+ add_accept(&ctx, fd);
tstop = gettimeofday_ms() + 5000;
- while (!stop && gettimeofday_ms() < tstop)
- server_loop(&ring);
+ while (!ctx.stop && gettimeofday_ms() < tstop)
+ server_loop(&ctx);
- if (!stop)
+ if (!ctx.stop)
error(1, 0, "test failed\n");
}
--
2.50.1
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH net-next v6 4/6] selftests: net: add multithread client support to iou-zcrx
2026-08-31 17:34 [PATCH net-next v6 0/6] selftests: net: make iou-zcrx tests multithreaded Juanlu Herrero
` (2 preceding siblings ...)
2026-08-31 17:34 ` [PATCH net-next v6 3/6] selftests: net: refactor server state into struct thread_ctx Juanlu Herrero
@ 2026-08-31 17:34 ` Juanlu Herrero
2026-09-02 23:38 ` [net-next,v6,4/6] " netdev-bot+sashiko
2026-08-31 17:34 ` [PATCH net-next v6 5/6] selftests: net: add multithread server " Juanlu Herrero
2026-08-31 17:34 ` [PATCH net-next v6 6/6] selftests: net: add rss_multiqueue test variant " Juanlu Herrero
5 siblings, 1 reply; 15+ messages in thread
From: Juanlu Herrero @ 2026-08-31 17:34 UTC (permalink / raw)
To: dw, netdev
Cc: io-uring, linux-kselftest, linux-kernel, kuba, asml.silence,
pabeni, shuah, juanlu
Add pthreads to the iou-zcrx client so that multiple connections can be
established simultaneously. Each client thread connects to the server
and sends its payload independently.
Introduce the -t option to control the number of threads (default 1),
preserving backwards compatibility with existing tests.
Assisted-by: Claude:claude-opus-4-8
Reviewed-by: David Wei <dw@davidwei.uk>
Signed-off-by: Juanlu Herrero <juanlu@fastmail.com>
---
tools/testing/selftests/drivers/net/hw/Makefile | 2 +-
tools/testing/selftests/drivers/net/hw/iou-zcrx.c | 35 +++++++++++++++++++++--
2 files changed, 33 insertions(+), 4 deletions(-)
diff --git a/tools/testing/selftests/drivers/net/hw/Makefile b/tools/testing/selftests/drivers/net/hw/Makefile
index 78bb0169350b..37023ba580de 100644
--- a/tools/testing/selftests/drivers/net/hw/Makefile
+++ b/tools/testing/selftests/drivers/net/hw/Makefile
@@ -91,5 +91,5 @@ include ../../../net/ynl.mk
include ../../../net/bpf.mk
ifeq ($(HAS_IOURING_ZCRX),y)
-$(OUTPUT)/iou-zcrx: LDLIBS += -luring
+$(OUTPUT)/iou-zcrx: LDLIBS += -luring -lpthread
endif
diff --git a/tools/testing/selftests/drivers/net/hw/iou-zcrx.c b/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
index 9b62fd0703e6..f793a6c04e41 100644
--- a/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
+++ b/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
@@ -4,6 +4,7 @@
#include <error.h>
#include <fcntl.h>
#include <limits.h>
+#include <pthread.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
@@ -85,6 +86,7 @@ static int cfg_send_size = SEND_SIZE;
static struct sockaddr_in6 cfg_addr;
static unsigned int cfg_rx_buf_len;
static bool cfg_dry_run;
+static int cfg_num_threads = 1;
static char *payload;
@@ -379,7 +381,7 @@ static void run_server(void)
error(1, 0, "test failed\n");
}
-static void run_client(void)
+static void *client_worker(void *arg)
{
ssize_t to_send = cfg_send_size;
ssize_t sent = 0;
@@ -405,12 +407,36 @@ static void run_client(void)
}
close(fd);
+ return NULL;
+}
+
+static void run_client(void)
+{
+ int total_conns = cfg_num_threads * cfg_num_threads;
+ pthread_t *threads;
+ int i, ret;
+
+ threads = calloc(total_conns, sizeof(*threads));
+ if (!threads)
+ error(1, 0, "calloc()");
+
+ for (i = 0; i < total_conns; i++) {
+ ret = pthread_create(&threads[i], NULL, client_worker, NULL);
+ if (ret)
+ error(1, ret, "pthread_create()");
+ }
+
+ for (i = 0; i < total_conns; i++)
+ pthread_join(threads[i], NULL);
+
+ free(threads);
}
static void usage(const char *filepath)
{
error(1, 0, "Usage: %s (-4|-6) (-s|-c) -h<server_ip> -p<port> "
- "-l<payload_size> -i<ifname> -q<rxq_id>", filepath);
+ "-l<payload_size> -i<ifname> -q<rxq_id> -t<num_threads>",
+ filepath);
}
static void parse_opts(int argc, char **argv)
@@ -428,7 +454,7 @@ static void parse_opts(int argc, char **argv)
usage(argv[0]);
cfg_payload_len = max_payload_len;
- while ((c = getopt(argc, argv, "sch:p:l:i:q:o:z:x:d")) != -1) {
+ while ((c = getopt(argc, argv, "sch:p:l:i:q:o:z:x:dt:")) != -1) {
switch (c) {
case 's':
if (cfg_client)
@@ -469,6 +495,9 @@ static void parse_opts(int argc, char **argv)
case 'd':
cfg_dry_run = true;
break;
+ case 't':
+ cfg_num_threads = strtoul(optarg, NULL, 0);
+ break;
}
}
--
2.50.1
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH net-next v6 5/6] selftests: net: add multithread server support to iou-zcrx
2026-08-31 17:34 [PATCH net-next v6 0/6] selftests: net: make iou-zcrx tests multithreaded Juanlu Herrero
` (3 preceding siblings ...)
2026-08-31 17:34 ` [PATCH net-next v6 4/6] selftests: net: add multithread client support to iou-zcrx Juanlu Herrero
@ 2026-08-31 17:34 ` Juanlu Herrero
2026-08-31 18:08 ` David Wei
2026-09-02 23:38 ` [net-next,v6,5/6] " netdev-bot+sashiko
2026-08-31 17:34 ` [PATCH net-next v6 6/6] selftests: net: add rss_multiqueue test variant " Juanlu Herrero
5 siblings, 2 replies; 15+ messages in thread
From: Juanlu Herrero @ 2026-08-31 17:34 UTC (permalink / raw)
To: dw, netdev
Cc: io-uring, linux-kselftest, linux-kernel, kuba, asml.silence,
pabeni, shuah, juanlu
Run the iou-zcrx server as N worker threads, each owning one receive
queue with its own io_uring and zero-copy receive (zcrx) ifq, so the
test can exercise multi-queue zero-copy receive.
The main thread owns the listening socket, accepts connections, and
dispatches each to the worker owning the queue it landed on by matching
SO_INCOMING_NAPI_ID against per-queue NAPI IDs.
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Juanlu Herrero <juanlu@fastmail.com>
---
tools/testing/selftests/drivers/net/hw/Makefile | 5 +-
tools/testing/selftests/drivers/net/hw/iou-zcrx.c | 263 ++++++++++++++++------
2 files changed, 196 insertions(+), 72 deletions(-)
diff --git a/tools/testing/selftests/drivers/net/hw/Makefile b/tools/testing/selftests/drivers/net/hw/Makefile
index 37023ba580de..f613c88fffc5 100644
--- a/tools/testing/selftests/drivers/net/hw/Makefile
+++ b/tools/testing/selftests/drivers/net/hw/Makefile
@@ -13,10 +13,6 @@ else
$(warning excluding iouring tests, liburing not installed or too old)
endif
-TEST_GEN_FILES := \
- $(COND_GEN_FILES) \
-# end of TEST_GEN_FILES
-
TEST_PROGS = \
csum.py \
devlink_rate_cross_esw.py \
@@ -74,6 +70,7 @@ TEST_INCLUDES := \
YNL_GEN_FILES := \
ncdevmem \
toeplitz \
+ $(COND_GEN_FILES) \
# end of YNL_GEN_FILES
TEST_GEN_FILES += $(YNL_GEN_FILES)
TEST_GEN_FILES += $(patsubst %.c,%.o,$(wildcard *.bpf.c))
diff --git a/tools/testing/selftests/drivers/net/hw/iou-zcrx.c b/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
index f793a6c04e41..29905cb3b243 100644
--- a/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
+++ b/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
@@ -38,6 +38,8 @@
#include <sys/wait.h>
#include <liburing.h>
+#include <ynl.h>
+#include "netdev-user.h"
#define SKIP_CODE 42
@@ -90,6 +92,8 @@ static int cfg_num_threads = 1;
static char *payload;
+#define MAX_CONNS_PER_THREAD 64
+
struct thread_ctx {
struct io_uring ring;
void *area_ptr;
@@ -97,9 +101,15 @@ struct thread_ctx {
size_t ring_size;
struct io_uring_zcrx_rq rq_ring;
unsigned long area_token;
- int connfd;
- bool stop;
- size_t received;
+ int queue_id;
+ int napi_id;
+ pthread_barrier_t *setup_done;
+ pthread_barrier_t *dispatch_done;
+
+ int connfds[MAX_CONNS_PER_THREAD];
+ size_t received[MAX_CONNS_PER_THREAD];
+ int oneshot_recvs[MAX_CONNS_PER_THREAD];
+ int nr_conns;
};
static unsigned long gettimeofday_ms(void)
@@ -199,7 +209,7 @@ static void setup_zcrx(struct thread_ctx *ctx)
struct t_io_uring_zcrx_ifq_reg reg = {
.if_idx = ifindex,
- .if_rxq = cfg_queue_id,
+ .if_rxq = ctx->queue_id,
.rq_entries = rq_entries,
.area_ptr = (__u64)(unsigned long)&area_reg,
.region_ptr = (__u64)(unsigned long)®ion_reg,
@@ -224,53 +234,32 @@ static void setup_zcrx(struct thread_ctx *ctx)
ctx->area_token = area_reg.rq_area_token;
}
-static void add_accept(struct thread_ctx *ctx, int sockfd)
+static void add_recvzc(struct thread_ctx *ctx, int conn_idx)
{
struct io_uring_sqe *sqe;
sqe = io_uring_get_sqe(&ctx->ring);
- io_uring_prep_accept(sqe, sockfd, NULL, NULL, 0);
- sqe->user_data = 1;
-}
-
-static void add_recvzc(struct thread_ctx *ctx, int sockfd)
-{
- struct io_uring_sqe *sqe;
-
- sqe = io_uring_get_sqe(&ctx->ring);
-
- io_uring_prep_rw(IORING_OP_RECV_ZC, sqe, sockfd, NULL, 0, 0);
+ io_uring_prep_rw(IORING_OP_RECV_ZC, sqe, ctx->connfds[conn_idx],
+ NULL, 0, 0);
sqe->ioprio |= IORING_RECV_MULTISHOT;
- sqe->user_data = 2;
+ sqe->user_data = conn_idx;
}
-static void add_recvzc_oneshot(struct thread_ctx *ctx, int sockfd, size_t len)
+static void add_recvzc_oneshot(struct thread_ctx *ctx, int conn_idx, size_t len)
{
struct io_uring_sqe *sqe;
sqe = io_uring_get_sqe(&ctx->ring);
- io_uring_prep_rw(IORING_OP_RECV_ZC, sqe, sockfd, NULL, len, 0);
+ io_uring_prep_rw(IORING_OP_RECV_ZC, sqe, ctx->connfds[conn_idx],
+ NULL, len, 0);
sqe->ioprio |= IORING_RECV_MULTISHOT;
- sqe->user_data = 2;
+ sqe->user_data = conn_idx;
}
-static void process_accept(struct thread_ctx *ctx, struct io_uring_cqe *cqe)
-{
- if (cqe->res < 0)
- error(1, 0, "accept()");
- if (ctx->connfd)
- error(1, 0, "Unexpected second connection");
-
- ctx->connfd = cqe->res;
- if (cfg_oneshot)
- add_recvzc_oneshot(ctx, ctx->connfd, page_size);
- else
- add_recvzc(ctx, ctx->connfd);
-}
-
-static void process_recvzc(struct thread_ctx *ctx, struct io_uring_cqe *cqe)
+static void process_recvzc(struct thread_ctx *ctx, struct io_uring_cqe *cqe,
+ int conn_idx)
{
unsigned int rq_mask = ctx->rq_ring.ring_entries - 1;
struct io_uring_zcrx_cqe *rcqe;
@@ -280,8 +269,9 @@ static void process_recvzc(struct thread_ctx *ctx, struct io_uring_cqe *cqe)
ssize_t n;
int i;
- if (cqe->res == 0 && cqe->flags == 0 && cfg_oneshot_recvs == 0) {
- ctx->stop = true;
+ if (cqe->res == 0 && cqe->flags == 0 &&
+ ctx->oneshot_recvs[conn_idx] == 0) {
+ ctx->nr_conns--;
return;
}
@@ -289,12 +279,14 @@ static void process_recvzc(struct thread_ctx *ctx, struct io_uring_cqe *cqe)
error(1, 0, "recvzc(): %d", cqe->res);
if (cfg_oneshot) {
- if (cqe->res == 0 && cqe->flags == 0 && cfg_oneshot_recvs) {
- add_recvzc_oneshot(ctx, ctx->connfd, page_size);
- cfg_oneshot_recvs--;
+ if (cqe->res == 0 && cqe->flags == 0 &&
+ ctx->oneshot_recvs[conn_idx]) {
+ add_recvzc_oneshot(ctx, conn_idx, page_size);
+ ctx->oneshot_recvs[conn_idx]--;
+ return;
}
} else if (!(cqe->flags & IORING_CQE_F_MORE)) {
- add_recvzc(ctx, ctx->connfd);
+ add_recvzc(ctx, conn_idx);
}
rcqe = (struct io_uring_zcrx_cqe *)(cqe + 1);
@@ -304,10 +296,10 @@ static void process_recvzc(struct thread_ctx *ctx, struct io_uring_cqe *cqe)
data = (char *)ctx->area_ptr + (rcqe->off & mask);
for (i = 0; i < n; i++) {
- if (*(data + i) != payload[(ctx->received + i)])
+ if (*(data + i) != payload[(ctx->received[conn_idx] + i)])
error(1, 0, "payload mismatch at %d", i);
}
- ctx->received += n;
+ ctx->received[conn_idx] += n;
rqe = &ctx->rq_ring.rqes[(ctx->rq_ring.rq_tail & rq_mask)];
rqe->off = (rcqe->off & ~IORING_ZCRX_AREA_MASK) | ctx->area_token;
@@ -320,28 +312,124 @@ static void server_loop(struct thread_ctx *ctx)
struct io_uring_cqe *cqe;
unsigned int count = 0;
unsigned int head;
- int i, ret;
io_uring_submit_and_wait(&ctx->ring, 1);
io_uring_for_each_cqe(&ctx->ring, head, cqe) {
- if (cqe->user_data == 1)
- process_accept(ctx, cqe);
- else if (cqe->user_data == 2)
- process_recvzc(ctx, cqe);
- else
- error(1, 0, "unknown cqe");
+ process_recvzc(ctx, cqe, cqe->user_data);
count++;
}
io_uring_cq_advance(&ctx->ring, count);
}
-static void run_server(void)
+static void *server_worker(void *arg)
{
- struct thread_ctx ctx = {};
- unsigned int flags = 0;
- int fd, enable, ret;
+ struct io_uring_params params = { };
+ struct thread_ctx *ctx = arg;
uint64_t tstop;
+ int i;
+
+ params.flags |= IORING_SETUP_COOP_TASKRUN;
+ params.flags |= IORING_SETUP_SINGLE_ISSUER;
+ params.flags |= IORING_SETUP_DEFER_TASKRUN;
+ params.flags |= IORING_SETUP_SUBMIT_ALL;
+ params.flags |= IORING_SETUP_CQE32;
+ params.flags |= IORING_SETUP_CQSIZE;
+ params.cq_entries = AREA_SIZE / page_size;
+
+ io_uring_queue_init_params(512, &ctx->ring, ¶ms);
+ setup_zcrx(ctx);
+
+ if (cfg_dry_run)
+ return NULL;
+
+ pthread_barrier_wait(ctx->setup_done);
+ pthread_barrier_wait(ctx->dispatch_done);
+
+ for (i = 0; i < ctx->nr_conns; i++) {
+ if (cfg_oneshot) {
+ ctx->oneshot_recvs[i] = cfg_oneshot_recvs;
+ add_recvzc_oneshot(ctx, i, page_size);
+ } else {
+ add_recvzc(ctx, i);
+ }
+ }
+
+ tstop = gettimeofday_ms() + 5000;
+ while (ctx->nr_conns > 0 && gettimeofday_ms() < tstop)
+ server_loop(ctx);
+
+ if (ctx->nr_conns != 0)
+ error(1, 0, "test failed: %d connections incomplete",
+ ctx->nr_conns);
+
+ return NULL;
+}
+
+static int query_napi_id(unsigned int ifindex, int queue_id)
+{
+ struct netdev_queue_get_req *req;
+ struct netdev_queue_get_rsp *rsp;
+ struct ynl_error yerr;
+ struct ynl_sock *ys;
+ int napi_id;
+
+ ys = ynl_sock_create(&ynl_netdev_family, &yerr);
+ if (!ys)
+ error(1, 0, "ynl_sock_create: %s", yerr.msg);
+
+ req = netdev_queue_get_req_alloc();
+ netdev_queue_get_req_set_ifindex(req, ifindex);
+ netdev_queue_get_req_set_type(req, NETDEV_QUEUE_TYPE_RX);
+ netdev_queue_get_req_set_id(req, queue_id);
+
+ rsp = netdev_queue_get(ys, req);
+ if (!rsp)
+ error(1, 0, "netdev_queue_get(q=%d): %s", queue_id,
+ ys->err.msg);
+ if (!rsp->_present.napi_id)
+ error(1, 0, "netdev_queue_get(q=%d): napi_id not present",
+ queue_id);
+
+ napi_id = rsp->napi_id;
+
+ netdev_queue_get_req_free(req);
+ netdev_queue_get_rsp_free(rsp);
+ ynl_sock_destroy(ys);
+
+ return napi_id;
+}
+
+static int find_thread_by_conn(struct thread_ctx *ctxs, int connfd)
+{
+ socklen_t len = sizeof(int);
+ int napi_id, i;
+
+ if (getsockopt(connfd, SOL_SOCKET, SO_INCOMING_NAPI_ID, &napi_id, &len))
+ error(1, errno, "getsockopt(SO_INCOMING_NAPI_ID)");
+
+ for (i = 0; i < cfg_num_threads; i++) {
+ if (ctxs[i].napi_id == napi_id)
+ return i;
+ }
+
+ error(1, 0, "unknown NAPI ID: %d", napi_id);
+ return -1;
+}
+
+static void run_server(void)
+{
+ pthread_barrier_t setup_done, dispatch_done;
+ int total_conns, accepted = 0, connfd;
+ struct thread_ctx *ctxs;
+ int fd, ret, enable, i;
+ unsigned int ifindex;
+ pthread_t *threads;
+
+ ctxs = calloc(cfg_num_threads, sizeof(*ctxs));
+ threads = calloc(cfg_num_threads, sizeof(*threads));
+ if (!ctxs || !threads)
+ error(1, 0, "calloc()");
fd = socket(AF_INET6, SOCK_STREAM, 0);
if (fd == -1)
@@ -356,29 +444,68 @@ static void run_server(void)
if (ret < 0)
error(1, 0, "bind()");
- flags |= IORING_SETUP_COOP_TASKRUN;
- flags |= IORING_SETUP_SINGLE_ISSUER;
- flags |= IORING_SETUP_DEFER_TASKRUN;
- flags |= IORING_SETUP_SUBMIT_ALL;
- flags |= IORING_SETUP_CQE32;
+ pthread_barrier_init(&setup_done, NULL, cfg_num_threads + 1);
+ pthread_barrier_init(&dispatch_done, NULL, cfg_num_threads + 1);
+
+ for (i = 0; i < cfg_num_threads; i++) {
+ ctxs[i].queue_id = cfg_queue_id + i;
+ ctxs[i].setup_done = &setup_done;
+ ctxs[i].dispatch_done = &dispatch_done;
+ }
- io_uring_queue_init(512, &ctx.ring, flags);
+ for (i = 0; i < cfg_num_threads; i++) {
+ ret = pthread_create(&threads[i], NULL,
+ server_worker, &ctxs[i]);
+ if (ret)
+ error(1, ret, "pthread_create()");
+ }
- setup_zcrx(&ctx);
if (cfg_dry_run)
- return;
+ goto join;
if (listen(fd, 1024) < 0)
error(1, 0, "listen()");
- add_accept(&ctx, fd);
+ pthread_barrier_wait(&setup_done);
- tstop = gettimeofday_ms() + 5000;
- while (!ctx.stop && gettimeofday_ms() < tstop)
- server_loop(&ctx);
+ if (cfg_num_threads > 1) {
+ ifindex = if_nametoindex(cfg_ifname);
+ if (!ifindex)
+ error(1, 0, "bad interface name: %s", cfg_ifname);
+ for (i = 0; i < cfg_num_threads; i++)
+ ctxs[i].napi_id = query_napi_id(ifindex,
+ ctxs[i].queue_id);
+ }
+
+ total_conns = cfg_num_threads * cfg_num_threads;
+
+ while (accepted < total_conns) {
+ int idx = 0;
+
+ connfd = accept(fd, NULL, NULL);
+ if (connfd < 0)
+ error(1, errno, "accept()");
- if (!ctx.stop)
- error(1, 0, "test failed\n");
+ if (cfg_num_threads > 1)
+ idx = find_thread_by_conn(ctxs, connfd);
+
+ if (ctxs[idx].nr_conns >= MAX_CONNS_PER_THREAD)
+ error(1, 0, "worker %d connection overflow", idx);
+ ctxs[idx].connfds[ctxs[idx].nr_conns++] = connfd;
+ accepted++;
+ }
+
+ pthread_barrier_wait(&dispatch_done);
+
+join:
+ for (i = 0; i < cfg_num_threads; i++)
+ pthread_join(threads[i], NULL);
+
+ pthread_barrier_destroy(&setup_done);
+ pthread_barrier_destroy(&dispatch_done);
+ close(fd);
+ free(threads);
+ free(ctxs);
}
static void *client_worker(void *arg)
--
2.50.1
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH net-next v6 6/6] selftests: net: add rss_multiqueue test variant to iou-zcrx
2026-08-31 17:34 [PATCH net-next v6 0/6] selftests: net: make iou-zcrx tests multithreaded Juanlu Herrero
` (4 preceding siblings ...)
2026-08-31 17:34 ` [PATCH net-next v6 5/6] selftests: net: add multithread server " Juanlu Herrero
@ 2026-08-31 17:34 ` Juanlu Herrero
2026-09-02 23:38 ` [net-next,v6,6/6] " netdev-bot+sashiko
5 siblings, 1 reply; 15+ messages in thread
From: Juanlu Herrero @ 2026-08-31 17:34 UTC (permalink / raw)
To: dw, netdev
Cc: io-uring, linux-kselftest, linux-kernel, kuba, asml.silence,
pabeni, shuah, juanlu
Add a rss_multiqueue Python test variant that exercises multi-queue
zero-copy receive on a single listening socket.
Assisted-by: Claude:claude-opus-4-8
Reviewed-by: David Wei <dw@davidwei.uk>
Signed-off-by: Juanlu Herrero <juanlu@fastmail.com>
---
tools/testing/selftests/drivers/net/hw/iou-zcrx.py | 50 +++++++++++++++++++++-
1 file changed, 48 insertions(+), 2 deletions(-)
diff --git a/tools/testing/selftests/drivers/net/hw/iou-zcrx.py b/tools/testing/selftests/drivers/net/hw/iou-zcrx.py
index b7a225fe4bea..c833535d8a03 100755
--- a/tools/testing/selftests/drivers/net/hw/iou-zcrx.py
+++ b/tools/testing/selftests/drivers/net/hw/iou-zcrx.py
@@ -30,6 +30,13 @@ def create_rss_ctx(cfg):
return int(values)
+def create_rss_ctx_multi(cfg, start, count):
+ """Create an RSS context spanning count queues from start, return its ID."""
+ output = ethtool(f"-X {cfg.ifname} context new start {start} equal {count}").stdout
+ values = re.search(r'New RSS context is (\d+)', output).group(1)
+ return int(values)
+
+
def set_flow_rule(cfg):
output = ethtool(f"-N {cfg.ifname} flow-type tcp6 dst-port {cfg.port} action {cfg.target}").stdout
values = re.search(r'ID (\d+)', output).group(1)
@@ -127,17 +134,56 @@ def _require_ntuple(cfg):
defer(ethtool, f"-K {cfg.ifname} ntuple-filters off")
+def rss_multiqueue(cfg):
+ """Steer the test flow to a multi-queue RSS context for multi-thread zcrx."""
+ channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
+ channels = channels['combined-count']
+ if channels < 3:
+ raise KsftSkipEx('Test requires NETIF with at least 3 combined channels')
+
+ rings = cfg.ethnl.rings_get({'header': {'dev-index': cfg.ifindex}})
+ rx_rings = rings['rx']
+ hds_thresh = rings.get('hds-thresh', 0)
+
+ cfg.ethnl.rings_set({'header': {'dev-index': cfg.ifindex},
+ 'tcp-data-split': 'enabled',
+ 'hds-thresh': 0,
+ 'rx': 64})
+ defer(cfg.ethnl.rings_set, {'header': {'dev-index': cfg.ifindex},
+ 'tcp-data-split': 'unknown',
+ 'hds-thresh': hds_thresh,
+ 'rx': rx_rings})
+ defer(mp_clear_wait, cfg)
+
+ cfg.num_threads = 2
+ cfg.target = channels - cfg.num_threads
+ ethtool(f"-X {cfg.ifname} equal {cfg.target}")
+ defer(ethtool, f"-X {cfg.ifname} default")
+
+ rss_ctx_id = create_rss_ctx_multi(cfg, cfg.target, cfg.num_threads)
+ defer(ethtool, f"-X {cfg.ifname} delete context {rss_ctx_id}")
+
+ flow_rule_id = set_flow_rule_rss(cfg, rss_ctx_id)
+ defer(ethtool, f"-N {cfg.ifname} delete {flow_rule_id}")
+
+
@ksft_variants([
KsftNamedVariant("single", single),
KsftNamedVariant("rss", rss),
+ KsftNamedVariant("rss_multiqueue", rss_multiqueue),
])
def test_zcrx(cfg, setup) -> None:
cfg.require_ipver('6')
_require_ntuple(cfg)
+ cfg.num_threads = 1
+
setup(cfg)
- rx_cmd = f"{cfg.bin_local} -s -p {cfg.port} -i {cfg.ifname} -q {cfg.target}"
- tx_cmd = f"{cfg.bin_remote} -c -h {cfg.addr_v['6']} -p {cfg.port} -l 12840"
+
+ rx_cmd = (f"{cfg.bin_local} -s -p {cfg.port} -i {cfg.ifname} "
+ f"-q {cfg.target} -t {cfg.num_threads}")
+ tx_cmd = (f"{cfg.bin_remote} -c -h {cfg.addr_v['6']} -p {cfg.port} "
+ f"-l 12840 -t {cfg.num_threads}")
with bkg(rx_cmd, exit_wait=True):
wait_port_listen(cfg.port, proto="tcp")
cmd(tx_cmd, host=cfg.remote)
--
2.50.1
^ permalink raw reply related [flat|nested] 15+ messages in thread
* Re: [PATCH net-next v6 5/6] selftests: net: add multithread server support to iou-zcrx
2026-08-31 17:34 ` [PATCH net-next v6 5/6] selftests: net: add multithread server " Juanlu Herrero
@ 2026-08-31 18:08 ` David Wei
2026-09-02 23:38 ` [net-next,v6,5/6] " netdev-bot+sashiko
1 sibling, 0 replies; 15+ messages in thread
From: David Wei @ 2026-08-31 18:08 UTC (permalink / raw)
To: Juanlu Herrero, netdev
Cc: io-uring, linux-kselftest, linux-kernel, kuba, asml.silence,
pabeni, shuah
On 2026-08-31 10:34, Juanlu Herrero wrote:
> Run the iou-zcrx server as N worker threads, each owning one receive
> queue with its own io_uring and zero-copy receive (zcrx) ifq, so the
> test can exercise multi-queue zero-copy receive.
>
> The main thread owns the listening socket, accepts connections, and
> dispatches each to the worker owning the queue it landed on by matching
> SO_INCOMING_NAPI_ID against per-queue NAPI IDs.
>
> Assisted-by: Claude:claude-opus-4-8
> Signed-off-by: Juanlu Herrero <juanlu@fastmail.com>
> ---
> tools/testing/selftests/drivers/net/hw/Makefile | 5 +-
> tools/testing/selftests/drivers/net/hw/iou-zcrx.c | 263 ++++++++++++++++------
> 2 files changed, 196 insertions(+), 72 deletions(-)
>
> diff --git a/tools/testing/selftests/drivers/net/hw/Makefile b/tools/testing/selftests/drivers/net/hw/Makefile
> index 37023ba580de..f613c88fffc5 100644
> --- a/tools/testing/selftests/drivers/net/hw/Makefile
> +++ b/tools/testing/selftests/drivers/net/hw/Makefile
> @@ -13,10 +13,6 @@ else
> $(warning excluding iouring tests, liburing not installed or too old)
> endif
>
> -TEST_GEN_FILES := \
> - $(COND_GEN_FILES) \
> -# end of TEST_GEN_FILES
> -
> TEST_PROGS = \
> csum.py \
> devlink_rate_cross_esw.py \
> @@ -74,6 +70,7 @@ TEST_INCLUDES := \
> YNL_GEN_FILES := \
> ncdevmem \
> toeplitz \
> + $(COND_GEN_FILES) \
> # end of YNL_GEN_FILES
> TEST_GEN_FILES += $(YNL_GEN_FILES)
> TEST_GEN_FILES += $(patsubst %.c,%.o,$(wildcard *.bpf.c))
> diff --git a/tools/testing/selftests/drivers/net/hw/iou-zcrx.c b/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
> index f793a6c04e41..29905cb3b243 100644
> --- a/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
> +++ b/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
> @@ -38,6 +38,8 @@
> #include <sys/wait.h>
>
> #include <liburing.h>
> +#include <ynl.h>
> +#include "netdev-user.h"
>
> #define SKIP_CODE 42
>
> @@ -90,6 +92,8 @@ static int cfg_num_threads = 1;
>
> static char *payload;
>
> +#define MAX_CONNS_PER_THREAD 64
> +
> struct thread_ctx {
> struct io_uring ring;
> void *area_ptr;
> @@ -97,9 +101,15 @@ struct thread_ctx {
> size_t ring_size;
> struct io_uring_zcrx_rq rq_ring;
> unsigned long area_token;
> - int connfd;
> - bool stop;
> - size_t received;
> + int queue_id;
> + int napi_id;
> + pthread_barrier_t *setup_done;
> + pthread_barrier_t *dispatch_done;
> +
> + int connfds[MAX_CONNS_PER_THREAD];
> + size_t received[MAX_CONNS_PER_THREAD];
> + int oneshot_recvs[MAX_CONNS_PER_THREAD];
> + int nr_conns;
> };
>
> static unsigned long gettimeofday_ms(void)
> @@ -199,7 +209,7 @@ static void setup_zcrx(struct thread_ctx *ctx)
>
> struct t_io_uring_zcrx_ifq_reg reg = {
> .if_idx = ifindex,
> - .if_rxq = cfg_queue_id,
> + .if_rxq = ctx->queue_id,
> .rq_entries = rq_entries,
> .area_ptr = (__u64)(unsigned long)&area_reg,
> .region_ptr = (__u64)(unsigned long)®ion_reg,
> @@ -224,53 +234,32 @@ static void setup_zcrx(struct thread_ctx *ctx)
> ctx->area_token = area_reg.rq_area_token;
> }
>
> -static void add_accept(struct thread_ctx *ctx, int sockfd)
> +static void add_recvzc(struct thread_ctx *ctx, int conn_idx)
> {
> struct io_uring_sqe *sqe;
>
> sqe = io_uring_get_sqe(&ctx->ring);
>
> - io_uring_prep_accept(sqe, sockfd, NULL, NULL, 0);
> - sqe->user_data = 1;
> -}
> -
> -static void add_recvzc(struct thread_ctx *ctx, int sockfd)
> -{
> - struct io_uring_sqe *sqe;
> -
> - sqe = io_uring_get_sqe(&ctx->ring);
> -
> - io_uring_prep_rw(IORING_OP_RECV_ZC, sqe, sockfd, NULL, 0, 0);
> + io_uring_prep_rw(IORING_OP_RECV_ZC, sqe, ctx->connfds[conn_idx],
> + NULL, 0, 0);
> sqe->ioprio |= IORING_RECV_MULTISHOT;
> - sqe->user_data = 2;
> + sqe->user_data = conn_idx;
> }
>
> -static void add_recvzc_oneshot(struct thread_ctx *ctx, int sockfd, size_t len)
> +static void add_recvzc_oneshot(struct thread_ctx *ctx, int conn_idx, size_t len)
> {
> struct io_uring_sqe *sqe;
>
> sqe = io_uring_get_sqe(&ctx->ring);
>
> - io_uring_prep_rw(IORING_OP_RECV_ZC, sqe, sockfd, NULL, len, 0);
> + io_uring_prep_rw(IORING_OP_RECV_ZC, sqe, ctx->connfds[conn_idx],
> + NULL, len, 0);
> sqe->ioprio |= IORING_RECV_MULTISHOT;
> - sqe->user_data = 2;
> + sqe->user_data = conn_idx;
> }
>
> -static void process_accept(struct thread_ctx *ctx, struct io_uring_cqe *cqe)
> -{
> - if (cqe->res < 0)
> - error(1, 0, "accept()");
> - if (ctx->connfd)
> - error(1, 0, "Unexpected second connection");
> -
> - ctx->connfd = cqe->res;
> - if (cfg_oneshot)
> - add_recvzc_oneshot(ctx, ctx->connfd, page_size);
> - else
> - add_recvzc(ctx, ctx->connfd);
> -}
> -
> -static void process_recvzc(struct thread_ctx *ctx, struct io_uring_cqe *cqe)
> +static void process_recvzc(struct thread_ctx *ctx, struct io_uring_cqe *cqe,
> + int conn_idx)
> {
> unsigned int rq_mask = ctx->rq_ring.ring_entries - 1;
> struct io_uring_zcrx_cqe *rcqe;
> @@ -280,8 +269,9 @@ static void process_recvzc(struct thread_ctx *ctx, struct io_uring_cqe *cqe)
> ssize_t n;
> int i;
>
> - if (cqe->res == 0 && cqe->flags == 0 && cfg_oneshot_recvs == 0) {
> - ctx->stop = true;
> + if (cqe->res == 0 && cqe->flags == 0 &&
> + ctx->oneshot_recvs[conn_idx] == 0) {
> + ctx->nr_conns--;
> return;
> }
>
> @@ -289,12 +279,14 @@ static void process_recvzc(struct thread_ctx *ctx, struct io_uring_cqe *cqe)
> error(1, 0, "recvzc(): %d", cqe->res);
>
> if (cfg_oneshot) {
> - if (cqe->res == 0 && cqe->flags == 0 && cfg_oneshot_recvs) {
> - add_recvzc_oneshot(ctx, ctx->connfd, page_size);
> - cfg_oneshot_recvs--;
> + if (cqe->res == 0 && cqe->flags == 0 &&
> + ctx->oneshot_recvs[conn_idx]) {
> + add_recvzc_oneshot(ctx, conn_idx, page_size);
> + ctx->oneshot_recvs[conn_idx]--;
> + return;
> }
> } else if (!(cqe->flags & IORING_CQE_F_MORE)) {
> - add_recvzc(ctx, ctx->connfd);
> + add_recvzc(ctx, conn_idx);
> }
>
> rcqe = (struct io_uring_zcrx_cqe *)(cqe + 1);
> @@ -304,10 +296,10 @@ static void process_recvzc(struct thread_ctx *ctx, struct io_uring_cqe *cqe)
> data = (char *)ctx->area_ptr + (rcqe->off & mask);
>
> for (i = 0; i < n; i++) {
> - if (*(data + i) != payload[(ctx->received + i)])
> + if (*(data + i) != payload[(ctx->received[conn_idx] + i)])
> error(1, 0, "payload mismatch at %d", i);
> }
> - ctx->received += n;
> + ctx->received[conn_idx] += n;
>
> rqe = &ctx->rq_ring.rqes[(ctx->rq_ring.rq_tail & rq_mask)];
> rqe->off = (rcqe->off & ~IORING_ZCRX_AREA_MASK) | ctx->area_token;
> @@ -320,28 +312,124 @@ static void server_loop(struct thread_ctx *ctx)
> struct io_uring_cqe *cqe;
> unsigned int count = 0;
> unsigned int head;
> - int i, ret;
>
> io_uring_submit_and_wait(&ctx->ring, 1);
>
> io_uring_for_each_cqe(&ctx->ring, head, cqe) {
> - if (cqe->user_data == 1)
> - process_accept(ctx, cqe);
> - else if (cqe->user_data == 2)
> - process_recvzc(ctx, cqe);
> - else
> - error(1, 0, "unknown cqe");
> + process_recvzc(ctx, cqe, cqe->user_data);
> count++;
> }
> io_uring_cq_advance(&ctx->ring, count);
> }
>
> -static void run_server(void)
> +static void *server_worker(void *arg)
> {
> - struct thread_ctx ctx = {};
> - unsigned int flags = 0;
> - int fd, enable, ret;
> + struct io_uring_params params = { };
> + struct thread_ctx *ctx = arg;
> uint64_t tstop;
> + int i;
> +
> + params.flags |= IORING_SETUP_COOP_TASKRUN;
> + params.flags |= IORING_SETUP_SINGLE_ISSUER;
> + params.flags |= IORING_SETUP_DEFER_TASKRUN;
> + params.flags |= IORING_SETUP_SUBMIT_ALL;
> + params.flags |= IORING_SETUP_CQE32;
> + params.flags |= IORING_SETUP_CQSIZE;
> + params.cq_entries = AREA_SIZE / page_size;
> +
> + io_uring_queue_init_params(512, &ctx->ring, ¶ms);
> + setup_zcrx(ctx);
> +
> + if (cfg_dry_run)
> + return NULL;
> +
> + pthread_barrier_wait(ctx->setup_done);
> + pthread_barrier_wait(ctx->dispatch_done);
> +
> + for (i = 0; i < ctx->nr_conns; i++) {
> + if (cfg_oneshot) {
> + ctx->oneshot_recvs[i] = cfg_oneshot_recvs;
> + add_recvzc_oneshot(ctx, i, page_size);
> + } else {
> + add_recvzc(ctx, i);
> + }
> + }
> +
> + tstop = gettimeofday_ms() + 5000;
> + while (ctx->nr_conns > 0 && gettimeofday_ms() < tstop)
> + server_loop(ctx);
> +
> + if (ctx->nr_conns != 0)
> + error(1, 0, "test failed: %d connections incomplete",
> + ctx->nr_conns);
> +
> + return NULL;
> +}
> +
> +static int query_napi_id(unsigned int ifindex, int queue_id)
> +{
> + struct netdev_queue_get_req *req;
> + struct netdev_queue_get_rsp *rsp;
> + struct ynl_error yerr;
> + struct ynl_sock *ys;
> + int napi_id;
> +
> + ys = ynl_sock_create(&ynl_netdev_family, &yerr);
> + if (!ys)
> + error(1, 0, "ynl_sock_create: %s", yerr.msg);
> +
> + req = netdev_queue_get_req_alloc();
> + netdev_queue_get_req_set_ifindex(req, ifindex);
> + netdev_queue_get_req_set_type(req, NETDEV_QUEUE_TYPE_RX);
> + netdev_queue_get_req_set_id(req, queue_id);
> +
> + rsp = netdev_queue_get(ys, req);
> + if (!rsp)
> + error(1, 0, "netdev_queue_get(q=%d): %s", queue_id,
> + ys->err.msg);
> + if (!rsp->_present.napi_id)
> + error(1, 0, "netdev_queue_get(q=%d): napi_id not present",
> + queue_id);
> +
> + napi_id = rsp->napi_id;
> +
> + netdev_queue_get_req_free(req);
> + netdev_queue_get_rsp_free(rsp);
> + ynl_sock_destroy(ys);
> +
> + return napi_id;
> +}
> +
> +static int find_thread_by_conn(struct thread_ctx *ctxs, int connfd)
> +{
> + socklen_t len = sizeof(int);
> + int napi_id, i;
> +
> + if (getsockopt(connfd, SOL_SOCKET, SO_INCOMING_NAPI_ID, &napi_id, &len))
> + error(1, errno, "getsockopt(SO_INCOMING_NAPI_ID)");
> +
> + for (i = 0; i < cfg_num_threads; i++) {
> + if (ctxs[i].napi_id == napi_id)
> + return i;
> + }
> +
> + error(1, 0, "unknown NAPI ID: %d", napi_id);
> + return -1;
> +}
> +
> +static void run_server(void)
> +{
> + pthread_barrier_t setup_done, dispatch_done;
> + int total_conns, accepted = 0, connfd;
> + struct thread_ctx *ctxs;
> + int fd, ret, enable, i;
> + unsigned int ifindex;
> + pthread_t *threads;
> +
> + ctxs = calloc(cfg_num_threads, sizeof(*ctxs));
> + threads = calloc(cfg_num_threads, sizeof(*threads));
> + if (!ctxs || !threads)
> + error(1, 0, "calloc()");
>
> fd = socket(AF_INET6, SOCK_STREAM, 0);
> if (fd == -1)
> @@ -356,29 +444,68 @@ static void run_server(void)
> if (ret < 0)
> error(1, 0, "bind()");
>
> - flags |= IORING_SETUP_COOP_TASKRUN;
> - flags |= IORING_SETUP_SINGLE_ISSUER;
> - flags |= IORING_SETUP_DEFER_TASKRUN;
> - flags |= IORING_SETUP_SUBMIT_ALL;
> - flags |= IORING_SETUP_CQE32;
> + pthread_barrier_init(&setup_done, NULL, cfg_num_threads + 1);
> + pthread_barrier_init(&dispatch_done, NULL, cfg_num_threads + 1);
> +
> + for (i = 0; i < cfg_num_threads; i++) {
> + ctxs[i].queue_id = cfg_queue_id + i;
> + ctxs[i].setup_done = &setup_done;
> + ctxs[i].dispatch_done = &dispatch_done;
> + }
>
> - io_uring_queue_init(512, &ctx.ring, flags);
> + for (i = 0; i < cfg_num_threads; i++) {
> + ret = pthread_create(&threads[i], NULL,
> + server_worker, &ctxs[i]);
> + if (ret)
> + error(1, ret, "pthread_create()");
> + }
>
> - setup_zcrx(&ctx);
> if (cfg_dry_run)
> - return;
> + goto join;
>
> if (listen(fd, 1024) < 0)
> error(1, 0, "listen()");
>
> - add_accept(&ctx, fd);
> + pthread_barrier_wait(&setup_done);
>
> - tstop = gettimeofday_ms() + 5000;
> - while (!ctx.stop && gettimeofday_ms() < tstop)
> - server_loop(&ctx);
> + if (cfg_num_threads > 1) {
> + ifindex = if_nametoindex(cfg_ifname);
> + if (!ifindex)
> + error(1, 0, "bad interface name: %s", cfg_ifname);
> + for (i = 0; i < cfg_num_threads; i++)
> + ctxs[i].napi_id = query_napi_id(ifindex,
> + ctxs[i].queue_id);
> + }
> +
> + total_conns = cfg_num_threads * cfg_num_threads;
> +
> + while (accepted < total_conns) {
> + int idx = 0;
> +
> + connfd = accept(fd, NULL, NULL);
> + if (connfd < 0)
> + error(1, errno, "accept()");
>
> - if (!ctx.stop)
> - error(1, 0, "test failed\n");
> + if (cfg_num_threads > 1)
> + idx = find_thread_by_conn(ctxs, connfd);
> +
> + if (ctxs[idx].nr_conns >= MAX_CONNS_PER_THREAD)
> + error(1, 0, "worker %d connection overflow", idx);
> + ctxs[idx].connfds[ctxs[idx].nr_conns++] = connfd;
> + accepted++;
> + }
> +
> + pthread_barrier_wait(&dispatch_done);
> +
> +join:
> + for (i = 0; i < cfg_num_threads; i++)
> + pthread_join(threads[i], NULL);
> +
> + pthread_barrier_destroy(&setup_done);
> + pthread_barrier_destroy(&dispatch_done);
> + close(fd);
> + free(threads);
> + free(ctxs);
> }
>
> static void *client_worker(void *arg)
>
Reviewed-by: David Wei <dw@davidwei.uk>
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH net-next v6 1/6] selftests: net: fix get_refill_ring_size() to use its local variable
2026-08-31 17:34 ` [PATCH net-next v6 1/6] selftests: net: fix get_refill_ring_size() to use its local variable Juanlu Herrero
@ 2026-08-31 18:09 ` David Wei
0 siblings, 0 replies; 15+ messages in thread
From: David Wei @ 2026-08-31 18:09 UTC (permalink / raw)
To: Juanlu Herrero, netdev
Cc: io-uring, linux-kselftest, linux-kernel, kuba, asml.silence,
pabeni, shuah
On 2026-08-31 10:34, Juanlu Herrero wrote:
> In preparation for multi-threaded rss selftests, fix
> get_refill_ring_size() to use its local `size` variable instead of
> assigning to the file-global `ring_size`.
>
> Assisted-by: Claude:claude-opus-4-8
> Signed-off-by: Juanlu Herrero <juanlu@fastmail.com>
> ---
> tools/testing/selftests/drivers/net/hw/iou-zcrx.c | 6 +++---
> 1 file changed, 3 insertions(+), 3 deletions(-)
>
> diff --git a/tools/testing/selftests/drivers/net/hw/iou-zcrx.c b/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
> index f6a8fc5fac24..df6b62204343 100644
> --- a/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
> +++ b/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
> @@ -132,10 +132,10 @@ static inline size_t get_refill_ring_size(unsigned int rq_entries)
> {
> size_t size;
>
> - ring_size = rq_entries * sizeof(struct io_uring_zcrx_rqe);
> + size = rq_entries * sizeof(struct io_uring_zcrx_rqe);
> /* add space for the header (head/tail/etc.) */
> - ring_size += page_size;
> - return ALIGN_UP(ring_size, page_size);
> + size += page_size;
> + return ALIGN_UP(size, page_size);
> }
>
> static void setup_zcrx(struct io_uring *ring)
>
Reviewed-by: David Wei <dw@davidwei.uk>
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH net-next v6 2/6] selftests: net: remove unused variable in process_recvzc()
2026-08-31 17:34 ` [PATCH net-next v6 2/6] selftests: net: remove unused variable in process_recvzc() Juanlu Herrero
@ 2026-08-31 18:09 ` David Wei
0 siblings, 0 replies; 15+ messages in thread
From: David Wei @ 2026-08-31 18:09 UTC (permalink / raw)
To: Juanlu Herrero, netdev
Cc: io-uring, linux-kselftest, linux-kernel, kuba, asml.silence,
pabeni, shuah
On 2026-08-31 10:34, Juanlu Herrero wrote:
> Remove the unused `sqe` variable in preparation for the multiqueue rss
> selftest changes to process_recvzc() in the following commits.
>
> Assisted-by: Claude:claude-opus-4-8
> Signed-off-by: Juanlu Herrero <juanlu@fastmail.com>
> ---
> tools/testing/selftests/drivers/net/hw/iou-zcrx.c | 1 -
> 1 file changed, 1 deletion(-)
>
> diff --git a/tools/testing/selftests/drivers/net/hw/iou-zcrx.c b/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
> index df6b62204343..c6dbd0ad5368 100644
> --- a/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
> +++ b/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
> @@ -269,7 +269,6 @@ static void process_recvzc(struct io_uring *ring, struct io_uring_cqe *cqe)
> unsigned rq_mask = rq_ring.ring_entries - 1;
> struct io_uring_zcrx_cqe *rcqe;
> struct io_uring_zcrx_rqe *rqe;
> - struct io_uring_sqe *sqe;
> uint64_t mask;
> char *data;
> ssize_t n;
>
Reviewed-by: David Wei <dw@davidwei.uk>
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [net-next,v6,4/6] selftests: net: add multithread client support to iou-zcrx
2026-08-31 17:34 ` [PATCH net-next v6 4/6] selftests: net: add multithread client support to iou-zcrx Juanlu Herrero
@ 2026-09-02 23:38 ` netdev-bot+sashiko
0 siblings, 0 replies; 15+ messages in thread
From: netdev-bot+sashiko @ 2026-09-02 23:38 UTC (permalink / raw)
To: juanlu
Cc: dw, netdev, io-uring, linux-kselftest, linux-kernel, kuba,
asml.silence, pabeni, shuah
Thank you for your contribution! Sashiko AI review found 4 potential
issue(s) to consider:
Critical: 0 · High: 0 · Medium: 2 (1) · Low: 2
- [Medium] run_client() creates cfg_num_threads * cfg_num_threads worker
threads/TCP connections instead of cfg_num_threads, contradicting both…
- [Medium] At this commit the multi-connection client has no counterpart
on the receive side of the same binary: run_server() (iou-zcrx.c:339)…
- [Low] The new -t option is parsed with strtoul() into the signed int
cfg_num_threads (iou-zcrx.c:499) with no syntax, range, or non-zero…
- [Low] At this commit no case in iou-zcrx.py (the only in-tree consumer
of the iou-zcrx binary) passes -t, so the newly added multithreaded…
Pre-existing issues:
- [Medium] client_worker() (iou-zcrx.c:384) walks `payload[sent]` for
cfg_send_size bytes, but payload is allocated with a fixed size of…
--
selftests: net: add multithread client support to iou-zcrx
This adds pthreads to the iou-zcrx client so several connections can be
made at once, and a new -t option to pick how many threads are used
(default 1). The Makefile now links iou-zcrx against -lpthread.
A few questions about the new client code below.
> diff --git a/tools/testing/selftests/drivers/net/hw/iou-zcrx.c b/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
> index 9b62fd0703e61..f793a6c04e412 100644
> --- a/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
> +++ b/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
[ ... ]
> @@ -85,6 +86,7 @@ static int cfg_send_size = SEND_SIZE;
> static struct sockaddr_in6 cfg_addr;
> static unsigned int cfg_rx_buf_len;
> static bool cfg_dry_run;
> +static int cfg_num_threads = 1;
>
> static char *payload;
>
> @@ -379,7 +381,7 @@ static void run_server(void)
> error(1, 0, "test failed\n");
> }
>
> -static void run_client(void)
> +static void *client_worker(void *arg)
> {
> ssize_t to_send = cfg_send_size;
> ssize_t sent = 0;
[Severity: Medium]
This is a pre-existing issue and is not introduced by this patch, since the
code was only renamed from run_client() to client_worker() here, but can
client_worker() read past the end of payload[]?
payload is allocated with a fixed size in main():
if (posix_memalign((void **)&payload, page_size, SEND_SIZE))
while the loop in client_worker() walks cfg_send_size bytes:
ssize_t to_send = cfg_send_size;
...
while (to_send) {
void *src = &payload[sent];
chunk = min_t(ssize_t, cfg_payload_len, to_send);
res = send(fd, src, chunk, 0);
and cfg_send_size comes straight from -z with no comparison against
SEND_SIZE:
case 'z':
cfg_send_size = strtoul(optarg, NULL, 0);
With -z larger than SEND_SIZE (512 * 4096), does &payload[sent] end up past
the allocation, and are those out-of-bounds bytes then handed to send()?
process_recvzc() looks to have the mirror-image indexing on
payload[ctx->received + i]. No in-tree caller currently passes such a
value (iou-zcrx.py uses -z 16384), and this patch only increases the number
of threads that would perform the same read.
> @@ -405,12 +407,36 @@ static void run_client(void)
> }
>
> close(fd);
> + return NULL;
> +}
> +
> +static void run_client(void)
> +{
> + int total_conns = cfg_num_threads * cfg_num_threads;
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[Severity: Medium]
Is squaring the thread count here intentional? The changelog says:
Introduce the -t option to control the number of threads (default 1)
and the usage string added by this patch says -t<num_threads>, but
total_conns is cfg_num_threads * cfg_num_threads and is then used as the
calloc() element count and as the bound of both the pthread_create() and
pthread_join() loops in run_client().
So -t 2 gives 4 threads and 4 connections, -t 4 gives 16, and -t 8 gives
64; only the default of 1 matches the documented meaning. Each worker
opens its own socket, so thread stacks and file descriptors also grow
quadratically with a value the help text presents as a plain thread count.
There is no comment explaining the squaring, and the later server patch in
this series treats -t as a literal thread/queue count:
ctxs = calloc(cfg_num_threads, sizeof(*ctxs));
...
pthread_barrier_init(&setup_done, NULL, cfg_num_threads + 1);
At the end of the series (4a6d87a68298) run_client() still has
total_conns = cfg_num_threads * cfg_num_threads and the usage string still
reads -t<num_threads>, so the two halves of the same option keep disagreeing.
Could the squaring be dropped, or the option documented as the per-side
value it actually is?
> + pthread_t *threads;
> + int i, ret;
> +
> + threads = calloc(total_conns, sizeof(*threads));
> + if (!threads)
> + error(1, 0, "calloc()");
> +
> + for (i = 0; i < total_conns; i++) {
> + ret = pthread_create(&threads[i], NULL, client_worker, NULL);
> + if (ret)
> + error(1, ret, "pthread_create()");
> + }
[Severity: Medium]
At this commit, can the server side of the same binary service more than
one of these connections?
run_server() builds a single struct thread_ctx and posts exactly one accept:
add_accept(&ctx, fd);
and process_accept() aborts on the second completion:
if (ctx->connfd)
error(1, 0, "Unexpected second connection");
Verification also uses a single ctx->received cursor into the global
payload, which only tracks one stream. With -t greater than 1, do the
extra connections sit in the backlog while the server exits on the first
stream's EOF, leaving the remaining client_worker() threads to fail in
send() or block until the server timeout?
The receive side gains multi-connection support in the next patch of this
series ("selftests: net: add multithread server support to iou-zcrx"),
where run_server() spawns cfg_num_threads workers, accepts until
accepted < total_conns, dispatches each connfd into ctxs[idx].connfds[],
and drops the "Unexpected second connection" abort. Would it be worth
ordering the server patch first so the tree is never in a state where
-t > 1 cannot complete?
> +
> + for (i = 0; i < total_conns; i++)
> + pthread_join(threads[i], NULL);
> +
> + free(threads);
> }
[Severity: Low]
This isn't a bug, but at this commit no case in iou-zcrx.py passes -t, so
the new pthread_create()/pthread_join() loops in run_client() only ever run
with the default cfg_num_threads == 1 and the multithreaded path is not
exercised by the shipped suite. Coverage arrives later in the series: at
4a6d87a68298 test_zcrx builds both command lines with -t {cfg.num_threads},
and the rss_multiqueue variant sets cfg.num_threads = 2.
>
> static void usage(const char *filepath)
> {
> error(1, 0, "Usage: %s (-4|-6) (-s|-c) -h<server_ip> -p<port> "
> - "-l<payload_size> -i<ifname> -q<rxq_id>", filepath);
> + "-l<payload_size> -i<ifname> -q<rxq_id> -t<num_threads>",
> + filepath);
> }
[ ... ]
> @@ -469,6 +495,9 @@ static void parse_opts(int argc, char **argv)
> case 'd':
> cfg_dry_run = true;
> break;
> + case 't':
> + cfg_num_threads = strtoul(optarg, NULL, 0);
> + break;
[Severity: Low]
This isn't a bug, but -t is stored with strtoul() into the signed int
cfg_num_threads with no non-zero or range check, and run_client() then
evaluates cfg_num_threads * cfg_num_threads in int arithmetic before using
it as a calloc() count and loop bound.
-t 0 and -t 65536 (65536 * 65536 wraps to 0 in int) both create no threads
and let the client exit successfully without sending anything, -t 4294967295
narrows to -1, values at or above 46341 overflow the signed multiplication,
and -t 1000 asks for a million threads.
Note the rest of the file parses its numeric options the same unvalidated
way (-p, -l, -q, -o, -z, -x), so a small sanity check on -t alone would
still be an improvement if the squaring stays.
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260831-iou-zcrx-v6-0-02d1d012f0fd%40fastmail.com
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [net-next,v6,5/6] selftests: net: add multithread server support to iou-zcrx
2026-08-31 17:34 ` [PATCH net-next v6 5/6] selftests: net: add multithread server " Juanlu Herrero
2026-08-31 18:08 ` David Wei
@ 2026-09-02 23:38 ` netdev-bot+sashiko
2026-09-03 11:06 ` Paolo Abeni
1 sibling, 1 reply; 15+ messages in thread
From: netdev-bot+sashiko @ 2026-09-02 23:38 UTC (permalink / raw)
To: juanlu
Cc: dw, netdev, io-uring, linux-kselftest, linux-kernel, kuba,
asml.silence, pabeni, shuah
Thank you for your contribution! Sashiko AI review found 6 potential
issue(s) to consider:
Critical: 0 · High: 1 · Medium: 2 (3) · Low: 3
- [High] listen() is now executed before the workers have registered
their zcrx ifqs, breaking the ordering that two existing in-tree tests…
- [Medium] A worker thread that is dispatched zero connections reports
success without receiving or verifying a single byte.
- [Medium] The new CQ sizing removes the implicit bound that kept the
zcrx refill-queue producer inside one ring.
- [Low] DISCARDED (resolved later in the series): the concern states that
the new multithread/multi-queue server path is dead code because iou-…
- [Low] `-t` is documented as a thread count but produces its square.
- [Low] `-t 0` is accepted and yields a silent PASS.
Pre-existing issues:
- [Medium] DISCARDED (pre-existing, medium): the concern states that the
5 s watchdog in server_worker() can never fire while the thread is…
- [Medium] DISCARDED (pre-existing, medium): the concern states that per-
worker resources are never released — no io_uring_queue_exit(), no…
- [Medium] DISCARDED (pre-existing, medium): the concern states that
process_recvzc() indexes the 2 MiB `payload` buffer with…
--
selftests: net: add multithread server support to iou-zcrx
This converts the iou-zcrx server into N worker threads, each owning one
RX queue with its own io_uring and zcrx ifq. The main thread keeps the
listening socket and dispatches accepted connections to workers by
matching SO_INCOMING_NAPI_ID against per-queue NAPI IDs queried over ynl.
A few questions about the new threading and setup ordering below.
> diff --git a/tools/testing/selftests/drivers/net/hw/iou-zcrx.c b/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
> index f793a6c04e412..29905cb3b2433 100644
> --- a/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
> +++ b/tools/testing/selftests/drivers/net/hw/iou-zcrx.c
[ ... ]
> @@ -97,9 +101,15 @@ struct thread_ctx {
> size_t ring_size;
> struct io_uring_zcrx_rq rq_ring;
> unsigned long area_token;
> - int connfd;
> - bool stop;
> - size_t received;
> + int queue_id;
> + int napi_id;
> + pthread_barrier_t *setup_done;
> + pthread_barrier_t *dispatch_done;
> +
> + int connfds[MAX_CONNS_PER_THREAD];
> + size_t received[MAX_CONNS_PER_THREAD];
> + int oneshot_recvs[MAX_CONNS_PER_THREAD];
> + int nr_conns;
> };
[ ... ]
> @@ -304,10 +296,10 @@ static void process_recvzc(struct thread_ctx *ctx, struct io_uring_cqe *cqe,
> data = (char *)ctx->area_ptr + (rcqe->off & mask);
>
> for (i = 0; i < n; i++) {
> - if (*(data + i) != payload[(ctx->received + i)])
> + if (*(data + i) != payload[(ctx->received[conn_idx] + i)])
> error(1, 0, "payload mismatch at %d", i);
> }
> - ctx->received += n;
> + ctx->received[conn_idx] += n;
[Severity: Medium]
This is a pre-existing issue and not introduced by this patch, but the
index into payload[] is still unbounded. payload is a single
posix_memalign(page_size, SEND_SIZE) allocation in main(), while
ctx->received[conn_idx] only ever accumulates cqe->res, which is driven
by whatever the peer sends into the wildcard-bound listener.
Would it be worth adding a check that
ctx->received[conn_idx] + n <= cfg_send_size before the comparison loop,
so a peer sending more than cfg_send_size bytes produces a clear error
rather than an out-of-bounds read of payload[]?
>
> rqe = &ctx->rq_ring.rqes[(ctx->rq_ring.rq_tail & rq_mask)];
> rqe->off = (rcqe->off & ~IORING_ZCRX_AREA_MASK) | ctx->area_token;
> @@ -320,28 +312,124 @@ static void server_loop(struct thread_ctx *ctx)
> struct io_uring_cqe *cqe;
> unsigned int count = 0;
> unsigned int head;
> - int i, ret;
>
> io_uring_submit_and_wait(&ctx->ring, 1);
>
> io_uring_for_each_cqe(&ctx->ring, head, cqe) {
> - if (cqe->user_data == 1)
> - process_accept(ctx, cqe);
> - else if (cqe->user_data == 2)
> - process_recvzc(ctx, cqe);
> - else
> - error(1, 0, "unknown cqe");
> + process_recvzc(ctx, cqe, cqe->user_data);
> count++;
> }
> io_uring_cq_advance(&ctx->ring, count);
> }
>
> -static void run_server(void)
> +static void *server_worker(void *arg)
> {
> - struct thread_ctx ctx = {};
> - unsigned int flags = 0;
> - int fd, enable, ret;
> + struct io_uring_params params = { };
> + struct thread_ctx *ctx = arg;
> uint64_t tstop;
> + int i;
> +
> + params.flags |= IORING_SETUP_COOP_TASKRUN;
> + params.flags |= IORING_SETUP_SINGLE_ISSUER;
> + params.flags |= IORING_SETUP_DEFER_TASKRUN;
> + params.flags |= IORING_SETUP_SUBMIT_ALL;
> + params.flags |= IORING_SETUP_CQE32;
> + params.flags |= IORING_SETUP_CQSIZE;
> + params.cq_entries = AREA_SIZE / page_size;
[Severity: Medium]
Can this overrun the zcrx refill ring? The CQ is now sized to
AREA_SIZE / page_size (8192 entries), while setup_zcrx() still uses
rq_entries = 4096.
server_loop() drains every available CQE in one batch, and
process_recvzc() publishes exactly one refill descriptor per CQE without
ever consulting khead:
rqe = &ctx->rq_ring.rqes[(ctx->rq_ring.rq_tail & rq_mask)];
...
io_uring_smp_store_release(ctx->rq_ring.ktail, ++ctx->rq_ring.rq_tail);
Previously io_uring_queue_init(512, ...) gave cq_entries = 1024, which is
strictly smaller than rq_entries, so a single drain could not wrap the
refill ring. With 8192 CQ entries a drain of more than 4096 zcrx CQEs
overwrites descriptors that the kernel has not consumed yet, and the
kernel side clamps rather than complains:
io_uring/zcrx.c:__zcrx_rq_entries() {
u32 entries = rq->cached_tail - rq->cached_head;
return min(entries, rq->nr_entries);
}
so the surplus buffers are simply never returned to the page pool. Should
the producer only publish while (ktail - khead) < rq_entries, or should
rq_entries be at least cq_entries?
> +
> + io_uring_queue_init_params(512, &ctx->ring, ¶ms);
> + setup_zcrx(ctx);
> +
> + if (cfg_dry_run)
> + return NULL;
> +
> + pthread_barrier_wait(ctx->setup_done);
> + pthread_barrier_wait(ctx->dispatch_done);
> +
> + for (i = 0; i < ctx->nr_conns; i++) {
> + if (cfg_oneshot) {
> + ctx->oneshot_recvs[i] = cfg_oneshot_recvs;
> + add_recvzc_oneshot(ctx, i, page_size);
> + } else {
> + add_recvzc(ctx, i);
> + }
> + }
> +
> + tstop = gettimeofday_ms() + 5000;
> + while (ctx->nr_conns > 0 && gettimeofday_ms() < tstop)
> + server_loop(ctx);
> +
> + if (ctx->nr_conns != 0)
> + error(1, 0, "test failed: %d connections incomplete",
> + ctx->nr_conns);
[Severity: Medium]
Can a worker that received no connections at all report success here?
nr_conns is used both as the dispatch count and as the live-connection
countdown, so with nr_conns == 0 the arming loop above does nothing, the
drain loop is skipped, and this final check passes without a single byte
having been received or verified.
Dispatch in run_server() is purely SO_INCOMING_NAPI_ID based, and nothing
guarantees a non-empty distribution. With -t 2 (total_conns = 4) all four
flows can hash onto one RSS queue, in which case the second worker exits
successfully having done nothing, and the multi-queue test reports pass
while only one queue was exercised. The previous code failed loudly in
this situation with "test failed".
Would a post-dispatch assertion in run_server() that every worker owns at
least one connection, plus a check on the received byte totals, keep the
old strictness?
The reverse skew looks unhandled too: total_conns is cfg_num_threads
squared, and if they all land on one worker then for -t 9 and above this
trips the "worker %d connection overflow" error in run_server(), which
would be a spurious failure rather than a real one.
[Severity: Medium]
This isn't a bug introduced by this patch, but the 5 s deadline can still
never fire while the thread is parked inside server_loop():
io_uring_submit_and_wait(&ctx->ring, 1);
The deadline is only re-evaluated between blocking waits, so a stalled
connection hangs the worker (and then pthread_join() in run_server())
instead of producing the "connections incomplete" failure. The same shape
existed before the patch, but it now applies per worker thread. Could the
wait use a timeout so the watchdog is effective?
> +
> + return NULL;
> +}
[ ... ]
> +static void run_server(void)
> +{
> + pthread_barrier_t setup_done, dispatch_done;
> + int total_conns, accepted = 0, connfd;
> + struct thread_ctx *ctxs;
> + int fd, ret, enable, i;
> + unsigned int ifindex;
> + pthread_t *threads;
> +
> + ctxs = calloc(cfg_num_threads, sizeof(*ctxs));
> + threads = calloc(cfg_num_threads, sizeof(*threads));
> + if (!ctxs || !threads)
> + error(1, 0, "calloc()");
[Severity: Low]
Should parse_opts() reject -t 0? cfg_num_threads is taken straight from
strtoul(optarg, NULL, 0) with no positivity check, and with 0:
- calloc(0, ...) returns a non-NULL pointer, so this guard passes
- both barriers are initialised with a single participant
- no workers are created
- total_conns is 0, so the accept loop never runs
- both barrier waits return immediately and run_server() returns 0
The client side also computes total_conns as cfg_num_threads squared, so
it makes no connection and exits successfully as well. Either way the
binary reports success without doing any work.
>
> fd = socket(AF_INET6, SOCK_STREAM, 0);
> if (fd == -1)
> @@ -356,29 +444,68 @@ static void run_server(void)
> if (ret < 0)
> error(1, 0, "bind()");
>
> - flags |= IORING_SETUP_COOP_TASKRUN;
> - flags |= IORING_SETUP_SINGLE_ISSUER;
> - flags |= IORING_SETUP_DEFER_TASKRUN;
> - flags |= IORING_SETUP_SUBMIT_ALL;
> - flags |= IORING_SETUP_CQE32;
> + pthread_barrier_init(&setup_done, NULL, cfg_num_threads + 1);
> + pthread_barrier_init(&dispatch_done, NULL, cfg_num_threads + 1);
> +
> + for (i = 0; i < cfg_num_threads; i++) {
> + ctxs[i].queue_id = cfg_queue_id + i;
> + ctxs[i].setup_done = &setup_done;
> + ctxs[i].dispatch_done = &dispatch_done;
> + }
>
> - io_uring_queue_init(512, &ctx.ring, flags);
> + for (i = 0; i < cfg_num_threads; i++) {
> + ret = pthread_create(&threads[i], NULL,
> + server_worker, &ctxs[i]);
> + if (ret)
> + error(1, ret, "pthread_create()");
> + }
>
> - setup_zcrx(&ctx);
> if (cfg_dry_run)
> - return;
> + goto join;
>
> if (listen(fd, 1024) < 0)
> error(1, 0, "listen()");
>
> - add_accept(&ctx, fd);
> + pthread_barrier_wait(&setup_done);
[Severity: High]
Is the listen() intentionally moved before the workers have registered
their ifqs? The main thread creates the threads, calls listen(), and only
then waits on setup_done, while the workers reach that barrier after
io_uring_queue_init_params() plus setup_zcrx(), i.e. after
io_uring_register_ifq() has restarted the device RX queue.
Before the patch the sequence was serialized in one thread:
io_uring_queue_init(512, &ctx.ring, flags);
setup_zcrx(&ctx);
...
listen(fd, 1024);
so the port only became visible once the memory provider was installed.
Two existing tests depend on that ordering. wait_port_listen() in
net/lib/py/utils.py polls every 5 ms, so it can now return while
registration is still in flight:
iou-zcrx.py:test_zcrx_large_chunks() {
wait_port_listen(cfg.port, proto="tcp")
check_iou_rx_buf_len(cfg, rx_buf_len)
}
check_iou_rx_buf_len() raises "Expected 1 io-uring page pool, found 0",
and nk_qlease.py:test_destroy() does queue_get() plus
ksft_in("io-uring", queue_info) immediately after wait_port_listen() with
no sleep.
Beyond the harness races, data delivered into the queue before the
provider exists is serviced by the copy fallback rather than zero copy,
and the queue restart during registration can drop in-flight frames.
Would moving listen() below pthread_barrier_wait(&setup_done) restore the
previous guarantee?
>
> - tstop = gettimeofday_ms() + 5000;
> - while (!ctx.stop && gettimeofday_ms() < tstop)
> - server_loop(&ctx);
> + if (cfg_num_threads > 1) {
> + ifindex = if_nametoindex(cfg_ifname);
> + if (!ifindex)
> + error(1, 0, "bad interface name: %s", cfg_ifname);
> + for (i = 0; i < cfg_num_threads; i++)
> + ctxs[i].napi_id = query_napi_id(ifindex,
> + ctxs[i].queue_id);
> + }
> +
> + total_conns = cfg_num_threads * cfg_num_threads;
[Severity: Low]
This isn't a bug, but -t is documented as a thread count while both
run_server() and run_client() square it. usage() advertises
-t<num_threads>, and run_client() creates total_conns threads with
total_conns = cfg_num_threads * cfg_num_threads, so -t 8 produces 64
client threads and 64 connections, not 8. That also interacts with the
64-entry MAX_CONNS_PER_THREAD limit. Could the option name or the usage
text spell out the quadratic connection count?
> +
> + while (accepted < total_conns) {
> + int idx = 0;
> +
> + connfd = accept(fd, NULL, NULL);
> + if (connfd < 0)
> + error(1, errno, "accept()");
>
> - if (!ctx.stop)
> - error(1, 0, "test failed\n");
> + if (cfg_num_threads > 1)
> + idx = find_thread_by_conn(ctxs, connfd);
> +
> + if (ctxs[idx].nr_conns >= MAX_CONNS_PER_THREAD)
> + error(1, 0, "worker %d connection overflow", idx);
> + ctxs[idx].connfds[ctxs[idx].nr_conns++] = connfd;
> + accepted++;
> + }
> +
> + pthread_barrier_wait(&dispatch_done);
> +
> +join:
> + for (i = 0; i < cfg_num_threads; i++)
> + pthread_join(threads[i], NULL);
> +
> + pthread_barrier_destroy(&setup_done);
> + pthread_barrier_destroy(&dispatch_done);
> + close(fd);
> + free(threads);
> + free(ctxs);
[Severity: Medium]
This is a pre-existing gap rather than something the patch introduces,
but the teardown is now asymmetric. The barriers, the listening fd and
the two calloc'ed arrays are released here, while the per-worker
resources are not: there is no io_uring_queue_exit(), no munmap() of the
AREA_SIZE zero-copy area or of the refill ring mapping created in
setup_zcrx(), and the accepted connfds stored in ctxs[idx].connfds[] are
never closed. With cfg_num_threads workers this is now multiplied by the
thread count, including on the cfg_dry_run early return in
server_worker(). Would it be worth adding the matching cleanup?
> }
>
> static void *client_worker(void *arg)
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260831-iou-zcrx-v6-0-02d1d012f0fd%40fastmail.com
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [net-next,v6,6/6] selftests: net: add rss_multiqueue test variant to iou-zcrx
2026-08-31 17:34 ` [PATCH net-next v6 6/6] selftests: net: add rss_multiqueue test variant " Juanlu Herrero
@ 2026-09-02 23:38 ` netdev-bot+sashiko
0 siblings, 0 replies; 15+ messages in thread
From: netdev-bot+sashiko @ 2026-09-02 23:38 UTC (permalink / raw)
To: juanlu
Cc: dw, netdev, io-uring, linux-kselftest, linux-kernel, kuba,
asml.silence, pabeni, shuah
Thank you for your contribution! Sashiko AI review found 2 potential
issue(s) to consider:
Critical: 0 · High: 0 · Medium: 2 · Low: 0
- [Medium] `rss_multiqueue()` in
tools/testing/selftests/drivers/net/hw/iou-zcrx.py claims to "exercise…
- [Medium] The readiness handshake used by the Python driver no longer
implies that the receiver is armed.
--
selftests: net: add rss_multiqueue test variant to iou-zcrx
Adds a new rss_multiqueue variant to the iou-zcrx Python test which builds
a two-queue RSS context, steers the tcp6 flow into it, and runs the
iou-zcrx helper with two receive threads. The existing test_zcrx variants
now also pass an explicit thread count of one.
> diff --git a/tools/testing/selftests/drivers/net/hw/iou-zcrx.py b/tools/testing/selftests/drivers/net/hw/iou-zcrx.py
> index b7a225fe4beae..c833535d8a03c 100755
> --- a/tools/testing/selftests/drivers/net/hw/iou-zcrx.py
> +++ b/tools/testing/selftests/drivers/net/hw/iou-zcrx.py
> @@ -30,6 +30,13 @@ def create_rss_ctx(cfg):
> return int(values)
>
>
> +def create_rss_ctx_multi(cfg, start, count):
> + """Create an RSS context spanning count queues from start, return its ID."""
> + output = ethtool(f"-X {cfg.ifname} context new start {start} equal {count}").stdout
> + values = re.search(r'New RSS context is (\d+)', output).group(1)
> + return int(values)
> +
> +
[ ... ]
> @@ -127,17 +134,56 @@ def _require_ntuple(cfg):
> defer(ethtool, f"-K {cfg.ifname} ntuple-filters off")
>
>
> +def rss_multiqueue(cfg):
> + """Steer the test flow to a multi-queue RSS context for multi-thread zcrx."""
[ ... ]
> + cfg.num_threads = 2
> + cfg.target = channels - cfg.num_threads
> + ethtool(f"-X {cfg.ifname} equal {cfg.target}")
> + defer(ethtool, f"-X {cfg.ifname} default")
> +
> + rss_ctx_id = create_rss_ctx_multi(cfg, cfg.target, cfg.num_threads)
> + defer(ethtool, f"-X {cfg.ifname} delete context {rss_ctx_id}")
> +
> + flow_rule_id = set_flow_rule_rss(cfg, rss_ctx_id)
> + defer(ethtool, f"-N {cfg.ifname} delete {flow_rule_id}")
[Severity: Medium]
Can this variant report a pass when only one of the two queues carried
zcrx traffic?
The RSS context spans queues [cfg.target, cfg.target + 1] and the flow rule
matches only flow-type tcp6 dst-port, so all connections share the same
addresses and destination port. The variant never configures or checks
ethtool -n rx-flow-hash tcp6, so on a device whose tcp6 hash covers only
addresses, every connection lands on the same queue. Even with a full
4-tuple hash, the four flows over a two-entry indirection table all collide
on a single queue roughly one run in eight.
On the helper side, run_server() distributes accepted connections purely by
the hardware-selected napi id:
tools/testing/selftests/drivers/net/hw/iou-zcrx.c:run_server() {
total_conns = cfg_num_threads * cfg_num_threads;
while (accepted < total_conns) {
int idx = 0;
connfd = accept(fd, NULL, NULL);
...
if (cfg_num_threads > 1)
idx = find_thread_by_conn(ctxs, connfd);
...
}
and a worker that ends up with no connections exits successfully:
tools/testing/selftests/drivers/net/hw/iou-zcrx.c:server_worker() {
...
while (ctx->nr_conns > 0 && gettimeofday_ms() < tstop)
server_loop(ctx);
if (ctx->nr_conns != 0)
error(1, 0, "test failed: %d connections incomplete",
ctx->nr_conns);
...
}
With nr_conns == 0 the loop and the check are both skipped, the process
exits 0, and rss_multiqueue passes while behaving exactly like the existing
single-queue rss variant.
Would it make sense to require L4 tcp6 hashing, fail when a worker in
multi-thread mode gets zero connections or zero bytes, and/or verify
io-uring page pool activity for both cfg.target and cfg.target + 1 from
Python? check_iou_rx_buf_len() and cfg.netnl.page_pool_get() already give
the per-queue view needed for the last part.
> @ksft_variants([
> KsftNamedVariant("single", single),
> KsftNamedVariant("rss", rss),
> + KsftNamedVariant("rss_multiqueue", rss_multiqueue),
> ])
> def test_zcrx(cfg, setup) -> None:
> cfg.require_ipver('6')
> _require_ntuple(cfg)
>
> + cfg.num_threads = 1
> +
> setup(cfg)
> - rx_cmd = f"{cfg.bin_local} -s -p {cfg.port} -i {cfg.ifname} -q {cfg.target}"
> - tx_cmd = f"{cfg.bin_remote} -c -h {cfg.addr_v['6']} -p {cfg.port} -l 12840"
> +
> + rx_cmd = (f"{cfg.bin_local} -s -p {cfg.port} -i {cfg.ifname} "
> + f"-q {cfg.target} -t {cfg.num_threads}")
> + tx_cmd = (f"{cfg.bin_remote} -c -h {cfg.addr_v['6']} -p {cfg.port} "
> + f"-l 12840 -t {cfg.num_threads}")
> with bkg(rx_cmd, exit_wait=True):
> wait_port_listen(cfg.port, proto="tcp")
> cmd(tx_cmd, host=cfg.remote)
[Severity: Medium]
Does wait_port_listen() still mean the receiver is armed once every
test_zcrx variant goes through the threaded server path?
wait_port_listen() in tools/testing/selftests/net/lib/py/utils.py only
polls /proc/net/tcp for TCP state 0A (LISTEN). In the threaded server the
listen() now happens before the setup barrier:
tools/testing/selftests/drivers/net/hw/iou-zcrx.c:run_server() {
...
/* pthread_create() loop for the workers */
...
if (listen(fd, 1024) < 0)
error(1, 0, "listen()");
pthread_barrier_wait(&setup_done);
...
}
while the worker only reaches that barrier after the 32 MB mmap and
io_uring_register_ifq() done by setup_zcrx():
tools/testing/selftests/drivers/net/hw/iou-zcrx.c:server_worker() {
io_uring_queue_init_params(512, &ctx->ring, ¶ms);
setup_zcrx(ctx);
if (cfg_dry_run)
return NULL;
pthread_barrier_wait(ctx->setup_done);
...
}
So the port is observable as LISTEN while the zcrx ifq is not yet
registered, and cmd(tx_cmd, host=cfg.remote) can start streaming into that
window.
Two consequences seem possible. Bytes arriving before
io_uring_register_ifq() land in ordinary driver pages and
io_zcrx_recv_frag() falls back to io_zcrx_copy_frag(), so the payload check
still succeeds while the zero-copy path is not exercised.
Also, test_zcrx_large_chunks() calls check_iou_rx_buf_len() right after
wait_port_listen(), which raises when the pool is not there yet:
tools/testing/selftests/drivers/net/hw/iou-zcrx.py:check_iou_rx_buf_len() {
...
if len(pools) != 1:
raise Exception(f"Expected 1 io-uring page pool, found {len(pools)}")
...
}
Would moving listen(fd, 1024) to after pthread_barrier_wait(&setup_done)
restore the previous ordering, where the port only became observable once
zcrx registration had completed?
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260831-iou-zcrx-v6-0-02d1d012f0fd%40fastmail.com
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [net-next,v6,5/6] selftests: net: add multithread server support to iou-zcrx
2026-09-02 23:38 ` [net-next,v6,5/6] " netdev-bot+sashiko
@ 2026-09-03 11:06 ` Paolo Abeni
2026-09-03 20:47 ` Juanlu Herrero
0 siblings, 1 reply; 15+ messages in thread
From: Paolo Abeni @ 2026-09-03 11:06 UTC (permalink / raw)
To: netdev-bot+sashiko, juanlu
Cc: dw, netdev, io-uring, linux-kselftest, linux-kernel, kuba,
asml.silence, shuah
On 9/3/26 1:38 AM, netdev-bot+sashiko@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 6 potential
> issue(s) to consider:
>
> Critical: 0 · High: 1 · Medium: 2 (3) · Low: 3
>
> - [High] listen() is now executed before the workers have registered
> their zcrx ifqs, breaking the ordering that two existing in-tree tests…
> - [Medium] A worker thread that is dispatched zero connections reports
> success without receiving or verifying a single byte.
> - [Medium] The new CQ sizing removes the implicit bound that kept the
> zcrx refill-queue producer inside one ring.
My understanding is that the above 3 concerns may affect tests stability
and should be addressed before merging.
Thanks,
Paolo
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [net-next,v6,5/6] selftests: net: add multithread server support to iou-zcrx
2026-09-03 11:06 ` Paolo Abeni
@ 2026-09-03 20:47 ` Juanlu Herrero
0 siblings, 0 replies; 15+ messages in thread
From: Juanlu Herrero @ 2026-09-03 20:47 UTC (permalink / raw)
To: Paolo Abeni
Cc: netdev-bot+sashiko, dw, netdev, io-uring, linux-kselftest,
linux-kernel, kuba, asml.silence, shuah
On Thu, Sep 03, 2026 at 01:06:11PM -0600, Paolo Abeni wrote:
>
>
> On 9/3/26 1:38 AM, netdev-bot+sashiko@kernel.org wrote:
> > Thank you for your contribution! Sashiko AI review found 6 potential
> > issue(s) to consider:
> >
> > Critical: 0 · High: 1 · Medium: 2 (3) · Low: 3
> >
> > - [High] listen() is now executed before the workers have registered
> > their zcrx ifqs, breaking the ordering that two existing in-tree tests…
> > - [Medium] A worker thread that is dispatched zero connections reports
> > success without receiving or verifying a single byte.
> > - [Medium] The new CQ sizing removes the implicit bound that kept the
> > zcrx refill-queue producer inside one ring.
>
> My understanding is that the above 3 concerns may affect tests stability
> and should be addressed before merging.
>
> Thanks,
>
> Paolo
>
makes sense, I will address these in v7 of this patchset.
Best,
Juanlu
^ permalink raw reply [flat|nested] 15+ messages in thread
end of thread, other threads:[~2026-09-03 20:48 UTC | newest]
Thread overview: 15+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-31 17:34 [PATCH net-next v6 0/6] selftests: net: make iou-zcrx tests multithreaded Juanlu Herrero
2026-08-31 17:34 ` [PATCH net-next v6 1/6] selftests: net: fix get_refill_ring_size() to use its local variable Juanlu Herrero
2026-08-31 18:09 ` David Wei
2026-08-31 17:34 ` [PATCH net-next v6 2/6] selftests: net: remove unused variable in process_recvzc() Juanlu Herrero
2026-08-31 18:09 ` David Wei
2026-08-31 17:34 ` [PATCH net-next v6 3/6] selftests: net: refactor server state into struct thread_ctx Juanlu Herrero
2026-08-31 17:34 ` [PATCH net-next v6 4/6] selftests: net: add multithread client support to iou-zcrx Juanlu Herrero
2026-09-02 23:38 ` [net-next,v6,4/6] " netdev-bot+sashiko
2026-08-31 17:34 ` [PATCH net-next v6 5/6] selftests: net: add multithread server " Juanlu Herrero
2026-08-31 18:08 ` David Wei
2026-09-02 23:38 ` [net-next,v6,5/6] " netdev-bot+sashiko
2026-09-03 11:06 ` Paolo Abeni
2026-09-03 20:47 ` Juanlu Herrero
2026-08-31 17:34 ` [PATCH net-next v6 6/6] selftests: net: add rss_multiqueue test variant " Juanlu Herrero
2026-09-02 23:38 ` [net-next,v6,6/6] " netdev-bot+sashiko
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox