* [PATCH v4 0/2] nvme-tcp: allow setting per-queue io_cpu through sysfs
@ 2026-09-27 7:09 Saravanan D
2026-09-27 7:09 ` [PATCH v4 1/2] nvme: add per-queue sysfs directories Saravanan D
` (2 more replies)
0 siblings, 3 replies; 7+ messages in thread
From: Saravanan D @ 2026-09-27 7:09 UTC (permalink / raw)
To: linux-nvme
Cc: kbusch, hch, sagi, axboe, nilay, dwagner, linux-kernel,
iyamahata, kiyer, ganbalagane, sj, Saravanan D
On hosts that partition their CPUs after connect time, the connect
time io_cpu selection can land a queue's socket work on CPUs owned by
a different workload. This series lets a control plane set each
queue's io_cpu directly through sysfs.
The first patch adds a small common queue info that a transport embeds
in its queue structure, backing a transport neutral
/sys/class/nvme/nvmeX/queues/<qid>/
directory under the controller device. The second patch has nvme-tcp
expose io_cpu and managed attributes there.
Changes since v3 [1]:
- Split into two patches and introduced the common queue info that
Sagi proposed [2], named nvme_queue_info since the PCI driver
already uses nvme_queue, as Nilay pointed out.
- Renamed the tcp_queues directory to queues so the ABI does not bake
in the transport, as suggested by Nilay [3].
- Added the read-only managed attribute, 1 while io_cpu is driver
managed and 0 once the user assigned it, as suggested by Nilay.
- io_cpu now accepts -1 or the string "unbound" to leave the socket
work unbound, instead of restoring the connect time selection, as
suggested by Sagi.
- A user assignment is tracked in the common queue info flags and
still persists across reconnects, following Sagi's earlier guidance
that io_cpu should not change on reconnect.
- The queue directories register once per controller lifetime and
persist across reconnects, and the io_cpu accounting moved from the
queue lock to a controller level lock since a write can now arrive
while a queue is torn down.
- The io_cpu accounting now also covers a pin to a queue whose connect
time pick did not bind a cpu, and the queue teardown clears any
accounting left by a write racing the teardown.
[1] https://lore.kernel.org/linux-nvme/20260909211432.6741-1-saravanand@crusoe.ai/
[2] https://lore.kernel.org/linux-nvme/13c160d0-0657-47ce-9a16-58d348267ab0@grimberg.me/
[3] https://lore.kernel.org/linux-nvme/8dfee10f-89f3-463b-8d16-587873c3394c@linux.ibm.com/
Saravanan D (2):
nvme: add per-queue sysfs directories
nvme-tcp: allow setting per-queue io_cpu through sysfs
Documentation/ABI/stable/sysfs-nvme | 21 ++++
drivers/nvme/host/core.c | 2 +
drivers/nvme/host/nvme.h | 23 +++++
drivers/nvme/host/sysfs.c | 42 ++++++++
drivers/nvme/host/tcp.c | 153 +++++++++++++++++++++++++++-
5 files changed, 240 insertions(+), 1 deletion(-)
base-commit: 2ee54f01f07c0307deaf90ca8691a4643ae0357b
--
2.55.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH v4 1/2] nvme: add per-queue sysfs directories
2026-09-27 7:09 [PATCH v4 0/2] nvme-tcp: allow setting per-queue io_cpu through sysfs Saravanan D
@ 2026-09-27 7:09 ` Saravanan D
2026-10-08 9:59 ` Nilay Shroff
2026-09-27 7:09 ` [PATCH v4 2/2] nvme-tcp: allow setting per-queue io_cpu through sysfs Saravanan D
2026-10-08 5:39 ` [PATCH v4 0/2] " Saravanan D
2 siblings, 1 reply; 7+ messages in thread
From: Saravanan D @ 2026-09-27 7:09 UTC (permalink / raw)
To: linux-nvme
Cc: kbusch, hch, sagi, axboe, nilay, dwagner, linux-kernel,
iyamahata, kiyer, ganbalagane, sj, Saravanan D
Transports have per I/O queue state worth exposing, the nvme-tcp
io_cpu to begin with, but the core has no representation of an
individual queue and no place under the controller device to hang
per queue attributes.
Add a small nvme_queue_info that a transport embeds in its queue
structure, and helpers that register it as
/sys/class/nvme/nvmeX/queues/<qid>/
with a transport provided ktype. The queues directory is created on
the first registration and released with the controller device.
Registration is expected once per controller lifetime, so the
directories persist while the transport's queues cycle across
reconnects.
Suggested-by: Sagi Grimberg <sagi@grimberg.me>
Link: https://lore.kernel.org/linux-nvme/13c160d0-0657-47ce-9a16-58d348267ab0@grimberg.me/
Assisted-by: Claude:claude-opus-4-8 [Claude Code]
Signed-off-by: Saravanan D <saravanand@crusoe.ai>
---
drivers/nvme/host/core.c | 2 ++
drivers/nvme/host/nvme.h | 23 +++++++++++++++++++++
drivers/nvme/host/sysfs.c | 42 +++++++++++++++++++++++++++++++++++++++
3 files changed, 67 insertions(+)
diff --git a/drivers/nvme/host/core.c b/drivers/nvme/host/core.c
index beea23d04a70..211d05a1cc48 100644
--- a/drivers/nvme/host/core.c
+++ b/drivers/nvme/host/core.c
@@ -5197,6 +5197,8 @@ static void nvme_free_ctrl(struct device *dev)
ctrl->ops->free_ctrl(ctrl);
+ kobject_put(ctrl->queues_kobj);
+
if (subsys)
nvme_put_subsystem(subsys);
}
diff --git a/drivers/nvme/host/nvme.h b/drivers/nvme/host/nvme.h
index bac25b287d25..20410f0d6118 100644
--- a/drivers/nvme/host/nvme.h
+++ b/drivers/nvme/host/nvme.h
@@ -339,6 +339,28 @@ enum nvme_ctrl_flags {
NVME_CTRL_FROZEN = 6,
};
+enum nvme_queue_info_flags {
+ NVME_QUEUE_INFO_REGISTERED = 0,
+ NVME_QUEUE_INFO_IO_CPU_USER = 1,
+};
+
+/*
+ * Common per-queue state, embedded in the transport's queue structure and
+ * backing the /sys/class/nvme/nvmeX/queues/<qid>/ directory.
+ */
+struct nvme_queue_info {
+ struct kobject kobj;
+ unsigned int qid;
+ unsigned long flags;
+};
+
+struct nvme_ctrl;
+
+int nvme_register_queue_info(struct nvme_ctrl *ctrl,
+ struct nvme_queue_info *qinfo, unsigned int qid,
+ const struct kobj_type *ktype);
+void nvme_unregister_queue_info(struct nvme_queue_info *qinfo);
+
struct nvme_ctrl {
bool comp_seen;
bool identified;
@@ -360,6 +382,7 @@ struct nvme_ctrl {
struct srcu_struct srcu;
struct device ctrl_device;
struct device *device; /* char device */
+ struct kobject *queues_kobj;
#ifdef CONFIG_NVME_HWMON
struct device *hwmon_device;
#endif
diff --git a/drivers/nvme/host/sysfs.c b/drivers/nvme/host/sysfs.c
index 4ac3ea3a86fd..a2780445c33e 100644
--- a/drivers/nvme/host/sysfs.c
+++ b/drivers/nvme/host/sysfs.c
@@ -1313,3 +1313,45 @@ const struct attribute_group *nvme_subsys_attrs_groups[] = {
&nvme_subsys_attrs_group,
NULL,
};
+
+/*
+ * Per-queue sysfs directories under /sys/class/nvme/nvmeX/queues/. The
+ * transport backing the controller registers a directory for each of its
+ * queues and provides the attributes through the ktype. Registration is
+ * expected once per controller lifetime, so the directories persist while
+ * the transport's queues cycle across reconnects. The queues directory
+ * itself is released with the controller device.
+ */
+int nvme_register_queue_info(struct nvme_ctrl *ctrl,
+ struct nvme_queue_info *qinfo, unsigned int qid,
+ const struct kobj_type *ktype)
+{
+ int ret;
+
+ if (!ctrl->queues_kobj) {
+ ctrl->queues_kobj = kobject_create_and_add("queues",
+ &ctrl->device->kobj);
+ if (!ctrl->queues_kobj)
+ return -ENOMEM;
+ }
+
+ qinfo->qid = qid;
+ ret = kobject_init_and_add(&qinfo->kobj, ktype, ctrl->queues_kobj,
+ "%u", qid);
+ if (ret) {
+ kobject_put(&qinfo->kobj);
+ return ret;
+ }
+
+ set_bit(NVME_QUEUE_INFO_REGISTERED, &qinfo->flags);
+ return 0;
+}
+EXPORT_SYMBOL_GPL(nvme_register_queue_info);
+
+void nvme_unregister_queue_info(struct nvme_queue_info *qinfo)
+{
+ if (!test_and_clear_bit(NVME_QUEUE_INFO_REGISTERED, &qinfo->flags))
+ return;
+ kobject_put(&qinfo->kobj);
+}
+EXPORT_SYMBOL_GPL(nvme_unregister_queue_info);
--
2.55.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH v4 2/2] nvme-tcp: allow setting per-queue io_cpu through sysfs
2026-09-27 7:09 [PATCH v4 0/2] nvme-tcp: allow setting per-queue io_cpu through sysfs Saravanan D
2026-09-27 7:09 ` [PATCH v4 1/2] nvme: add per-queue sysfs directories Saravanan D
@ 2026-09-27 7:09 ` Saravanan D
2026-10-08 9:45 ` Nilay Shroff
2026-10-08 5:39 ` [PATCH v4 0/2] " Saravanan D
2 siblings, 1 reply; 7+ messages in thread
From: Saravanan D @ 2026-09-27 7:09 UTC (permalink / raw)
To: linux-nvme
Cc: kbusch, hch, sagi, axboe, nilay, dwagner, linux-kernel,
iyamahata, kiyer, ganbalagane, sj, Saravanan D
nvme_tcp_set_queue_io_cpu() picks each queue's io_cpu at connect time
as the least loaded CPU in the queue's blk-mq map group, and all socket
work runs there for the connection's lifetime. This decision falls
short when the host partitions its CPUs after connect time. On a 384
cpu multi tenant host with 128 queue controllers, blk-mq folds three
CPUs into every map group, some groups straddle two tenants' cpusets,
and 9% of nvme_tcp_io_work executions ran outside the submitting VM's
cpuset, seen by the neighbor as steal time.
Embed the nvme_queue_info in the tcp queue and expose the io_cpu
through the controller's queues directory
/sys/class/nvme/nvmeX/queues/<qid>/io_cpu
so a control plane that owns CPU placement can set it directly instead
of relying on the driver's heuristic. The attribute accepts a cpu
number, -1 or "unbound", where -1 and "unbound" leave the socket work
unbound, running each invocation on the cpu that queued it. A user
assignment is marked in the queue info flags, persists across
reconnects and is never re-picked by the driver. The read-only
managed attribute reports 1 while io_cpu is driver managed and 0 once
the user assigned it. The accounting of nvme_tcp_cpu_queues applies
regardless of who set the io_cpu.
The queue directories register on the first connect and persist while
the queues cycle across reconnects, so a write can arrive while a
queue is torn down. The io_cpu changes and their accounting therefore
serialize under a controller level lock rather than the queue lock,
which is destroyed with the queue, and the controller teardown waits
for the last kobject release before the queue array is freed.
Suggested-by: Sagi Grimberg <sagi@grimberg.me>
Link: https://lore.kernel.org/linux-nvme/13c160d0-0657-47ce-9a16-58d348267ab0@grimberg.me/
Assisted-by: Claude:claude-opus-4-8 [Claude Code]
Signed-off-by: Saravanan D <saravanand@crusoe.ai>
---
Documentation/ABI/stable/sysfs-nvme | 21 ++++
drivers/nvme/host/tcp.c | 153 +++++++++++++++++++++++++++-
2 files changed, 173 insertions(+), 1 deletion(-)
diff --git a/Documentation/ABI/stable/sysfs-nvme b/Documentation/ABI/stable/sysfs-nvme
index a0bb88ca1694..4d0c7688e8f7 100644
--- a/Documentation/ABI/stable/sysfs-nvme
+++ b/Documentation/ABI/stable/sysfs-nvme
@@ -472,3 +472,24 @@ Contact: Hannes Reinecke <hare@suse.de>
Description:
Shows the subsystem type. Possible values: "discovery",
"nvm", "reserved".
+
+What: /sys/class/nvme/nvmeX/queues/<qid>/io_cpu
+What: /sys/class/nvme/nvmeX/queues/<qid>/managed
+Date: September 2026
+KernelVersion: 7.4
+Contact: Saravanan D <saravanand@crusoe.ai>
+Description:
+ Per I/O queue directories, populated by the transport
+ backing the controller and persistent across controller
+ reconnects. NVMe over TCP exposes:
+
+ io_cpu: (RW) The CPU that runs the socket work for I/O
+ queue <qid>, selected by the driver at connect time.
+ Writing a CPU number overrides the selection and persists
+ across reconnects. Writing -1 or "unbound" leaves the
+ socket work unbound, running each invocation on the CPU
+ that queued it. Reads return the current CPU, or -1 when
+ the queue is unbound.
+
+ managed: (RO) Shows who owns the CPU placement. 1 while
+ io_cpu is driver managed, 0 once the user assigned it.
diff --git a/drivers/nvme/host/tcp.c b/drivers/nvme/host/tcp.c
index 921934028e0b..567c839e92a4 100644
--- a/drivers/nvme/host/tcp.c
+++ b/drivers/nvme/host/tcp.c
@@ -141,6 +141,8 @@ struct nvme_tcp_queue {
int tls_err;
struct page_frag_cache pf_cache;
+ struct nvme_queue_info q_info;
+
void (*state_change)(struct sock *);
void (*data_ready)(struct sock *);
void (*write_space)(struct sock *);
@@ -171,6 +173,11 @@ struct nvme_tcp_ctrl {
struct delayed_work connect_work;
struct nvme_tcp_request async_req;
u32 io_queues[HCTX_MAX_TYPES];
+ /* serializes io_cpu changes and their nvme_tcp_cpu_queues accounting */
+ struct mutex io_cpu_lock;
+ unsigned int nr_queue_infos;
+ atomic_t qinfo_refs;
+ struct completion qinfo_release;
};
static struct workqueue_struct *nvme_tcp_wq;
@@ -1497,6 +1504,12 @@ static void nvme_tcp_free_queue(struct nvme_ctrl *nctrl, int qid)
if (!test_and_clear_bit(NVME_TCP_Q_ALLOCATED, &queue->flags))
return;
+ /* settle accounting a write racing the queue stop may have left */
+ mutex_lock(&ctrl->io_cpu_lock);
+ if (test_and_clear_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags))
+ atomic_dec(&nvme_tcp_cpu_queues[queue->io_cpu]);
+ mutex_unlock(&ctrl->io_cpu_lock);
+
page_frag_cache_drain(&queue->pf_cache);
/**
@@ -1716,9 +1729,19 @@ static void nvme_tcp_set_queue_io_cpu(struct nvme_tcp_queue *queue)
unsigned int *mq_map = NULL;
int cpu, min_queues = INT_MAX, io_cpu;
+ lockdep_assert_held(&ctrl->io_cpu_lock);
+
if (wq_unbound)
goto out;
+ /* A user assigned io_cpu is kept across reconnects */
+ if (test_bit(NVME_QUEUE_INFO_IO_CPU_USER, &queue->q_info.flags)) {
+ if (queue->io_cpu != WORK_CPU_UNBOUND &&
+ !test_and_set_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags))
+ atomic_inc(&nvme_tcp_cpu_queues[queue->io_cpu]);
+ goto out;
+ }
+
if (nvme_tcp_default_queue(queue))
mq_map = set->map[HCTX_TYPE_DEFAULT].mq_map;
else if (nvme_tcp_read_queue(queue))
@@ -1836,6 +1859,114 @@ static int nvme_tcp_start_tls(struct nvme_ctrl *nctrl,
return ret;
}
+static struct nvme_tcp_queue *nvme_tcp_kobj_to_queue(struct kobject *kobj)
+{
+ struct nvme_queue_info *qinfo =
+ container_of(kobj, struct nvme_queue_info, kobj);
+
+ return container_of(qinfo, struct nvme_tcp_queue, q_info);
+}
+
+static ssize_t io_cpu_show(struct kobject *kobj, struct kobj_attribute *attr,
+ char *buf)
+{
+ struct nvme_tcp_queue *queue = nvme_tcp_kobj_to_queue(kobj);
+ int io_cpu = READ_ONCE(queue->io_cpu);
+
+ return sysfs_emit(buf, "%d\n",
+ io_cpu == WORK_CPU_UNBOUND ? -1 : io_cpu);
+}
+
+static ssize_t io_cpu_store(struct kobject *kobj, struct kobj_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct nvme_tcp_queue *queue = nvme_tcp_kobj_to_queue(kobj);
+ struct nvme_tcp_ctrl *ctrl = queue->ctrl;
+ int cpu, old;
+ int ret;
+
+ if (sysfs_streq(buf, "unbound")) {
+ cpu = WORK_CPU_UNBOUND;
+ } else {
+ ret = kstrtoint(buf, 0, &cpu);
+ if (ret)
+ return ret;
+ if (cpu == -1)
+ cpu = WORK_CPU_UNBOUND;
+ else if ((unsigned int)cpu >= nr_cpu_ids || !cpu_online(cpu))
+ return -EINVAL;
+ }
+
+ mutex_lock(&ctrl->io_cpu_lock);
+ old = xchg(&queue->io_cpu, cpu);
+ set_bit(NVME_QUEUE_INFO_IO_CPU_USER, &queue->q_info.flags);
+ if (test_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags)) {
+ atomic_dec(&nvme_tcp_cpu_queues[old]);
+ if (cpu != WORK_CPU_UNBOUND)
+ atomic_inc(&nvme_tcp_cpu_queues[cpu]);
+ else
+ clear_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags);
+ } else if (cpu != WORK_CPU_UNBOUND &&
+ test_bit(NVME_TCP_Q_ALLOCATED, &queue->flags)) {
+ /* account a pin to a connected queue the pick left unbound */
+ atomic_inc(&nvme_tcp_cpu_queues[cpu]);
+ set_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags);
+ }
+ mutex_unlock(&ctrl->io_cpu_lock);
+
+ return count;
+}
+
+static ssize_t managed_show(struct kobject *kobj, struct kobj_attribute *attr,
+ char *buf)
+{
+ struct nvme_tcp_queue *queue = nvme_tcp_kobj_to_queue(kobj);
+
+ return sysfs_emit(buf, "%d\n",
+ !test_bit(NVME_QUEUE_INFO_IO_CPU_USER, &queue->q_info.flags));
+}
+
+static struct kobj_attribute nvme_tcp_io_cpu_attr =
+ __ATTR(io_cpu, 0644, io_cpu_show, io_cpu_store);
+static struct kobj_attribute nvme_tcp_managed_attr =
+ __ATTR_RO(managed);
+
+static struct attribute *nvme_tcp_queue_attrs[] = {
+ &nvme_tcp_io_cpu_attr.attr,
+ &nvme_tcp_managed_attr.attr,
+ NULL,
+};
+ATTRIBUTE_GROUPS(nvme_tcp_queue);
+
+static void nvme_tcp_queue_info_release(struct kobject *kobj)
+{
+ struct nvme_tcp_queue *queue = nvme_tcp_kobj_to_queue(kobj);
+ struct nvme_tcp_ctrl *ctrl = queue->ctrl;
+
+ if (atomic_dec_and_test(&ctrl->qinfo_refs))
+ complete(&ctrl->qinfo_release);
+}
+
+static const struct kobj_type nvme_tcp_queue_ktype = {
+ .sysfs_ops = &kobj_sysfs_ops,
+ .release = nvme_tcp_queue_info_release,
+ .default_groups = nvme_tcp_queue_groups,
+};
+
+static void nvme_tcp_register_queue_sysfs(struct nvme_tcp_queue *queue)
+{
+ struct nvme_tcp_ctrl *ctrl = queue->ctrl;
+
+ if (test_bit(NVME_QUEUE_INFO_REGISTERED, &queue->q_info.flags))
+ return;
+
+ /* a failed registration drops the reference through the release */
+ atomic_inc(&ctrl->qinfo_refs);
+ nvme_register_queue_info(&ctrl->ctrl, &queue->q_info,
+ nvme_tcp_queue_id(queue),
+ &nvme_tcp_queue_ktype);
+}
+
static int nvme_tcp_alloc_queue(struct nvme_ctrl *nctrl, int qid,
key_serial_t pskid)
{
@@ -1906,7 +2037,8 @@ static int nvme_tcp_alloc_queue(struct nvme_ctrl *nctrl, int qid,
queue->sock->sk->sk_allocation = GFP_ATOMIC;
queue->sock->sk->sk_use_task_frag = false;
- queue->io_cpu = WORK_CPU_UNBOUND;
+ if (!test_bit(NVME_QUEUE_INFO_IO_CPU_USER, &queue->q_info.flags))
+ queue->io_cpu = WORK_CPU_UNBOUND;
queue->request = NULL;
queue->data_remaining = 0;
queue->ddgst_remaining = 0;
@@ -1974,6 +2106,9 @@ static int nvme_tcp_alloc_queue(struct nvme_ctrl *nctrl, int qid,
set_bit(NVME_TCP_Q_ALLOCATED, &queue->flags);
+ if (qid)
+ nvme_tcp_register_queue_sysfs(queue);
+
return 0;
err_init_connect:
@@ -2022,8 +2157,10 @@ static void nvme_tcp_stop_queue_nowait(struct nvme_ctrl *nctrl, int qid)
if (!test_bit(NVME_TCP_Q_ALLOCATED, &queue->flags))
return;
+ mutex_lock(&ctrl->io_cpu_lock);
if (test_and_clear_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags))
atomic_dec(&nvme_tcp_cpu_queues[queue->io_cpu]);
+ mutex_unlock(&ctrl->io_cpu_lock);
mutex_lock(&queue->queue_lock);
if (test_and_clear_bit(NVME_TCP_Q_LIVE, &queue->flags))
@@ -2085,7 +2222,9 @@ static int nvme_tcp_start_queue(struct nvme_ctrl *nctrl, int idx)
nvme_tcp_setup_sock_ops(queue);
if (idx) {
+ mutex_lock(&ctrl->io_cpu_lock);
nvme_tcp_set_queue_io_cpu(queue);
+ mutex_unlock(&ctrl->io_cpu_lock);
ret = nvmf_connect_io_queue(nctrl, idx);
} else
ret = nvmf_connect_admin_queue(nctrl);
@@ -2650,6 +2789,14 @@ static void nvme_tcp_free_ctrl(struct nvme_ctrl *nctrl)
nvmf_free_options(nctrl->opts);
free_ctrl:
+ if (ctrl->queues) {
+ unsigned int qid;
+
+ for (qid = 1; qid < ctrl->nr_queue_infos; qid++)
+ nvme_unregister_queue_info(&ctrl->queues[qid].q_info);
+ if (!atomic_dec_and_test(&ctrl->qinfo_refs))
+ wait_for_completion(&ctrl->qinfo_release);
+ }
kfree(ctrl->queues);
kfree(ctrl);
}
@@ -3047,6 +3194,10 @@ static struct nvme_tcp_ctrl *nvme_tcp_alloc_ctrl(struct device *dev,
ret = -ENOMEM;
goto out_free_ctrl;
}
+ ctrl->nr_queue_infos = ctrl->ctrl.queue_count;
+ mutex_init(&ctrl->io_cpu_lock);
+ atomic_set(&ctrl->qinfo_refs, 1);
+ init_completion(&ctrl->qinfo_release);
ret = nvme_init_ctrl(&ctrl->ctrl, dev, &nvme_tcp_ctrl_ops, 0);
if (ret)
--
2.55.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH v4 0/2] nvme-tcp: allow setting per-queue io_cpu through sysfs
2026-09-27 7:09 [PATCH v4 0/2] nvme-tcp: allow setting per-queue io_cpu through sysfs Saravanan D
2026-09-27 7:09 ` [PATCH v4 1/2] nvme: add per-queue sysfs directories Saravanan D
2026-09-27 7:09 ` [PATCH v4 2/2] nvme-tcp: allow setting per-queue io_cpu through sysfs Saravanan D
@ 2026-10-08 5:39 ` Saravanan D
2 siblings, 0 replies; 7+ messages in thread
From: Saravanan D @ 2026-10-08 5:39 UTC (permalink / raw)
To: linux-nvme
Cc: Saravanan D, kbusch, hch, sagi, axboe, nilay, dwagner,
linux-kernel, iyamahata, kiyer, ganbalagane, sj
The Sashiko review of this series [1] found lifecycle bugs in the
sysfs registration, including a use after free of ctrl->queues_kobj
after ops->free_ctrl() and a reference cycle that leaks the controller
on every disconnect. I will address all of its comments in v5, so
please hold off on applying v4.
[1] https://sashiko.dev/#/patchset/20260927070925.47209-1-saravanand%40crusoe.ai
Thanks,
Saravanan D.
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH v4 2/2] nvme-tcp: allow setting per-queue io_cpu through sysfs
2026-09-27 7:09 ` [PATCH v4 2/2] nvme-tcp: allow setting per-queue io_cpu through sysfs Saravanan D
@ 2026-10-08 9:45 ` Nilay Shroff
2026-10-09 2:01 ` Saravanan D
0 siblings, 1 reply; 7+ messages in thread
From: Nilay Shroff @ 2026-10-08 9:45 UTC (permalink / raw)
To: Saravanan D, linux-nvme
Cc: kbusch, hch, sagi, axboe, dwagner, linux-kernel, iyamahata,
kiyer, ganbalagane, sj
On 9/27/26 12:39 PM, Saravanan D wrote:
> nvme_tcp_set_queue_io_cpu() picks each queue's io_cpu at connect time
> as the least loaded CPU in the queue's blk-mq map group, and all socket
> work runs there for the connection's lifetime. This decision falls
> short when the host partitions its CPUs after connect time. On a 384
> cpu multi tenant host with 128 queue controllers, blk-mq folds three
> CPUs into every map group, some groups straddle two tenants' cpusets,
> and 9% of nvme_tcp_io_work executions ran outside the submitting VM's
> cpuset, seen by the neighbor as steal time.
>
> Embed the nvme_queue_info in the tcp queue and expose the io_cpu
> through the controller's queues directory
>
> /sys/class/nvme/nvmeX/queues/<qid>/io_cpu
>
> so a control plane that owns CPU placement can set it directly instead
> of relying on the driver's heuristic. The attribute accepts a cpu
> number, -1 or "unbound", where -1 and "unbound" leave the socket work
> unbound, running each invocation on the cpu that queued it. A user
> assignment is marked in the queue info flags, persists across
> reconnects and is never re-picked by the driver. The read-only
> managed attribute reports 1 while io_cpu is driver managed and 0 once
> the user assigned it. The accounting of nvme_tcp_cpu_queues applies
> regardless of who set the io_cpu.
>
> The queue directories register on the first connect and persist while
> the queues cycle across reconnects, so a write can arrive while a
> queue is torn down. The io_cpu changes and their accounting therefore
> serialize under a controller level lock rather than the queue lock,
> which is destroyed with the queue, and the controller teardown waits
> for the last kobject release before the queue array is freed.
>
I understand based on your requirement you may want to persist io_cpu
changes (when managed by user) during controller reconnect. However
in case num of queues changes after reconnect (assume it's 32 at first
connect but after controller reconnects it's reduced to 16) keeping
those 32 queues entries under queues/<qid>/ looks bit odd and not
correct. I'd expect queues entries under queue/<qid>/ to be also
adjusted based on the controller queue count.
[...]
> diff --git a/drivers/nvme/host/tcp.c b/drivers/nvme/host/tcp.c
> index 921934028e0b..567c839e92a4 100644
> --- a/drivers/nvme/host/tcp.c
> +++ b/drivers/nvme/host/tcp.c
> @@ -141,6 +141,8 @@ struct nvme_tcp_queue {
> int tls_err;
> struct page_frag_cache pf_cache;
>
> + struct nvme_queue_info q_info;
> +
> void (*state_change)(struct sock *);
> void (*data_ready)(struct sock *);
> void (*write_space)(struct sock *);
> @@ -171,6 +173,11 @@ struct nvme_tcp_ctrl {
> struct delayed_work connect_work;
> struct nvme_tcp_request async_req;
> u32 io_queues[HCTX_MAX_TYPES];
> + /* serializes io_cpu changes and their nvme_tcp_cpu_queues accounting */
> + struct mutex io_cpu_lock;
The io_cpu_lock protect updates to queue->io_cpu and as we now support clang
context annotations for NVMe host driver, I'd suggest you annotate the
queue->io_cpu with __guarded_by(...) so that clang context analyzer can
validate each access to queue->io_cpu. Furthermore, I see that queue->io_cpu
could be concurrently accessed from both control path and I/O hotpath. So how
would we protect it while it's being accessed from I/O path and concurrently
updated from sysfs path?
> + unsigned int nr_queue_infos;
> + atomic_t qinfo_refs;
> + struct completion qinfo_release;
> };
I think the qinfo_refs and qinfo_release are added for tracking the
lifecycle of nvme_queue_info object. But do we really need to do
that ?
When a nvme_queue_info->kobj is initialized/added, its parent kobject
is referenced by the kobject infrastructure. Therefore, as long as a
queue-info kobject exists, its parent queues_kobj remains alive, which
in turn keeps ctrl->device->kobj alive. This should already prevent the
controller object from being released while a queue-info kobject still
exists. Could we therefore avoid maintaining a second qinfo_refs
reference count and completion? It seems that entire kobject parent
chain already provides the lifetime dependency that qinfo_refs is
trying to enforce.
[...]
> +static ssize_t io_cpu_store(struct kobject *kobj, struct kobj_attribute *attr,
> + const char *buf, size_t count)
> +{
> + struct nvme_tcp_queue *queue = nvme_tcp_kobj_to_queue(kobj);
> + struct nvme_tcp_ctrl *ctrl = queue->ctrl;
> + int cpu, old;
> + int ret;
> +
> + if (sysfs_streq(buf, "unbound")) {
> + cpu = WORK_CPU_UNBOUND;
> + } else {
> + ret = kstrtoint(buf, 0, &cpu);
> + if (ret)
> + return ret;
> + if (cpu == -1)
> + cpu = WORK_CPU_UNBOUND;
> + else if ((unsigned int)cpu >= nr_cpu_ids || !cpu_online(cpu))
> + return -EINVAL;
> + }
> +
> + mutex_lock(&ctrl->io_cpu_lock);
> + old = xchg(&queue->io_cpu, cpu);
> + set_bit(NVME_QUEUE_INFO_IO_CPU_USER, &queue->q_info.flags);
> + if (test_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags)) {
> + atomic_dec(&nvme_tcp_cpu_queues[old]);
> + if (cpu != WORK_CPU_UNBOUND)
> + atomic_inc(&nvme_tcp_cpu_queues[cpu]);
> + else
> + clear_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags);
> + } else if (cpu != WORK_CPU_UNBOUND &&
> + test_bit(NVME_TCP_Q_ALLOCATED, &queue->flags)) {
> + /* account a pin to a connected queue the pick left unbound */
> + atomic_inc(&nvme_tcp_cpu_queues[cpu]);
> + set_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags);
> + }
> + mutex_unlock(&ctrl->io_cpu_lock);
> +
This look overly complicated. In a simple scheme can't we get rid off
NVME_QUEUE_INFO_IO_CPU_USER and instead use NVME_TCP_Q_IO_CPU_SET as an
indicator for differentiating between io_cpu is managed by user or driver?
For instance,
NVME_TCP_Q_IO_CPU_SET = 1
- driver has selected a specific io_cpu
NVME_TCP_Q_IO_CPU_SET = 0 && io_cpu != WORK_CPU_UNBOUND
- user has selected a specific io_cpu
NVME_TCP_Q_IO_CPU_SET = 0 && io_cpu == WORK_CPU_UNBOUND
- unbound; workqueue chooses the execution CPU and so it's driver managed
[...]
Other that what sashiko provided in the review feedback, above are my
few additional comments.
Thanks,
--Nilay
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH v4 1/2] nvme: add per-queue sysfs directories
2026-09-27 7:09 ` [PATCH v4 1/2] nvme: add per-queue sysfs directories Saravanan D
@ 2026-10-08 9:59 ` Nilay Shroff
0 siblings, 0 replies; 7+ messages in thread
From: Nilay Shroff @ 2026-10-08 9:59 UTC (permalink / raw)
To: Saravanan D, linux-nvme
Cc: kbusch, hch, sagi, axboe, dwagner, linux-kernel, iyamahata,
kiyer, ganbalagane, sj
> diff --git a/drivers/nvme/host/nvme.h b/drivers/nvme/host/nvme.h
> index bac25b287d25..20410f0d6118 100644
> --- a/drivers/nvme/host/nvme.h
> +++ b/drivers/nvme/host/nvme.h
> @@ -339,6 +339,28 @@ enum nvme_ctrl_flags {
> NVME_CTRL_FROZEN = 6,
> };
>
> +enum nvme_queue_info_flags {
> + NVME_QUEUE_INFO_REGISTERED = 0,
> + NVME_QUEUE_INFO_IO_CPU_USER = 1,
> +};
NVME_QUEUE_INFO_IO_CPU_USER appears to be a transport-specific flag,
so I don't think it belongs in the common NVMe core code. It would
be better to keep this state in the transport-specific queue structure.
[...]
> +/*
> + * Per-queue sysfs directories under /sys/class/nvme/nvmeX/queues/. The
> + * transport backing the controller registers a directory for each of its
> + * queues and provides the attributes through the ktype. Registration is
> + * expected once per controller lifetime, so the directories persist while
> + * the transport's queues cycle across reconnects. The queues directory
> + * itself is released with the controller device.
> + */
> +int nvme_register_queue_info(struct nvme_ctrl *ctrl,
> + struct nvme_queue_info *qinfo, unsigned int qid,
> + const struct kobj_type *ktype)
> +{
> + int ret;
> +
> + if (!ctrl->queues_kobj) {
> + ctrl->queues_kobj = kobject_create_and_add("queues",
> + &ctrl->device->kobj);
> + if (!ctrl->queues_kobj)
> + return -ENOMEM;
> + }
> +
If this function can be invoked concurrently, how is ctrl->queues_kobj creation
serialized? For example, if two callers concurrently pass the !ctrl->queues_kobj
check, both could call kobject_create_and_add(), which is not what we want.
I understand that the current user of this API in patch 2/2 invokes it serially.
If serialization is an API requirement, could we document that in the function
comment? Alternatively, if this API is expected to be safe for concurrent callers,
the creation of queues_kobj should be serialized within this function.
In particular, I'd prefer the API contract to make the expected serialization
explicit rather than relying on the current caller to invoke it serially.
Thanks,
--Nilay
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH v4 2/2] nvme-tcp: allow setting per-queue io_cpu through sysfs
2026-10-08 9:45 ` Nilay Shroff
@ 2026-10-09 2:01 ` Saravanan D
0 siblings, 0 replies; 7+ messages in thread
From: Saravanan D @ 2026-10-09 2:01 UTC (permalink / raw)
To: Nilay Shroff
Cc: Saravanan D, linux-nvme, kbusch, hch, sagi, axboe, dwagner,
linux-kernel, iyamahata, kiyer, ganbalagane, sjpark
On 10/8/26 2:45 AM, Nilay Shroff wrote:
[...]
>> The queue directories register on the first connect and persist while
>> the queues cycle across reconnects, so a write can arrive while a
>> queue is torn down. The io_cpu changes and their accounting therefore
>> serialize under a controller level lock rather than the queue lock,
>> which is destroyed with the queue, and the controller teardown waits
>> for the last kobject release before the queue array is freed.
>>
> I understand based on your requirement you may want to persist io_cpu
> changes (when managed by user) during controller reconnect. However
> in case num of queues changes after reconnect (assume it's 32 at first
> connect but after controller reconnects it's reduced to 16) keeping
> those 32 queues entries under queues/<qid>/ looks bit odd and not
> correct. I'd expect queues entries under queue/<qid>/ to be also
> adjusted based on the controller queue count.
Agreed. In v5 the queue directory follows the queue's connected
lifetime. nvme_tcp_alloc_queue() adds it and nvme_tcp_free_queue()
removes it, so queues/<qid>/ matches the queue count after every
reconnect, as blk-mq does for mq/<n> when nr_hw_queues changes. A user
assigned io_cpu is kept in the nvme_tcp_queue, which persists across
reconnects, so it applies again when the queue comes back. A write
while the controller is reconnecting fails with ENOENT.
[...]
>> @@ -171,6 +173,11 @@ struct nvme_tcp_ctrl {
>> struct delayed_work connect_work;
>> struct nvme_tcp_request async_req;
>> u32 io_queues[HCTX_MAX_TYPES];
>> + /* serializes io_cpu changes and their nvme_tcp_cpu_queues accounting */
>> + struct mutex io_cpu_lock;
>
> The io_cpu_lock protect updates to queue->io_cpu and as we now support clang
> context annotations for NVMe host driver, I'd suggest you annotate the
> queue->io_cpu with __guarded_by(...) so that clang context analyzer can
> validate each access to queue->io_cpu. Furthermore, I see that queue->io_cpu
> could be concurrently accessed from both control path and I/O hotpath. So how
> would we protect it while it's being accessed from I/O path and concurrently
> updated from sysfs path?
The I/O path reads io_cpu without the lock by design, and a stale read
is benign. Each queue_work_on() call reads io_cpu once, so a call that
races with the store queues io_work on the old cpu one last time and
later calls use the new one. io_work is a single work item, so it
never runs on both cpus at once. The inline send in
nvme_tcp_queue_request() only compares io_cpu with the current cpu and
is serialized by send_mutex. v5 marks the lockless reads with
READ_ONCE() and the writes with WRITE_ONCE().
Annotating io_cpu with __guarded_by(&ctrl->io_cpu_lock) would need
context_unsafe() around each of those hot path reads, so I'd rather
document the lockless readers next to the field. Happy to add the
annotation if you prefer it.
>> + unsigned int nr_queue_infos;
>> + atomic_t qinfo_refs;
>> + struct completion qinfo_release;
>> };
>
> I think the qinfo_refs and qinfo_release are added for tracking the
> lifecycle of nvme_queue_info object. But do we really need to do
> that ?
> When a nvme_queue_info->kobj is initialized/added, its parent kobject
> is referenced by the kobject infrastructure. Therefore, as long as a
> queue-info kobject exists, its parent queues_kobj remains alive, which
> in turn keeps ctrl->device->kobj alive. This should already prevent the
> controller object from being released while a queue-info kobject still
> exists. Could we therefore avoid maintaining a second qinfo_refs
> reference count and completion? It seems that entire kobject parent
> chain already provides the lifetime dependency that qinfo_refs is
> trying to enforce.
Agreed. With the directory removed in nvme_tcp_free_queue(), no queue
directory outlives its queue, and the kobject release only frees its
own allocation, so the parent chain is enough. v5 drops qinfo_refs,
the completion and nr_queue_infos. Each connection allocates its own
kobject, so a reconnect never reuses one.
[...]
> This look overly complicated. In a simple scheme can't we get rid off
> NVME_QUEUE_INFO_IO_CPU_USER and instead use NVME_TCP_Q_IO_CPU_SET as an
> indicator for differentiating between io_cpu is managed by user or driver?
> For instance,
>
> NVME_TCP_Q_IO_CPU_SET = 1
> - driver has selected a specific io_cpu
>
> NVME_TCP_Q_IO_CPU_SET = 0 && io_cpu != WORK_CPU_UNBOUND
> - user has selected a specific io_cpu
>
> NVME_TCP_Q_IO_CPU_SET = 0 && io_cpu == WORK_CPU_UNBOUND
> - unbound; workqueue chooses the execution CPU and so it's driver managed
I drew your scheme as a state diagram to compare it with v4.
DRIVER MANAGED | USER MANAGED
(SET=1, or SET=0 && UNBOUND) | (SET=0 && io_cpu=c)
|
connect +--------------------+ store(c) +--------------------+
------->| A driver chosen |-------+-->| C user pin |
| io_cpu=c, SET=1 | | | io_cpu=c, SET=0 |
| counted | | | not counted (1) |
+--------------------+ | +--------------------+
| stop, SET=0 and | |
| io_cpu must be reset (2) | | store(-1)
v | |
+--------------------+ | |
| B unbound |<------+-----+
| io_cpu=UNBOUND | |
| SET=0 | | +- - - - - - - - - - +
| also where a | | : D user unbound :
| user -1 lands(3)| | : not reachable, :
| | | : see (3) :
+--------------------+ | +- - - - - - - - - - +
| next connect, reselect |
+--> A |
(1) A user pin has SET=0, so it drops out of nvme_tcp_cpu_queues.
When the driver selects io_cpu for other queues and other
controllers in nvme_tcp_set_queue_io_cpu(), it sees that cpu as
idle and keeps placing queues on it. v4 counts every bound queue
regardless of who assigned it.
(2) SET clears at stop. Unless the stop path also resets io_cpu to
WORK_CPU_UNBOUND once io_work is cancelled, a driver chosen io_cpu
reads as a user pin after the reconnect and the driver never
selects a cpu for that queue again. In v4 only the sysfs store
sets the USER bit, so connect and stop never touch ownership.
(3) The user managed side has no unbound state. A -1 written by the
user is the same state as a queue the driver has not bound yet,
so managed reads 1 and the next connect binds it again. Also,
nvme_tcp_wq is a per-cpu workqueue, so an unbound io_work runs
on the cpu that queued it rather than on a cpu chosen by the
workqueue.
The USER bit records who owns io_cpu and survives reconnects, while
SET records whether io_cpu is in the ledger and follows connect and
stop. I'd like to keep both, with USER moved into nvme_tcp_queue flags
as you asked on patch 1. The complexity you point at is the extra
branch in the store, which collapses to a single path with the same
behavior in v5.
mutex_lock(&ctrl->io_cpu_lock);
old = xchg(&queue->io_cpu, cpu);
set_bit(NVME_TCP_Q_IO_CPU_USER, &queue->flags);
if (test_and_clear_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags))
atomic_dec(&nvme_tcp_cpu_queues[old]);
if (cpu != WORK_CPU_UNBOUND &&
test_bit(NVME_TCP_Q_ALLOCATED, &queue->flags)) {
atomic_inc(&nvme_tcp_cpu_queues[cpu]);
set_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags);
}
mutex_unlock(&ctrl->io_cpu_lock);
Does that work for you?
> Other that what sashiko provided in the review feedback, above are my
> few additional comments.
Thanks for the review. v5 also fixes the Sashiko findings.
Thanks,
Saravanan D.
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-10-09 2:01 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-27 7:09 [PATCH v4 0/2] nvme-tcp: allow setting per-queue io_cpu through sysfs Saravanan D
2026-09-27 7:09 ` [PATCH v4 1/2] nvme: add per-queue sysfs directories Saravanan D
2026-10-08 9:59 ` Nilay Shroff
2026-09-27 7:09 ` [PATCH v4 2/2] nvme-tcp: allow setting per-queue io_cpu through sysfs Saravanan D
2026-10-08 9:45 ` Nilay Shroff
2026-10-09 2:01 ` Saravanan D
2026-10-08 5:39 ` [PATCH v4 0/2] " Saravanan D
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®