* [PATCH RFC v2 1/8] SUNRPC: Use atomic_t for XID allocation
2026-09-02 19:28 [PATCH RFC v2 0/8] Reduce lock contention in the NFS client Chuck Lever
@ 2026-09-02 19:28 ` Chuck Lever
2026-09-02 19:28 ` [PATCH RFC v2 2/8] SUNRPC: Split recv_lock out of xprt->queue_lock Chuck Lever
` (6 subsequent siblings)
7 siblings, 0 replies; 11+ messages in thread
From: Chuck Lever @ 2026-09-02 19:28 UTC (permalink / raw)
To: Trond Myklebust, Anna Schumaker, Tejun Heo
Cc: Lai Jiangshan, linux-nfs, open list, Chuck Lever
xprt_alloc_xid() acquires reserve_lock to increment a simple
counter. Under a high-IOPS NFSv3 workload on 100GbE RDMA,
profiling shows 1.06% of system-wide CPU cycles contending on
this lock in xprt_request_init, as ~150 RPC worker threads
serialize on the counter.
reserve_lock protects the slot table and backlog queue, but
XID allocation is an independent operation that does not require
synchronization with either.
Signed-off-by: Chuck Lever <cel@kernel.org>
---
include/linux/sunrpc/xprt.h | 2 +-
net/sunrpc/xprt.c | 9 ++-------
2 files changed, 3 insertions(+), 8 deletions(-)
diff --git a/include/linux/sunrpc/xprt.h b/include/linux/sunrpc/xprt.h
index a82045804d34..0d6c3f6bf97e 100644
--- a/include/linux/sunrpc/xprt.h
+++ b/include/linux/sunrpc/xprt.h
@@ -273,7 +273,7 @@ struct rpc_xprt {
spinlock_t transport_lock; /* lock transport info */
spinlock_t reserve_lock; /* lock slot table */
spinlock_t queue_lock; /* send/receive queue lock */
- u32 xid; /* Next XID value to use */
+ atomic_t xid; /* Most recently issued XID */
struct rpc_task * snd_task; /* Task blocked in send */
struct list_head xmit_queue; /* Send queue */
diff --git a/net/sunrpc/xprt.c b/net/sunrpc/xprt.c
index 48a3618cbb29..186c14f0f928 100644
--- a/net/sunrpc/xprt.c
+++ b/net/sunrpc/xprt.c
@@ -1882,18 +1882,13 @@ xprt_init_connect_cookie(struct rpc_rqst *req, struct rpc_xprt *xprt)
static __be32
xprt_alloc_xid(struct rpc_xprt *xprt)
{
- __be32 xid;
-
- spin_lock(&xprt->reserve_lock);
- xid = (__force __be32)xprt->xid++;
- spin_unlock(&xprt->reserve_lock);
- return xid;
+ return (__force __be32)atomic_inc_return(&xprt->xid);
}
static void
xprt_init_xid(struct rpc_xprt *xprt)
{
- xprt->xid = get_random_u32();
+ atomic_set(&xprt->xid, get_random_u32());
}
static void
--
2.55.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH RFC v2 2/8] SUNRPC: Split recv_lock out of xprt->queue_lock
2026-09-02 19:28 [PATCH RFC v2 0/8] Reduce lock contention in the NFS client Chuck Lever
2026-09-02 19:28 ` [PATCH RFC v2 1/8] SUNRPC: Use atomic_t for XID allocation Chuck Lever
@ 2026-09-02 19:28 ` Chuck Lever
2026-09-02 19:28 ` [PATCH RFC v2 3/8] SUNRPC: Set WQ_SYSFS on rpciod and xprtiod Chuck Lever
` (5 subsequent siblings)
7 siblings, 0 replies; 11+ messages in thread
From: Chuck Lever @ 2026-09-02 19:28 UTC (permalink / raw)
To: Trond Myklebust, Anna Schumaker, Tejun Heo
Cc: Lai Jiangshan, linux-nfs, open list, Chuck Lever
xprt->queue_lock protects two independent structures: the recv_queue
rb-tree for reply matching and the xmit_queue list for transmit
draining. No hot path touches both in one critical section, yet every
RPC submit and completion contends on the same lock. Under a 4KB
NFSv3 READ workload on 100GbE RDMA, 53% of non-idle CPU cycles are
spent in native_queued_spin_lock_slowpath: the CQ completion worker
running rpcrdma_reply_handler serializes against ~150 kworker threads
enqueuing receives and transmits.
Introduce xprt->recv_lock for the receive path -- recv_queue
operations, request lookup, receive-side pinning, and completion --
leaving queue_lock to the xmit_queue and the xprt_transmit drain
loop.
A request is pinned under the lock of the queue it was found through,
so xprt_request_dequeue_xprt() drains pins once under each lock
before it dequeues from that queue. The transmit dequeue has to see a
zero pin count under queue_lock: it frees the send buffer's bvec that
a transmitter is iterating, and it aborts a partial send only if the
request is still first in the queue. A receive-side unpin runs under
recv_lock rather than the lock that publishes RPC_TASK_MSG_PIN_WAIT,
so xprt_unpin_rqst() wakes the waiter whenever the count reaches
zero instead of testing the flag.
Also move the rq_private_buf memcpy in xprt_request_enqueue_receive
above the lock acquisition: until the rb-tree insert publishes the
request, the reply handler cannot see it, so the copy is safe
unlocked and the submitter's critical section shrinks to the insert
alone.
Signed-off-by: Chuck Lever <cel@kernel.org>
---
include/linux/sunrpc/xprt.h | 6 ++-
net/sunrpc/svcsock.c | 6 +--
net/sunrpc/xprt.c | 76 +++++++++++++++++++-----------
net/sunrpc/xprtrdma/rpc_rdma.c | 14 +++---
net/sunrpc/xprtrdma/svc_rdma_backchannel.c | 8 ++--
net/sunrpc/xprtsock.c | 18 +++----
6 files changed, 77 insertions(+), 51 deletions(-)
diff --git a/include/linux/sunrpc/xprt.h b/include/linux/sunrpc/xprt.h
index 0d6c3f6bf97e..ed1e28b74f02 100644
--- a/include/linux/sunrpc/xprt.h
+++ b/include/linux/sunrpc/xprt.h
@@ -272,7 +272,7 @@ struct rpc_xprt {
atomic_long_t queuelen;
spinlock_t transport_lock; /* lock transport info */
spinlock_t reserve_lock; /* lock slot table */
- spinlock_t queue_lock; /* send/receive queue lock */
+ spinlock_t queue_lock; /* send queue lock */
atomic_t xid; /* Most recently issued XID */
struct rpc_task * snd_task; /* Task blocked in send */
@@ -292,6 +292,10 @@ struct rpc_xprt {
* backchannel rpc_rqst's */
#endif /* CONFIG_SUNRPC_BACKCHANNEL */
+ /*
+ * Receive stuff
+ */
+ spinlock_t recv_lock; /* receive queue lock */
struct rb_root recv_queue; /* Receive queue */
struct {
diff --git a/net/sunrpc/svcsock.c b/net/sunrpc/svcsock.c
index 50e5e7f5b762..8939ba604385 100644
--- a/net/sunrpc/svcsock.c
+++ b/net/sunrpc/svcsock.c
@@ -1102,7 +1102,7 @@ static int receive_cb_reply(struct svc_sock *svsk, struct svc_rqst *rqstp)
if (!bc_xprt)
return -EAGAIN;
- spin_lock(&bc_xprt->queue_lock);
+ spin_lock(&bc_xprt->recv_lock);
req = xprt_lookup_rqst(bc_xprt, xid);
if (!req)
goto unlock_eagain;
@@ -1120,10 +1120,10 @@ static int receive_cb_reply(struct svc_sock *svsk, struct svc_rqst *rqstp)
memcpy(dst->iov_base, src->iov_base, src->iov_len);
xprt_complete_rqst(req->rq_task, rqstp->rq_arg.len);
rqstp->rq_arg.len = 0;
- spin_unlock(&bc_xprt->queue_lock);
+ spin_unlock(&bc_xprt->recv_lock);
return 0;
unlock_eagain:
- spin_unlock(&bc_xprt->queue_lock);
+ spin_unlock(&bc_xprt->recv_lock);
return -EAGAIN;
}
diff --git a/net/sunrpc/xprt.c b/net/sunrpc/xprt.c
index 186c14f0f928..883123ec70b0 100644
--- a/net/sunrpc/xprt.c
+++ b/net/sunrpc/xprt.c
@@ -1061,7 +1061,7 @@ xprt_request_rb_remove(struct rpc_xprt *xprt, struct rpc_rqst *req)
* @xprt: transport on which the original request was transmitted
* @xid: RPC XID of incoming reply
*
- * Caller holds xprt->queue_lock.
+ * Caller holds xprt->recv_lock.
*/
struct rpc_rqst *xprt_lookup_rqst(struct rpc_xprt *xprt, __be32 xid)
{
@@ -1092,8 +1092,9 @@ xprt_is_pinned_rqst(struct rpc_rqst *req)
* xprt_pin_rqst - Pin a request on the transport receive list
* @req: Request to pin
*
- * Caller must ensure this is atomic with the call to xprt_lookup_rqst()
- * so should be holding xprt->queue_lock.
+ * Caller must hold the lock that protects the queue through which
+ * it found the request: xprt->recv_lock for the receive path,
+ * xprt->queue_lock for the transmit drain path.
*/
void xprt_pin_rqst(struct rpc_rqst *req)
{
@@ -1105,14 +1106,10 @@ EXPORT_SYMBOL_GPL(xprt_pin_rqst);
* xprt_unpin_rqst - Unpin a request on the transport receive list
* @req: Request to pin
*
- * Caller should be holding xprt->queue_lock.
+ * Caller holds the lock it held for the matching xprt_pin_rqst().
*/
void xprt_unpin_rqst(struct rpc_rqst *req)
{
- if (!test_bit(RPC_TASK_MSG_PIN_WAIT, &req->rq_task->tk_runstate)) {
- atomic_dec(&req->rq_pin);
- return;
- }
if (atomic_dec_and_test(&req->rq_pin))
wake_up_var(&req->rq_pin);
}
@@ -1123,6 +1120,26 @@ static void xprt_wait_on_pinned_rqst(struct rpc_rqst *req)
wait_var_event(&req->rq_pin, !xprt_is_pinned_rqst(req));
}
+/*
+ * A pin is taken under the lock of the queue the request was found
+ * through, so a zero count observed under @lock rules out any pinner
+ * that came through that queue. The lock is dropped to wait, and the
+ * re-test under it catches a pinner that arrived in the gap.
+ */
+static void xprt_request_drain_pins(struct rpc_task *task, spinlock_t *lock)
+ __must_hold(lock)
+{
+ struct rpc_rqst *req = task->tk_rqstp;
+
+ while (xprt_is_pinned_rqst(req)) {
+ set_bit(RPC_TASK_MSG_PIN_WAIT, &task->tk_runstate);
+ spin_unlock(lock);
+ xprt_wait_on_pinned_rqst(req);
+ spin_lock(lock);
+ clear_bit(RPC_TASK_MSG_PIN_WAIT, &task->tk_runstate);
+ }
+}
+
static bool
xprt_request_data_received(struct rpc_task *task)
{
@@ -1155,16 +1172,16 @@ xprt_request_enqueue_receive(struct rpc_task *task)
ret = xprt_request_prepare(task->tk_rqstp, &req->rq_rcv_buf);
if (ret)
return ret;
- spin_lock(&xprt->queue_lock);
-
- /* Update the softirq receive buffer */
+ /* Reply handlers cannot find the request until the rb-tree
+ * insert below publishes it, so the copy needs no lock.
+ */
memcpy(&req->rq_private_buf, &req->rq_rcv_buf,
sizeof(req->rq_private_buf));
- /* Add request to the receive list */
+ spin_lock(&xprt->recv_lock);
xprt_request_rb_insert(xprt, req);
set_bit(RPC_TASK_NEED_RECV, &task->tk_runstate);
- spin_unlock(&xprt->queue_lock);
+ spin_unlock(&xprt->recv_lock);
/* Turn off autodisconnect */
timer_delete_sync(&xprt->timer);
@@ -1175,7 +1192,7 @@ xprt_request_enqueue_receive(struct rpc_task *task)
* xprt_request_dequeue_receive_locked - Remove a request from the receive queue
* @task: RPC task
*
- * Caller must hold xprt->queue_lock.
+ * Caller must hold xprt->recv_lock.
*/
static void
xprt_request_dequeue_receive_locked(struct rpc_task *task)
@@ -1190,7 +1207,7 @@ xprt_request_dequeue_receive_locked(struct rpc_task *task)
* xprt_update_rtt - Update RPC RTT statistics
* @task: RPC request that recently completed
*
- * Caller holds xprt->queue_lock.
+ * Caller holds xprt->recv_lock.
*/
void xprt_update_rtt(struct rpc_task *task)
{
@@ -1212,7 +1229,7 @@ EXPORT_SYMBOL_GPL(xprt_update_rtt);
* @task: RPC request that recently completed
* @copied: actual number of bytes received from the transport
*
- * Caller holds xprt->queue_lock.
+ * Caller holds xprt->recv_lock.
*/
void xprt_complete_rqst(struct rpc_task *task, int copied)
{
@@ -1309,7 +1326,7 @@ void xprt_request_wait_receive(struct rpc_task *task)
* The spinlock ensures atomicity between the test of
* req->rq_reply_bytes_recvd, and the call to rpc_sleep_on().
*/
- spin_lock(&xprt->queue_lock);
+ spin_lock(&xprt->recv_lock);
if (test_bit(RPC_TASK_NEED_RECV, &task->tk_runstate)) {
xprt->ops->wait_for_reply_request(task);
/*
@@ -1321,7 +1338,7 @@ void xprt_request_wait_receive(struct rpc_task *task)
rpc_wake_up_queued_task_set_status(&xprt->pending,
task, -ENOTCONN);
}
- spin_unlock(&xprt->queue_lock);
+ spin_unlock(&xprt->recv_lock);
}
static bool
@@ -1439,7 +1456,12 @@ xprt_request_dequeue_transmit(struct rpc_task *task)
* @task: pointer to rpc_task
*
* Remove a task from the transmit and receive queues, and ensure that
- * it is not pinned by the receive work item.
+ * it is not pinned by any concurrent work item.
+ *
+ * The transmit dequeue frees the send buffer's bvec and may abort a
+ * partial send, so it must not run while a transmitter holds a pin.
+ * Drain pins under each queue's lock before leaving that queue; once
+ * the request is off both, no new pin can be taken.
*/
void
xprt_request_dequeue_xprt(struct rpc_task *task)
@@ -1451,16 +1473,15 @@ xprt_request_dequeue_xprt(struct rpc_task *task)
test_bit(RPC_TASK_NEED_RECV, &task->tk_runstate) ||
xprt_is_pinned_rqst(req)) {
spin_lock(&xprt->queue_lock);
- while (xprt_is_pinned_rqst(req)) {
- set_bit(RPC_TASK_MSG_PIN_WAIT, &task->tk_runstate);
- spin_unlock(&xprt->queue_lock);
- xprt_wait_on_pinned_rqst(req);
- spin_lock(&xprt->queue_lock);
- clear_bit(RPC_TASK_MSG_PIN_WAIT, &task->tk_runstate);
- }
+ xprt_request_drain_pins(task, &xprt->queue_lock);
xprt_request_dequeue_transmit_locked(task);
- xprt_request_dequeue_receive_locked(task);
spin_unlock(&xprt->queue_lock);
+
+ spin_lock(&xprt->recv_lock);
+ xprt_request_drain_pins(task, &xprt->recv_lock);
+ xprt_request_dequeue_receive_locked(task);
+ spin_unlock(&xprt->recv_lock);
+
xdr_free_bvec(&req->rq_rcv_buf);
}
}
@@ -2038,6 +2059,7 @@ static void xprt_init(struct rpc_xprt *xprt, struct net *net)
spin_lock_init(&xprt->transport_lock);
spin_lock_init(&xprt->reserve_lock);
spin_lock_init(&xprt->queue_lock);
+ spin_lock_init(&xprt->recv_lock);
INIT_LIST_HEAD(&xprt->free);
xprt->recv_queue = RB_ROOT;
diff --git a/net/sunrpc/xprtrdma/rpc_rdma.c b/net/sunrpc/xprtrdma/rpc_rdma.c
index 1285f04cdac1..a82d3d9bc7ae 100644
--- a/net/sunrpc/xprtrdma/rpc_rdma.c
+++ b/net/sunrpc/xprtrdma/rpc_rdma.c
@@ -1321,9 +1321,9 @@ void rpcrdma_unpin_rqst(struct rpcrdma_rep *rep)
req->rl_reply = NULL;
rep->rr_rqst = NULL;
- spin_lock(&xprt->queue_lock);
+ spin_lock(&xprt->recv_lock);
xprt_unpin_rqst(rqst);
- spin_unlock(&xprt->queue_lock);
+ spin_unlock(&xprt->recv_lock);
}
/**
@@ -1363,10 +1363,10 @@ void rpcrdma_complete_rqst(struct rpcrdma_rep *rep)
goto out_badheader;
out:
- spin_lock(&xprt->queue_lock);
+ spin_lock(&xprt->recv_lock);
xprt_complete_rqst(rqst->rq_task, status);
xprt_unpin_rqst(rqst);
- spin_unlock(&xprt->queue_lock);
+ spin_unlock(&xprt->recv_lock);
return;
out_badheader:
@@ -1492,12 +1492,12 @@ void rpcrdma_reply_handler(struct rpcrdma_rep *rep)
/* Match incoming rpcrdma_rep to an rpcrdma_req to
* get context for handling any incoming chunks.
*/
- spin_lock(&xprt->queue_lock);
+ spin_lock(&xprt->recv_lock);
rqst = xprt_lookup_rqst(xprt, rep->rr_xid);
if (!rqst)
goto out_norqst;
xprt_pin_rqst(rqst);
- spin_unlock(&xprt->queue_lock);
+ spin_unlock(&xprt->recv_lock);
if (buf->rb_credits != credits)
rpcrdma_update_cwnd(r_xprt, credits);
@@ -1524,7 +1524,7 @@ void rpcrdma_reply_handler(struct rpcrdma_rep *rep)
return;
out_norqst:
- spin_unlock(&xprt->queue_lock);
+ spin_unlock(&xprt->recv_lock);
trace_xprtrdma_reply_rqst_err(rep);
rpcrdma_rep_put(buf, rep);
goto out_post;
diff --git a/net/sunrpc/xprtrdma/svc_rdma_backchannel.c b/net/sunrpc/xprtrdma/svc_rdma_backchannel.c
index e5a78b761012..3c7b85427f33 100644
--- a/net/sunrpc/xprtrdma/svc_rdma_backchannel.c
+++ b/net/sunrpc/xprtrdma/svc_rdma_backchannel.c
@@ -28,7 +28,7 @@ void svc_rdma_handle_bc_reply(struct svc_rqst *rqstp,
struct rpc_rqst *req;
u32 credits;
- spin_lock(&xprt->queue_lock);
+ spin_lock(&xprt->recv_lock);
req = xprt_lookup_rqst(xprt, *rdma_resp);
if (!req)
goto out_unlock;
@@ -39,7 +39,7 @@ void svc_rdma_handle_bc_reply(struct svc_rqst *rqstp,
goto out_unlock;
memcpy(dst->iov_base, src->iov_base, src->iov_len);
xprt_pin_rqst(req);
- spin_unlock(&xprt->queue_lock);
+ spin_unlock(&xprt->recv_lock);
credits = be32_to_cpup(rdma_resp + 2);
if (credits == 0)
@@ -50,13 +50,13 @@ void svc_rdma_handle_bc_reply(struct svc_rqst *rqstp,
xprt->cwnd = credits << RPC_CWNDSHIFT;
spin_unlock(&xprt->transport_lock);
- spin_lock(&xprt->queue_lock);
+ spin_lock(&xprt->recv_lock);
xprt_complete_rqst(req->rq_task, rcvbuf->len);
xprt_unpin_rqst(req);
rcvbuf->len = 0;
out_unlock:
- spin_unlock(&xprt->queue_lock);
+ spin_unlock(&xprt->recv_lock);
}
/* Send a reverse-direction RPC Call.
diff --git a/net/sunrpc/xprtsock.c b/net/sunrpc/xprtsock.c
index 7f60723fa64d..1454da9575b3 100644
--- a/net/sunrpc/xprtsock.c
+++ b/net/sunrpc/xprtsock.c
@@ -673,25 +673,25 @@ xs_read_stream_reply(struct sock_xprt *transport, struct msghdr *msg, int flags)
ssize_t ret = 0;
/* Look up and lock the request corresponding to the given XID */
- spin_lock(&xprt->queue_lock);
+ spin_lock(&xprt->recv_lock);
req = xprt_lookup_rqst(xprt, transport->recv.xid);
if (!req || (transport->recv.copied && !req->rq_private_buf.len)) {
msg->msg_flags |= MSG_TRUNC;
goto out;
}
xprt_pin_rqst(req);
- spin_unlock(&xprt->queue_lock);
+ spin_unlock(&xprt->recv_lock);
ret = xs_read_stream_request(transport, msg, flags, req);
- spin_lock(&xprt->queue_lock);
+ spin_lock(&xprt->recv_lock);
if (msg->msg_flags & (MSG_EOR|MSG_TRUNC))
xprt_complete_rqst(req->rq_task, transport->recv.copied);
else
req->rq_private_buf.len = transport->recv.copied;
xprt_unpin_rqst(req);
out:
- spin_unlock(&xprt->queue_lock);
+ spin_unlock(&xprt->recv_lock);
return ret;
}
@@ -1398,13 +1398,13 @@ static void xs_udp_data_read_skb(struct rpc_xprt *xprt,
return;
/* Look up and lock the request corresponding to the given XID */
- spin_lock(&xprt->queue_lock);
+ spin_lock(&xprt->recv_lock);
rovr = xprt_lookup_rqst(xprt, *xp);
if (!rovr)
goto out_unlock;
xprt_pin_rqst(rovr);
xprt_update_rtt(rovr->rq_task);
- spin_unlock(&xprt->queue_lock);
+ spin_unlock(&xprt->recv_lock);
task = rovr->rq_task;
if ((copied = rovr->rq_private_buf.buflen) > repsize)
@@ -1412,7 +1412,7 @@ static void xs_udp_data_read_skb(struct rpc_xprt *xprt,
/* Suck it into the iovec, verify checksum if not done by hw. */
if (csum_partial_copy_to_xdr(&rovr->rq_private_buf, skb)) {
- spin_lock(&xprt->queue_lock);
+ spin_lock(&xprt->recv_lock);
__UDPX_INC_STATS(sk, UDP_MIB_INERRORS);
goto out_unpin;
}
@@ -1421,13 +1421,13 @@ static void xs_udp_data_read_skb(struct rpc_xprt *xprt,
spin_lock(&xprt->transport_lock);
xprt_adjust_cwnd(xprt, task, copied);
spin_unlock(&xprt->transport_lock);
- spin_lock(&xprt->queue_lock);
+ spin_lock(&xprt->recv_lock);
xprt_complete_rqst(task, copied);
__UDPX_INC_STATS(sk, UDP_MIB_INDATAGRAMS);
out_unpin:
xprt_unpin_rqst(rovr);
out_unlock:
- spin_unlock(&xprt->queue_lock);
+ spin_unlock(&xprt->recv_lock);
}
static void xs_udp_data_receive(struct sock_xprt *transport)
--
2.55.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH RFC v2 3/8] SUNRPC: Set WQ_SYSFS on rpciod and xprtiod
2026-09-02 19:28 [PATCH RFC v2 0/8] Reduce lock contention in the NFS client Chuck Lever
2026-09-02 19:28 ` [PATCH RFC v2 1/8] SUNRPC: Use atomic_t for XID allocation Chuck Lever
2026-09-02 19:28 ` [PATCH RFC v2 2/8] SUNRPC: Split recv_lock out of xprt->queue_lock Chuck Lever
@ 2026-09-02 19:28 ` Chuck Lever
2026-09-02 19:28 ` [PATCH RFC v2 4/8] NFS: Set WQ_SYSFS on nfsiod Chuck Lever
` (4 subsequent siblings)
7 siblings, 0 replies; 11+ messages in thread
From: Chuck Lever @ 2026-09-02 19:28 UTC (permalink / raw)
To: Trond Myklebust, Anna Schumaker, Tejun Heo
Cc: Lai Jiangshan, linux-nfs, open list, Chuck Lever
An unbound workqueue's attributes can be changed at run time only
through /sys/devices/virtual/workqueue/, and a workqueue appears
there only when it is created with WQ_SYSFS. rpciod and xprtiod are
created without it, so their affinity scope cannot be tuned without
reloading the sunrpc module.
Create both with WQ_SYSFS.
Signed-off-by: Chuck Lever <cel@kernel.org>
---
net/sunrpc/sched.c | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/net/sunrpc/sched.c b/net/sunrpc/sched.c
index 016f16ca5779..e81419aa553c 100644
--- a/net/sunrpc/sched.c
+++ b/net/sunrpc/sched.c
@@ -1273,16 +1273,17 @@ void rpciod_down(void)
*/
static int rpciod_start(void)
{
+ const unsigned int wq_flags = WQ_MEM_RECLAIM | WQ_UNBOUND | WQ_SYSFS;
struct workqueue_struct *wq;
/*
* Create the rpciod thread and wait for it to start.
*/
- wq = alloc_workqueue("rpciod", WQ_MEM_RECLAIM | WQ_UNBOUND, 0);
+ wq = alloc_workqueue("rpciod", wq_flags, 0);
if (!wq)
goto out_failed;
rpciod_workqueue = wq;
- wq = alloc_workqueue("xprtiod", WQ_UNBOUND | WQ_MEM_RECLAIM, 0);
+ wq = alloc_workqueue("xprtiod", wq_flags, 0);
if (!wq)
goto free_rpciod;
xprtiod_workqueue = wq;
--
2.55.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH RFC v2 4/8] NFS: Set WQ_SYSFS on nfsiod
2026-09-02 19:28 [PATCH RFC v2 0/8] Reduce lock contention in the NFS client Chuck Lever
` (2 preceding siblings ...)
2026-09-02 19:28 ` [PATCH RFC v2 3/8] SUNRPC: Set WQ_SYSFS on rpciod and xprtiod Chuck Lever
@ 2026-09-02 19:28 ` Chuck Lever
2026-09-02 19:28 ` [PATCH RFC v2 5/8] workqueue: add workqueue_set_affn_scope() Chuck Lever
` (3 subsequent siblings)
7 siblings, 0 replies; 11+ messages in thread
From: Chuck Lever @ 2026-09-02 19:28 UTC (permalink / raw)
To: Trond Myklebust, Anna Schumaker, Tejun Heo
Cc: Lai Jiangshan, linux-nfs, open list, Chuck Lever
An unbound workqueue's attributes can be changed at run time only
through /sys/devices/virtual/workqueue/, and a workqueue appears
there only when it is created with WQ_SYSFS. nfsiod is created
without it, so its affinity scope cannot be tuned without reloading
the nfs module.
Create nfsiod with WQ_SYSFS.
Signed-off-by: Chuck Lever <cel@kernel.org>
---
fs/nfs/inode.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/fs/nfs/inode.c b/fs/nfs/inode.c
index 3022454f7698..107a2135029d 100644
--- a/fs/nfs/inode.c
+++ b/fs/nfs/inode.c
@@ -2619,7 +2619,8 @@ static void nfsiod_stop(void)
static int nfsiod_start(void)
{
dprintk("RPC: creating workqueue nfsiod\n");
- nfsiod_workqueue = alloc_workqueue("nfsiod", WQ_MEM_RECLAIM | WQ_UNBOUND, 0);
+ nfsiod_workqueue = alloc_workqueue("nfsiod",
+ WQ_MEM_RECLAIM | WQ_UNBOUND | WQ_SYSFS, 0);
if (nfsiod_workqueue == NULL)
return -ENOMEM;
#if IS_ENABLED(CONFIG_NFS_LOCALIO)
--
2.55.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH RFC v2 5/8] workqueue: add workqueue_set_affn_scope()
2026-09-02 19:28 [PATCH RFC v2 0/8] Reduce lock contention in the NFS client Chuck Lever
` (3 preceding siblings ...)
2026-09-02 19:28 ` [PATCH RFC v2 4/8] NFS: Set WQ_SYSFS on nfsiod Chuck Lever
@ 2026-09-02 19:28 ` Chuck Lever
2026-09-02 19:36 ` Tejun Heo
2026-09-02 19:28 ` [PATCH RFC v2 6/8] SUNRPC: Reduce rpciod workqueue contention Chuck Lever
` (2 subsequent siblings)
7 siblings, 1 reply; 11+ messages in thread
From: Chuck Lever @ 2026-09-02 19:28 UTC (permalink / raw)
To: Trond Myklebust, Anna Schumaker, Tejun Heo
Cc: Lai Jiangshan, linux-nfs, open list, Chuck Lever
An unbound workqueue's affinity scope can be changed only through
sysfs. A module whose workqueue contends on the pool lock at the
default scope has no in-kernel way to select a finer one, because
alloc_workqueue_attrs() and apply_workqueue_attrs() are not
exported. Exporting them would also invite a caller to apply freshly
allocated attributes, which resets the nice level, cpumask, and
strict affinity the workqueue already carries.
Add workqueue_set_affn_scope(), which copies the workqueue's current
attributes, replaces only the scope, and applies the result under
wq_pool_mutex, as the sysfs affinity_scope store does. Export it so
that SUNRPC and NFS can set the scope of their workqueues when they
create them.
Signed-off-by: Chuck Lever <cel@kernel.org>
---
include/linux/workqueue.h | 2 ++
kernel/workqueue.c | 37 +++++++++++++++++++++++++++++++++++++
2 files changed, 39 insertions(+)
diff --git a/include/linux/workqueue.h b/include/linux/workqueue.h
index c8a36423cb34..585c32d8dc98 100644
--- a/include/linux/workqueue.h
+++ b/include/linux/workqueue.h
@@ -618,6 +618,8 @@ struct workqueue_attrs *alloc_workqueue_attrs_noprof(void);
void free_workqueue_attrs(struct workqueue_attrs *attrs);
int apply_workqueue_attrs(struct workqueue_struct *wq,
const struct workqueue_attrs *attrs);
+int workqueue_set_affn_scope(struct workqueue_struct *wq,
+ enum wq_affn_scope affn_scope);
extern int workqueue_unbound_housekeeping_update(const struct cpumask *hk);
extern bool queue_work_on(int cpu, struct workqueue_struct *wq,
diff --git a/kernel/workqueue.c b/kernel/workqueue.c
index 3c034cbc5bb3..0c2b8e87cd59 100644
--- a/kernel/workqueue.c
+++ b/kernel/workqueue.c
@@ -5610,6 +5610,43 @@ int apply_workqueue_attrs(struct workqueue_struct *wq,
return ret;
}
+/**
+ * workqueue_set_affn_scope - change the affinity scope of an unbound workqueue
+ * @wq: the target unbound workqueue
+ * @affn_scope: the new scope, or %WQ_AFFN_DFL for the system default
+ *
+ * Reapply @wq's current attributes with only the affinity scope
+ * replaced, so the nice level, cpumask, and strict affinity the
+ * workqueue already carries survive. Pool-workqueue replacement
+ * proceeds as for apply_workqueue_attrs().
+ *
+ * Context: Process context. Takes wq_pool_mutex and performs
+ * GFP_KERNEL allocations.
+ *
+ * Return: 0 on success and -errno on failure.
+ */
+int workqueue_set_affn_scope(struct workqueue_struct *wq,
+ enum wq_affn_scope affn_scope)
+{
+ struct workqueue_attrs *attrs;
+ int ret = -ENOMEM;
+
+ if ((unsigned int)affn_scope >= WQ_AFFN_NR_TYPES)
+ return -EINVAL;
+
+ mutex_lock(&wq_pool_mutex);
+ attrs = alloc_workqueue_attrs();
+ if (attrs) {
+ copy_workqueue_attrs(attrs, wq->attrs);
+ attrs->affn_scope = affn_scope;
+ ret = apply_workqueue_attrs_locked(wq, attrs);
+ }
+ mutex_unlock(&wq_pool_mutex);
+ free_workqueue_attrs(attrs);
+ return ret;
+}
+EXPORT_SYMBOL_GPL(workqueue_set_affn_scope);
+
/**
* unbound_wq_update_pwq - update a pwq slot for CPU hot[un]plug
* @wq: the target workqueue
--
2.55.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* Re: [PATCH RFC v2 5/8] workqueue: add workqueue_set_affn_scope()
2026-09-02 19:28 ` [PATCH RFC v2 5/8] workqueue: add workqueue_set_affn_scope() Chuck Lever
@ 2026-09-02 19:36 ` Tejun Heo
2026-09-03 13:41 ` Chuck Lever
0 siblings, 1 reply; 11+ messages in thread
From: Tejun Heo @ 2026-09-02 19:36 UTC (permalink / raw)
To: Chuck Lever
Cc: Trond Myklebust, Anna Schumaker, Lai Jiangshan, linux-nfs,
open list
On Wed, Sep 02, 2026 at 03:28:50PM -0400, Chuck Lever wrote:
> An unbound workqueue's affinity scope can be changed only through
> sysfs. A module whose workqueue contends on the pool lock at the
> default scope has no in-kernel way to select a finer one, because
> alloc_workqueue_attrs() and apply_workqueue_attrs() are not
> exported. Exporting them would also invite a caller to apply freshly
BTW, please feel free to export them.
> +int workqueue_set_affn_scope(struct workqueue_struct *wq,
> + enum wq_affn_scope affn_scope)
No need for the line break.
> +{
> + struct workqueue_attrs *attrs;
> + int ret = -ENOMEM;
> +
> + if ((unsigned int)affn_scope >= WQ_AFFN_NR_TYPES)
> + return -EINVAL;
> +
> + mutex_lock(&wq_pool_mutex);
> + attrs = alloc_workqueue_attrs();
Maybe alloc outside mutex and use alloc and mutex guards?
Thanks.
--
tejun
^ permalink raw reply [flat|nested] 11+ messages in thread* Re: [PATCH RFC v2 5/8] workqueue: add workqueue_set_affn_scope()
2026-09-02 19:36 ` Tejun Heo
@ 2026-09-03 13:41 ` Chuck Lever
0 siblings, 0 replies; 11+ messages in thread
From: Chuck Lever @ 2026-09-03 13:41 UTC (permalink / raw)
To: Tejun Heo
Cc: Trond Myklebust, Anna Schumaker, Lai Jiangshan, linux-nfs,
open list
On Wed, Sep 2, 2026, at 3:36 PM, Tejun Heo wrote:
> On Wed, Sep 02, 2026 at 03:28:50PM -0400, Chuck Lever wrote:
>> An unbound workqueue's affinity scope can be changed only through
>> sysfs. A module whose workqueue contends on the pool lock at the
>> default scope has no in-kernel way to select a finer one, because
>> alloc_workqueue_attrs() and apply_workqueue_attrs() are not
>> exported. Exporting them would also invite a caller to apply freshly
>
> BTW, please feel free to export them.
>
>> +int workqueue_set_affn_scope(struct workqueue_struct *wq,
>> + enum wq_affn_scope affn_scope)
>
> No need for the line break.
>
>> +{
>> + struct workqueue_attrs *attrs;
>> + int ret = -ENOMEM;
>> +
>> + if ((unsigned int)affn_scope >= WQ_AFFN_NR_TYPES)
>> + return -EINVAL;
>> +
>> + mutex_lock(&wq_pool_mutex);
>> + attrs = alloc_workqueue_attrs();
>
> Maybe alloc outside mutex and use alloc and mutex guards?
>
> Thanks.
>
> --
> tejun
All applied to v3 (still unposted). Sorry all this has taken so long.
--
Chuck Lever (Come to NFS bake-a-thon! https://nfsv4bat.org)
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH RFC v2 6/8] SUNRPC: Reduce rpciod workqueue contention
2026-09-02 19:28 [PATCH RFC v2 0/8] Reduce lock contention in the NFS client Chuck Lever
` (4 preceding siblings ...)
2026-09-02 19:28 ` [PATCH RFC v2 5/8] workqueue: add workqueue_set_affn_scope() Chuck Lever
@ 2026-09-02 19:28 ` Chuck Lever
2026-09-02 19:28 ` [PATCH RFC v2 7/8] NFS: Reduce nfsiod " Chuck Lever
2026-09-02 19:28 ` [PATCH RFC v2 8/8] SUNRPC: Reduce xprtiod " Chuck Lever
7 siblings, 0 replies; 11+ messages in thread
From: Chuck Lever @ 2026-09-02 19:28 UTC (permalink / raw)
To: Trond Myklebust, Anna Schumaker, Tejun Heo
Cc: Lai Jiangshan, linux-nfs, open list, Chuck Lever
rpciod drives the RPC client state machine. Under heavy NFS
workloads, multiple CPUs queue RPC task completions concurrently and
contend on the UNBOUND worker pool lock. perf profiles on a 12-core
system show 30-40% of cycles lost to
native_queued_spin_lock_slowpath in the rpciod pool at the
WQ_AFFN_CACHE scope (one pool per LLC). The WQ_AFFN_CACHE_SHARD
default helps little here, because its 8-core shards split this
system into just two pools of six cores each.
Set WQ_AFFN_SMT on rpciod so each SMT group gets its own pool and
lock. Most UNBOUND workqueues never contend on the pool lock and
profit from a coarser scope's cache locality. rpciod's sustained
completion traffic makes the lock a first-order bottleneck, so the
override belongs on this workqueue rather than in the system-wide
default. The cost is one pool per SMT group, or per CPU on a system
without SMT, and each pool keeps up to two idle kworkers rather than
culling its last ones.
Suggested-by: Tejun Heo <tj@kernel.org>
Signed-off-by: Chuck Lever <cel@kernel.org>
---
net/sunrpc/sched.c | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/net/sunrpc/sched.c b/net/sunrpc/sched.c
index e81419aa553c..2a1938b9e41e 100644
--- a/net/sunrpc/sched.c
+++ b/net/sunrpc/sched.c
@@ -1268,6 +1268,17 @@ void rpciod_down(void)
module_put(THIS_MODULE);
}
+static void rpc_set_wq_smt_affinity(struct workqueue_struct *wq,
+ const char *name)
+{
+ int err;
+
+ err = workqueue_set_affn_scope(wq, WQ_AFFN_SMT);
+ if (err)
+ pr_warn("%s: failed to set SMT affinity scope: %d\n",
+ name, err);
+}
+
/*
* Start up the rpciod workqueue.
*/
@@ -1282,6 +1293,7 @@ static int rpciod_start(void)
wq = alloc_workqueue("rpciod", wq_flags, 0);
if (!wq)
goto out_failed;
+ rpc_set_wq_smt_affinity(wq, "rpciod");
rpciod_workqueue = wq;
wq = alloc_workqueue("xprtiod", wq_flags, 0);
if (!wq)
--
2.55.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH RFC v2 7/8] NFS: Reduce nfsiod workqueue contention
2026-09-02 19:28 [PATCH RFC v2 0/8] Reduce lock contention in the NFS client Chuck Lever
` (5 preceding siblings ...)
2026-09-02 19:28 ` [PATCH RFC v2 6/8] SUNRPC: Reduce rpciod workqueue contention Chuck Lever
@ 2026-09-02 19:28 ` Chuck Lever
2026-09-02 19:28 ` [PATCH RFC v2 8/8] SUNRPC: Reduce xprtiod " Chuck Lever
7 siblings, 0 replies; 11+ messages in thread
From: Chuck Lever @ 2026-09-02 19:28 UTC (permalink / raw)
To: Trond Myklebust, Anna Schumaker, Tejun Heo
Cc: Lai Jiangshan, linux-nfs, open list, Chuck Lever
The default affinity scope for unbound workqueues is now
WQ_AFFN_CACHE_SHARD, which splits each LLC into shards of about
eight cores. On a single-socket system whose LLC fits in one
shard, every NFS I/O completion serializes on one nfsiod pool
lock. Profiling 4KB random writes over NFSv3/RDMA with nconnect=3
shows that lock consuming 17% of CPU cycles: 8% dequeuing work and
9% enqueuing follow-on work from rpciod and nfsiod workers.
Set nfsiod's affinity scope to WQ_AFFN_SMT so each SMT group gets
its own pool and queue_work_on() contends only with sibling
threads. Enqueue contention disappears and dequeue contention drops
to 1.4%. Throughput is unchanged because the workload is
transport-limited, but the freed cycles cut submission latency
variance by 67% (slat stdev 31.6 us to 10.5 us), IOPS stdev by
31%, and p99.9 completion latency by 11%.
Suggested-by: Tejun Heo <tj@kernel.org>
Signed-off-by: Chuck Lever <cel@kernel.org>
---
fs/nfs/inode.c | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/fs/nfs/inode.c b/fs/nfs/inode.c
index 107a2135029d..21c4560696bd 100644
--- a/fs/nfs/inode.c
+++ b/fs/nfs/inode.c
@@ -2618,11 +2618,16 @@ static void nfsiod_stop(void)
*/
static int nfsiod_start(void)
{
+ int err;
+
dprintk("RPC: creating workqueue nfsiod\n");
nfsiod_workqueue = alloc_workqueue("nfsiod",
WQ_MEM_RECLAIM | WQ_UNBOUND | WQ_SYSFS, 0);
if (nfsiod_workqueue == NULL)
return -ENOMEM;
+ err = workqueue_set_affn_scope(nfsiod_workqueue, WQ_AFFN_SMT);
+ if (err)
+ pr_warn("nfsiod: failed to set SMT affinity scope: %d\n", err);
#if IS_ENABLED(CONFIG_NFS_LOCALIO)
/*
* localio writes need to use a normal (non-memreclaim) workqueue.
--
2.55.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH RFC v2 8/8] SUNRPC: Reduce xprtiod workqueue contention
2026-09-02 19:28 [PATCH RFC v2 0/8] Reduce lock contention in the NFS client Chuck Lever
` (6 preceding siblings ...)
2026-09-02 19:28 ` [PATCH RFC v2 7/8] NFS: Reduce nfsiod " Chuck Lever
@ 2026-09-02 19:28 ` Chuck Lever
7 siblings, 0 replies; 11+ messages in thread
From: Chuck Lever @ 2026-09-02 19:28 UTC (permalink / raw)
To: Trond Myklebust, Anna Schumaker, Tejun Heo
Cc: Lai Jiangshan, linux-nfs, open list, Chuck Lever
xprtiod handles transport-level operations: socket receive
processing, error recovery, and connection lifecycle. On systems
driving heavy NFS traffic, these operations contend on the UNBOUND
worker pool lock just as rpciod does.
Set WQ_AFFN_SMT on the xprtiod workqueue to give each SMT group its
own pool and lock.
Suggested-by: Tejun Heo <tj@kernel.org>
Signed-off-by: Chuck Lever <cel@kernel.org>
---
net/sunrpc/sched.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/net/sunrpc/sched.c b/net/sunrpc/sched.c
index 2a1938b9e41e..e678326e6b0d 100644
--- a/net/sunrpc/sched.c
+++ b/net/sunrpc/sched.c
@@ -1298,6 +1298,7 @@ static int rpciod_start(void)
wq = alloc_workqueue("xprtiod", wq_flags, 0);
if (!wq)
goto free_rpciod;
+ rpc_set_wq_smt_affinity(wq, "xprtiod");
xprtiod_workqueue = wq;
return 1;
free_rpciod:
--
2.55.0
^ permalink raw reply related [flat|nested] 11+ messages in thread