* [PATCH] nvme-tcp: fix io_work priority inversion when send_mutex is held
@ 2026-10-14 6:45 Seiji Nishikawa
2026-10-21 10:04 ` Hannes Reinecke
0 siblings, 1 reply; 3+ messages in thread
From: Seiji Nishikawa @ 2026-10-14 6:45 UTC (permalink / raw)
To: Keith Busch, Jens Axboe, Christoph Hellwig, Sagi Grimberg
Cc: Hannes Reinecke, Chris Leech, linux-nvme, linux-kernel,
Seiji Nishikawa, stable
Commit f42d4796ee10 ("nvme-tcp: fix I/O stalls on congested sockets")
added a check in nvme_tcp_io_work() that sets pending to true when the
queue has pending requests and the socket is writeable. The check is
placed after the mutex_trylock() block, so it also runs when
mutex_trylock(&queue->send_mutex) failed and io_work did not send
anything.
When .queue_rq sends a request directly from its own context, it holds
send_mutex and can give up the CPU in lock_sock_nested() when another
task needs the CPU, for example the io_work kworker of the same queue,
which runs on the same CPU. Then io_work cannot take send_mutex, but
the requests are still on req_list and the socket is writeable, so
pending is set to true. io_work loops until the deadline and queues
itself again on io_cpu, without sleeping.
This is a priority inversion. io_work runs on the high priority
nvme_tcp_wq and cannot make progress until send_mutex is released, but
it keeps running. The task holding send_mutex runs at normal priority
and needs the CPU to release it. The scheduler keeps giving the CPU to
io_work, so the task holding send_mutex may not run again, and the
requests on the queue are not sent and time out.
This is the same problem that commit 70f437fb4395 ("nvme-tcp: fix
io_work priority inversion") fixed. That commit removed
"pending = !llist_empty(&queue->req_list)" from the path where
mutex_trylock() fails, and let the task holding send_mutex queue
io_work again in nvme_tcp_queue_request() after it releases the mutex.
The new check brings back the same behavior in a different form.
Two things make this easier to hit. nvme_tcp_queue_request() sends
directly only when it runs on io_cpu, so the task that takes send_mutex
is on the same CPU as the io_work kworker of that queue when it takes
the mutex. Also, when the tasks are in a cgroup with the cpu controller
enabled, the whole group competes with the nice -20 kworker as one
entity, so each task in the group gets even less CPU time.
We hit this on a 6 CPU VM running a database on nvme-tcp storage. In
the vmcore, 2 tasks held send_mutex of 2 queues and were runnable in
lock_sock_nested() for 103 seconds, while the kworkers for those queues
kept running io_work on their CPUs. All database tasks were in one
cgroup, which got only about 0.3% of the CPU against the nice -20
kworker. Requests timed out, and blk_mq_timeout_work() waited in
synchronize_srcu() for the tasks still in .queue_rq.
Only do the pending check if io_work could take send_mutex. If it could
not, the task holding send_mutex sends what it can in
nvme_tcp_send_all(), and queues io_work again after it releases the
mutex if requests are still pending, so no request is left behind.
Fixes: f42d4796ee10 ("nvme-tcp: fix I/O stalls on congested sockets")
Cc: stable@vger.kernel.org # v6.16+
Signed-off-by: Seiji Nishikawa <snishika@redhat.com>
---
drivers/nvme/host/tcp.c | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/drivers/nvme/host/tcp.c b/drivers/nvme/host/tcp.c
index 7eec983c0ead..4b775e22d3cc 100644
--- a/drivers/nvme/host/tcp.c
+++ b/drivers/nvme/host/tcp.c
@@ -1433,9 +1433,11 @@ static void nvme_tcp_io_work(struct work_struct *w)
do {
bool pending = false;
+ bool got_send_mutex = false;
int result;
if (mutex_trylock(&queue->send_mutex)) {
+ got_send_mutex = true;
result = nvme_tcp_try_send(queue);
mutex_unlock(&queue->send_mutex);
if (result > 0)
@@ -1450,8 +1452,13 @@ static void nvme_tcp_io_work(struct work_struct *w)
else if (unlikely(result < 0))
return;
- /* did we get some space after spending time in recv? */
- if (nvme_tcp_queue_has_pending(queue) &&
+ /*
+ * did we get some space after spending time in recv?
+ * Only check this if we could take send_mutex. Otherwise
+ * the task holding it sends the requests and queues io_work
+ * again after it releases the mutex.
+ */
+ if (got_send_mutex && nvme_tcp_queue_has_pending(queue) &&
sk_stream_is_writeable(queue->sock->sk))
pending = true;
base-commit: 5755f1bd0217ffe3c5cf85467bf6b0680f398265
--
2.55.0
^ permalink raw reply [flat|nested] 3+ messages in thread* Re: [PATCH] nvme-tcp: fix io_work priority inversion when send_mutex is held 2026-10-14 6:45 [PATCH] nvme-tcp: fix io_work priority inversion when send_mutex is held Seiji Nishikawa @ 2026-10-21 10:04 ` Hannes Reinecke 2026-10-23 12:23 ` Seiji Nishikawa 0 siblings, 1 reply; 3+ messages in thread From: Hannes Reinecke @ 2026-10-21 10:04 UTC (permalink / raw) To: Seiji Nishikawa, Keith Busch, Jens Axboe, Christoph Hellwig, Sagi Grimberg Cc: Hannes Reinecke, Chris Leech, linux-nvme, linux-kernel, stable On 10/14/26 8:45 AM, Seiji Nishikawa wrote: > Commit f42d4796ee10 ("nvme-tcp: fix I/O stalls on congested sockets") > added a check in nvme_tcp_io_work() that sets pending to true when the > queue has pending requests and the socket is writeable. The check is > placed after the mutex_trylock() block, so it also runs when > mutex_trylock(&queue->send_mutex) failed and io_work did not send > anything. > > When .queue_rq sends a request directly from its own context, it holds > send_mutex and can give up the CPU in lock_sock_nested() when another > task needs the CPU, for example the io_work kworker of the same queue, > which runs on the same CPU. Then io_work cannot take send_mutex, but > the requests are still on req_list and the socket is writeable, so > pending is set to true. io_work loops until the deadline and queues > itself again on io_cpu, without sleeping. > > This is a priority inversion. io_work runs on the high priority > nvme_tcp_wq and cannot make progress until send_mutex is released, but > it keeps running. The task holding send_mutex runs at normal priority > and needs the CPU to release it. The scheduler keeps giving the CPU to > io_work, so the task holding send_mutex may not run again, and the > requests on the queue are not sent and time out. > > This is the same problem that commit 70f437fb4395 ("nvme-tcp: fix > io_work priority inversion") fixed. That commit removed > "pending = !llist_empty(&queue->req_list)" from the path where > mutex_trylock() fails, and let the task holding send_mutex queue > io_work again in nvme_tcp_queue_request() after it releases the mutex. > The new check brings back the same behavior in a different form. > > Two things make this easier to hit. nvme_tcp_queue_request() sends > directly only when it runs on io_cpu, so the task that takes send_mutex > is on the same CPU as the io_work kworker of that queue when it takes > the mutex. Also, when the tasks are in a cgroup with the cpu controller > enabled, the whole group competes with the nice -20 kworker as one > entity, so each task in the group gets even less CPU time. > > We hit this on a 6 CPU VM running a database on nvme-tcp storage. In > the vmcore, 2 tasks held send_mutex of 2 queues and were runnable in > lock_sock_nested() for 103 seconds, while the kworkers for those queues > kept running io_work on their CPUs. All database tasks were in one > cgroup, which got only about 0.3% of the CPU against the nice -20 > kworker. Requests timed out, and blk_mq_timeout_work() waited in > synchronize_srcu() for the tasks still in .queue_rq. > How very interesting. I don't think you could craft a blktest for this? > Only do the pending check if io_work could take send_mutex. If it could > not, the task holding send_mutex sends what it can in > nvme_tcp_send_all(), and queues io_work again after it releases the > mutex if requests are still pending, so no request is left behind. > The direct send path again. I really wonder if we need it at all, or whether we cannot simply delete it. Can you try if you see a regression with this patch? diff --git a/drivers/nvme/host/tcp.c b/drivers/nvme/host/tcp.c index ba5c7b3e2a7c..2c59b550e13f 100644 --- a/drivers/nvme/host/tcp.c +++ b/drivers/nvme/host/tcp.c @@ -405,17 +405,6 @@ static inline void nvme_tcp_queue_request(struct nvme_tcp_request *req, empty = llist_add(&req->lentry, &queue->req_list) && list_empty(&queue->send_list) && !queue->request; - /* - * if we're the first on the send_list and we can try to send - * directly, otherwise queue io_work. Also, only do that if we - * are on the same cpu, so we don't introduce contention. - */ - if (queue->io_cpu == raw_smp_processor_id() && - empty && mutex_trylock(&queue->send_mutex)) { - nvme_tcp_send_all(queue); - mutex_unlock(&queue->send_mutex); - } - if (last && nvme_tcp_queue_has_pending(queue)) queue_work_on(queue->io_cpu, nvme_tcp_wq, &queue->io_work); } Cheers, Hannes -- Dr. Hannes Reinecke Kernel Storage Architect hare@suse.de +49 911 74053 688 SUSE Software Solutions GmbH, Frankenstr. 146, 90461 Nürnberg HRB 36809 (AG Nürnberg), GF: I. Totev, A. McDonald, W. Knoblich ^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [PATCH] nvme-tcp: fix io_work priority inversion when send_mutex is held 2026-10-21 10:04 ` Hannes Reinecke @ 2026-10-23 12:23 ` Seiji Nishikawa 0 siblings, 0 replies; 3+ messages in thread From: Seiji Nishikawa @ 2026-10-23 12:23 UTC (permalink / raw) To: Hannes Reinecke Cc: Seiji Nishikawa, Keith Busch, Jens Axboe, Christoph Hellwig, Sagi Grimberg, Chris Leech, linux-nvme, linux-kernel, stable Thank you for the review. On 10/21/26 12:10 PM, Hannes Reinecke wrote: > On 10/14/26 8:45 AM, Seiji Nishikawa wrote: >> We hit this on a 6 CPU VM running a database on nvme-tcp storage. In >> the vmcore, 2 tasks held send_mutex of 2 queues and were runnable in >> lock_sock_nested() for 103 seconds, while the kworkers for those queues >> kept running io_work on their CPUs. All database tasks were in one >> cgroup, which got only about 0.3% of the CPU against the nice -20 >> kworker. Requests timed out, and blk_mq_timeout_work() waited in >> synchronize_srcu() for the tasks still in .queue_rq. >> > How very interesting. I don't think you could craft a blktest for this? > I do not have a reliable reproducer in my lab. Reproducing the hang requires a particular sequence of scheduling events. The task holding send_mutex is preempted inside lock_sock_nested(), a higher priority (nice -20) kworker looping in io_work, waiting for send_mutex to be released, takes that CPU, and then starves the task holding send_mutex of CPU time indefinitely. The kworker keeps looping because mutex_trylock() keeps failing, since the preempted task still holds send_mutex, and the pending check (f42d4796ee10) keeps the kworker running instead of letting it return. It happened on a 6 CPU VM with a database under heavy load. So far I have only seen it on the customer system, but there it reproduced every time. >> Only do the pending check if io_work could take send_mutex. If it could >> not, the task holding send_mutex sends what it can in >> nvme_tcp_send_all(), and queues io_work again after it releases the >> mutex if requests are still pending, so no request is left behind. >> > > The direct send path again. I really wonder if we need it at all, or > whether we cannot simply delete it. > > Can you try if you see a regression with this patch? > > diff --git a/drivers/nvme/host/tcp.c b/drivers/nvme/host/tcp.c > index ba5c7b3e2a7c..2c59b550e13f 100644 > --- a/drivers/nvme/host/tcp.c > +++ b/drivers/nvme/host/tcp.c > @@ -405,17 +405,6 @@ static inline void nvme_tcp_queue_request(struct > nvme_tcp_request *req, > empty = llist_add(&req->lentry, &queue->req_list) && > list_empty(&queue->send_list) && !queue->request; > > - /* > - * if we're the first on the send_list and we can try to send > - * directly, otherwise queue io_work. Also, only do that if we > - * are on the same cpu, so we don't introduce contention. > - */ > - if (queue->io_cpu == raw_smp_processor_id() && > - empty && mutex_trylock(&queue->send_mutex)) { > - nvme_tcp_send_all(queue); > - mutex_unlock(&queue->send_mutex); > - } > - > if (last && nvme_tcp_queue_has_pending(queue)) > queue_work_on(queue->io_cpu, nvme_tcp_wq, &queue->io_work); > } > I cannot try it where the hang reproduced. That is a customer production system, and I have no reproducer outside of it. On the kernel with the regression, that system hung within 12 to about 50 minutes after boot, 5 times in 2 days, under its normal database load. It has been running a kernel with my patch since October 15 under the same load, and the hang has not come back. With your patch this hang cannot happen, since io_work becomes the only sender and the trylock in io_work never fails. What I could do is a sanity check. On a 4 CPU VM with an nvmet tcp loopback target I ran # NVMET_TRTYPES=tcp ./check nvme on 3 kernels: v7.3 (base), v7.3 with my patch (seiji), and v7.3 with your patch (hannes). base passed=49 failed=0 not_run=12 seiji passed=49 failed=0 not_run=12 hannes passed=49 failed=0 not_run=12 per-test results identical The 12 not run are the same on all 3 kernels, skipped for reasons unrelated to the patches: 6 nvme_trtype=tcp is not supported in this test 1 command tlshd is not available 1 driver scsi_debug is not available 1 kernel option KASAN has not been enabled 1 kernel option NVME_TARGET_DEBUGFS has not been enabled 1 libnvme version is older than 1.11.0 1 Remote target required but NVME_TARGET_CONTROL is not set This is only a functional check. It says nothing about latency. To apply your patch on v7.3 I had to drop the !nvme_tcp_queue_tls() line as well, since 08acb54b063a ("nvme-tcp: defer TLS inline send to io_work") is in v7.3. That commit already skips the direct send for TLS queues, so the io_work only path is already in use there. Two small things in the patch. empty is set but no longer used, which warns with W=1, and nvme_tcp_send_all() loses its only caller. I dropped both in the version I built. I agree the direct send path is the source of this family of bugs, 5c11f7d9f843, a0fdd1418007, 70f437fb4395 and this one, and I am fine with removing it. But I think the two should be kept apart. This regression is in v6.16 and later, so it is in every current stable tree and in the distro kernels based on them, and people are hitting it in production now. What stable and the distros need is a small fix that only changes behavior for those who hit the problem, and that is what this patch is. Removing the direct send path changes performance for everyone using nvme-tcp and requires a performance review before it is introduced. Also, it belongs in the next merge window rather than in stable. So I would prefer to take this minimal fix now for v7.4 and stable, and do the removal as a separate patch for-next. If you agree, I can write and send that patch with your Suggested-by. For the performance side I can only test on VMs, so latency numbers from a real nvme-tcp setup would be needed from someone else. If later performance measurements reveal that the direct send is unnecessary, the check added here goes away with it, which is fine. Thanks, Seiji ^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-10-23 12:24 UTC | newest] Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2026-10-14 6:45 [PATCH] nvme-tcp: fix io_work priority inversion when send_mutex is held Seiji Nishikawa 2026-10-21 10:04 ` Hannes Reinecke 2026-10-23 12:23 ` Seiji Nishikawa
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®